826 lines
36 KiB
C#

using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace MarcoBMS.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TaskController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
private readonly CacheUpdateHelper _cache;
private readonly PermissionServices _permissionServices;
public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices,
IHubContext<MarcoHub> signalR, CacheUpdateHelper cache)
{
_context = context;
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_signalR = signalR;
_cache = cache;
_permissionServices = permissionServices;
}
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
}
[HttpPost("assign")]
public async Task<IActionResult> AssignTask([FromBody] AssignTaskDto assignTask)
{
// Validate the incoming model
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("AssignTask failed validation: {@Errors}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
// Retrieve tenant and employee context
var tenantId = GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync();
// Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, employee.Id);
if (!hasPermission)
{
_logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to approve tasks", 403));
}
_logger.LogInfo("Employee {EmployeeId} is assigning a new task", employee.Id);
// Convert DTO to entity and save TaskAllocation
var taskAllocation = assignTask.ToTaskAllocationFromAssignTaskDto(employee.Id, tenantId);
_context.TaskAllocations.Add(taskAllocation);
await _context.SaveChangesAsync();
await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask);
_logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id);
var response = taskAllocation.ToAssignTaskVMFromTaskAllocation();
// Map team members
var teamMembers = new List<TaskMembers>();
if (assignTask.TaskTeam != null && assignTask.TaskTeam.Any())
{
teamMembers = assignTask.TaskTeam.Select(memberId => new TaskMembers
{
TaskAllocationId = taskAllocation.Id,
EmployeeId = memberId,
TenantId = tenantId
}).ToList();
_context.TaskMembers.AddRange(teamMembers);
await _context.SaveChangesAsync();
_logger.LogInfo("Team members added to Task {TaskId}: {@TeamMemberIds}", taskAllocation.Id, assignTask.TaskTeam);
}
// Get team member details
var employeeIds = teamMembers.Select(m => m.EmployeeId).ToList();
var employees = await _context.Employees
.Where(e => employeeIds.Contains(e.Id))
.ToListAsync();
var team = employees.Select(e => e.ToBasicEmployeeVMFromEmployee()).ToList();
response.teamMembers = team;
return Ok(ApiResponse<object>.SuccessResponse(response, "Task assigned successfully", 200));
}
[HttpPost("report")]
public async Task<IActionResult> ReportTaskProgress([FromBody] ReportTaskDto reportTask)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("Task report validation failed: {@Errors}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to report tasks", 403));
}
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == reportTask.Id);
if (taskAllocation == null)
{
_logger.LogWarning("No task allocation found with ID {TaskId}", reportTask.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("No such task has been allocated.", "No such task has been allocated.", 400));
}
var checkListIds = reportTask.CheckList?.Select(c => c.Id).ToList() ?? new List<Guid>();
var checkList = await _context.ActivityCheckLists
.Where(c => checkListIds.Contains(c.Id))
.ToListAsync();
if (taskAllocation.WorkItem != null)
{
if (taskAllocation.CompletedTask > 0)
taskAllocation.WorkItem.CompletedWork -= taskAllocation.CompletedTask;
taskAllocation.WorkItem.CompletedWork += reportTask.CompletedTask;
}
taskAllocation.ParentTaskId = reportTask.ParentTaskId;
taskAllocation.ReportedDate = reportTask.ReportedDate;
taskAllocation.ReportedById = loggedInEmployee.Id;
taskAllocation.CompletedTask = reportTask.CompletedTask;
//taskAllocation.ReportedTask = reportTask.CompletedTask;
var checkListMappings = new List<CheckListMappings>();
var checkListVMs = new List<CheckListVM>();
if (reportTask.CheckList != null)
{
var activityId = taskAllocation.WorkItem?.ActivityId ?? Guid.Empty;
foreach (var checkDto in reportTask.CheckList)
{
checkListVMs.Add(checkDto.ToCheckListVMFromReportCheckListDto(activityId));
if (checkDto.IsChecked && checkList.Any(c => c.Id == checkDto.Id))
{
checkListMappings.Add(new CheckListMappings
{
CheckListId = checkDto.Id,
TaskAllocationId = reportTask.Id
});
}
}
_context.CheckListMappings.AddRange(checkListMappings);
}
var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id);
_context.TaskComments.Add(comment);
int numberofImages = 0;
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
var batchId = Guid.NewGuid();
var projectId = building?.ProjectId;
if (reportTask.Images?.Any() == true)
{
foreach (var image in reportTask.Images)
{
if (string.IsNullOrEmpty(image.Base64Data))
{
_logger.LogWarning("Image upload failed: Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
var base64 = image.Base64Data.Contains(',')
? image.Base64Data[(image.Base64Data.IndexOf(",") + 1)..]
: image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report");
var objectKey = $"tenant-{tenantId}/project-{projectId}/Actitvity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document
{
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = image.FileName ?? "",
ContentType = image.ContentType ?? "",
S3Key = objectKey,
//Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = reportTask.Id
};
_context.TaskAttachments.Add(attachment);
numberofImages += 1;
}
}
await _context.SaveChangesAsync();
var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty;
await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask);
await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask);
var response = taskAllocation.ToReportTaskVMFromTaskAllocation();
var comments = await _context.TaskComments
.Where(c => c.TaskAllocationId == taskAllocation.Id)
.ToListAsync();
response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList();
response.checkList = checkListVMs;
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Task reported successfully", 200));
}
[HttpPost("comment")]
public async Task<IActionResult> AddCommentForTask([FromBody] CreateCommentDto createComment)
{
_logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId);
var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Validate Task Allocation and associated WorkItem
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == createComment.TaskAllocationId);
if (taskAllocation == null || taskAllocation.WorkItem == null)
{
_logger.LogWarning("Invalid task allocation or work item not found.");
return BadRequest(ApiResponse<object>.ErrorResponse("No such task has been allocated.", "No such task has been allocated.", 400));
}
// Fetch WorkArea and Building (if available)
var workArea = await _context.WorkAreas
.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == taskAllocation.WorkItem.WorkAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty;
var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId);
var projectId = building?.ProjectId;
// Save comment
var comment = createComment.ToCommentFromCommentDto(tenantId, loggedInEmployee.Id);
_context.TaskComments.Add(comment);
await _context.SaveChangesAsync();
_logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id);
// Process image uploads
var images = createComment.Images;
var batchId = Guid.NewGuid();
int numberofImages = 0;
if (images != null && images.Any())
{
foreach (var image in images)
{
if (string.IsNullOrWhiteSpace(image.Base64Data))
{
_logger.LogWarning("Missing Base64 data in one of the images.");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
// Clean base64 string
var base64 = image.Base64Data.Contains(",")
? image.Base64Data.Substring(image.Base64Data.IndexOf(",") + 1)
: image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
var document = new Document
{
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = image.FileName ?? string.Empty,
ContentType = image.ContentType ?? fileType,
S3Key = objectKey,
//Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = comment.Id
};
_context.TaskAttachments.Add(attachment);
numberofImages += 1;
}
await _context.SaveChangesAsync();
_logger.LogInfo("Documents and attachments saved for commentId: {CommentId}", comment.Id);
}
// Convert to view model and return response
var response = comment.ToCommentVMFromTaskComment();
_logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id);
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(response, "Comment saved successfully", 200));
}
[HttpGet("list")]
public async Task<IActionResult> GetTasksList([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{
_logger.LogInfo("GetTasksList called for projectId: {ProjectId}, dateFrom: {DateFrom}, dateTo: {DateTo}", projectId, dateFrom ?? "", dateTo ?? "");
Guid tenantId = GetTenantId();
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
// Parse and validate dateFrom
if (dateFrom != null && !DateTime.TryParse(dateFrom, out fromDate))
{
_logger.LogWarning("Invalid starting date provided: {DateFrom}", dateFrom);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400));
}
// Parse and validate dateTo
if (dateTo != null && !DateTime.TryParse(dateTo, out toDate))
{
_logger.LogWarning("Invalid ending date provided: {DateTo}", dateTo);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid ending date.", "Invalid ending date.", 400));
}
// 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
.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) &&
t.AssignmentDate.Date >= fromDate.Date &&
t.AssignmentDate.Date <= toDate.Date &&
t.TenantId == tenantId)
.ToListAsync();
var taskIds = taskAllocations.Select(t => t.Id).ToList();
// 6. 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
_logger.LogInfo("Fetching comments and attachments.");
var allComments = await _context.TaskComments
.Include(c => c.Employee)
.Where(c => taskIds.Contains(c.TaskAllocationId))
.ToListAsync();
var commentIds = allComments.Select(c => c.Id).ToList();
// 8. 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
var documents = await _context.Documents
.Where(d => documentIds.Contains(d.Id))
.ToListAsync();
var tasks = new List<ListTaskVM>();
_logger.LogInfo("Constructing task response data.");
foreach (var taskAllocation in taskAllocations)
{
var response = taskAllocation.ToListTaskVMFromTaskAllocation();
// Attach documents to the main task
var taskDocIds = attachments
.Where(a => a.ReferenceId == taskAllocation.Id)
.Select(a => a.DocumentId)
.ToList();
var taskDocs = documents
.Where(d => taskDocIds.Contains(d.Id))
.ToList();
response.ReportedPreSignedUrls = taskDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
// Add team members
var taskMemberEntries = teamMembers
.Where(m => m.TaskAllocationId == taskAllocation.Id)
.ToList();
response.teamMembers = taskMemberEntries
.Select(m => m.Employee)
.Where(e => e != null)
.Select(e => e!.ToBasicEmployeeVMFromEmployee())
.ToList();
// Add comments with attachments
var commentVMs = new List<CommentVM>();
var taskComments = allComments
.Where(c => c.TaskAllocationId == taskAllocation.Id)
.ToList();
foreach (var comment in taskComments)
{
var commentDocIds = attachments
.Where(a => a.ReferenceId == comment.Id)
.Select(a => a.DocumentId)
.ToList();
var commentDocs = documents
.Where(d => commentDocIds.Contains(d.Id))
.ToList();
var commentVm = comment.ToCommentVMFromTaskComment();
commentVm.PreSignedUrls = commentDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
commentVMs.Add(commentVm);
}
response.comments = commentVMs;
// Checklists
var activityId = taskAllocation.WorkItem?.ActivityId ?? Guid.Empty;
var checkLists = await _context.ActivityCheckLists
.Where(c => c.ActivityId == activityId)
.ToListAsync();
var checkListMappings = await _context.CheckListMappings
.Where(c => c.TaskAllocationId == taskAllocation.Id)
.ToListAsync();
response.CheckList = checkLists.Select(check =>
{
var isChecked = checkListMappings.Any(m => m.CheckListId == check.Id);
return check.ToCheckListVMFromActivityCheckList(check.ActivityId, isChecked);
}).ToList();
tasks.Add(response);
}
_logger.LogInfo("Task list constructed successfully. Returning {Count} tasks.", tasks.Count);
return Ok(ApiResponse<object>.SuccessResponse(tasks, "Success", 200));
}
[HttpGet("get/{taskId}")]
public async Task<IActionResult> GetTask(Guid taskId)
{
_logger.LogInfo("GetTask called with taskId: {TaskId}", taskId);
// Validate input
if (taskId == Guid.Empty)
{
_logger.LogWarning("Invalid taskId provided.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invalid data", 400));
}
// Fetch Task Allocation with required related data
var taskAllocation = await _context.TaskAllocations
.Include(t => t.Tenant)
.Include(t => t.Employee)
.Include(t => t.ReportedBy)
.Include(t => t.ApprovedBy)
.Include(t => t.WorkItem)
.Include(t => t.WorkStatus)
.FirstOrDefaultAsync(t => t.Id == taskId);
if (taskAllocation == null)
{
_logger.LogWarning("Task not found for taskId: {TaskId}", taskId);
return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not found", 404));
}
if (taskAllocation.Employee == null || taskAllocation.Tenant == null)
{
_logger.LogWarning("Task found but missing Employee or Tenant data for taskId: {TaskId}", taskId);
return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not found", 404));
}
_logger.LogInfo("Task allocation found. Preparing response.");
var taskVM = taskAllocation.TaskAllocationToTaskVM();
// Fetch comments and attachments
_logger.LogInfo("Fetching comments and attachments for taskId: {TaskId}", taskId);
var comments = await _context.TaskComments
.Where(c => c.TaskAllocationId == taskId)
.Include(c => c.Employee)
.ToListAsync();
var commentIds = comments.Select(c => c.Id).ToList();
var taskAttachments = await _context.TaskAttachments
.Where(t => t.ReferenceId == taskId || commentIds.Contains(t.ReferenceId))
.ToListAsync();
var documentIds = taskAttachments.Select(t => t.DocumentId).Distinct().ToList();
var documents = await _context.Documents
.Where(d => documentIds.Contains(d.Id))
.ToListAsync();
// Fetch team members
_logger.LogInfo("Fetching team members for taskId: {TaskId}", taskId);
var team = await _context.TaskMembers
.Where(m => m.TaskAllocationId == taskId)
.Include(m => m.Employee)
.ToListAsync();
var teamMembers = team
.Where(m => m.Employee != null)
.Select(m => m.Employee!.ToBasicEmployeeVMFromEmployee())
.ToList();
taskVM.TeamMembers = teamMembers;
// Attach documents to the main task
_logger.LogInfo("Generating presigned URLs for task documents.");
var taskDocumentIds = taskAttachments
.Where(t => t.ReferenceId == taskId)
.Select(t => t.DocumentId)
.ToList();
var taskDocuments = documents
.Where(d => taskDocumentIds.Contains(d.Id))
.ToList();
taskVM.PreSignedUrls = taskDocuments
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
// Construct CommentVM list with document URLs
_logger.LogInfo("Preparing comment response data.");
var commentVMs = comments.Select(comment =>
{
var commentDocIds = taskAttachments
.Where(t => t.ReferenceId == comment.Id)
.Select(t => t.DocumentId)
.ToList();
var commentDocs = documents
.Where(d => commentDocIds.Contains(d.Id))
.ToList();
var commentVM = comment.ToCommentVMFromTaskComment();
commentVM.PreSignedUrls = commentDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
return commentVM;
}).ToList();
taskVM.Comments = commentVMs;
_logger.LogInfo("Task details prepared successfully for taskId: {TaskId}", taskId);
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
}
/// <summary>
/// Approves a reported task after validation, updates status, and stores attachments/comments.
/// </summary>
/// <param name="approveTask">DTO containing task approval details.</param>
/// <returns>IActionResult indicating success or failure.</returns>
[HttpPost("approve")]
public async Task<IActionResult> ApproveTask(ApproveTaskDto approveTask)
{
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("Employee {EmployeeId} is attempting to approve Task {TaskId}", loggedInEmployee.Id, approveTask.Id);
// Fetch task allocation with work item, only if it's reported
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == approveTask.Id && t.TenantId == tenantId && t.ReportedDate != null);
if (taskAllocation == null)
{
_logger.LogWarning("Task {TaskId} not found or not reported yet for Tenant {TenantId} by Employee {EmployeeId}",
approveTask.Id, tenantId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Task not found", "Task not found", 404));
}
// Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.ApproveTask, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to approve tasks", 403));
}
// Validation: Approved task count cannot exceed completed task count
if (taskAllocation.CompletedTask < approveTask.ApprovedTask)
{
_logger.LogWarning("Invalid approval attempt on Task {TaskId}: Approved tasks ({ApprovedTask}) > Completed tasks ({CompletedTask})",
approveTask.Id, approveTask.ApprovedTask, taskAllocation.CompletedTask);
return BadRequest(ApiResponse<object>.ErrorResponse("Approved tasks cannot be greater than completed tasks",
"Approved tasks cannot be greater than completed tasks", 400));
}
//// Update completed work in the associated work item, if it exists
//if (taskAllocation.WorkItem != null && taskAllocation.CompletedTask != approveTask.ApprovedTask)
//{
// if (taskAllocation.CompletedTask > 0)
// {
// taskAllocation.WorkItem.CompletedWork -= taskAllocation.CompletedTask;
// }
// taskAllocation.WorkItem.CompletedWork += approveTask.ApprovedTask;
//}
// Update task allocation details
taskAllocation.ApprovedById = loggedInEmployee.Id;
taskAllocation.ApprovedDate = DateTime.UtcNow;
taskAllocation.WorkStatusId = approveTask.WorkStatus;
taskAllocation.ReportedTask = approveTask.ApprovedTask;
// Add a comment (optional)
var comment = new TaskComment
{
TaskAllocationId = taskAllocation.Id,
CommentDate = DateTime.UtcNow,
Comment = approveTask.Comment ?? string.Empty,
CommentedBy = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TaskComments.Add(comment);
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
var projectId = building?.ProjectId;
int numberofImages = 0;
// Handle image attachments, if any
if (approveTask.Images?.Count > 0)
{
var batchId = Guid.NewGuid();
foreach (var image in approveTask.Images)
{
if (string.IsNullOrEmpty(image.Base64Data))
{
_logger.LogWarning("Image for Task {TaskId} is missing base64 data", approveTask.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
var base64 = image.Base64Data.Contains(",") ? image.Base64Data[(image.Base64Data.IndexOf(",") + 1)..] : image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document
{
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = fileName,
ContentType = image.ContentType ?? string.Empty,
S3Key = objectKey,
//Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = comment.Id
};
_context.TaskAttachments.Add(attachment);
_logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName);
numberofImages += 1;
}
}
// Commit all changes to the database
await _context.SaveChangesAsync();
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse("Task has been approved", "Task has been approved", 200));
}
}
}