diff --git a/Marco.Pms.Model/ViewModels/ServiceProject/JobTicketDetailsVM.cs b/Marco.Pms.Model/ViewModels/ServiceProject/JobTicketDetailsVM.cs index 36eb7d9..35c03f8 100644 --- a/Marco.Pms.Model/ViewModels/ServiceProject/JobTicketDetailsVM.cs +++ b/Marco.Pms.Model/ViewModels/ServiceProject/JobTicketDetailsVM.cs @@ -16,6 +16,7 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject public DateTime StartDate { get; set; } public DateTime DueDate { get; set; } public bool IsActive { get; set; } + public Guid? AttendanceId { get; set; } public TAGGING_MARK_TYPE? TaggingAction { get; set; } public TAGGING_MARK_TYPE? NextTaggingAction { get; set; } public DateTime CreatedAt { get; set; } diff --git a/Marco.Pms.Services/Service/ServiceProjectService.cs b/Marco.Pms.Services/Service/ServiceProjectService.cs index 7bf793f..e5f163f 100644 --- a/Marco.Pms.Services/Service/ServiceProjectService.cs +++ b/Marco.Pms.Services/Service/ServiceProjectService.cs @@ -746,6 +746,234 @@ namespace Marco.Pms.Services.Service #endregion + //#region =================================================================== Service Project Talking Points Functions =================================================================== + //public async Task> CreateTalkingPointToServiceProjectAsync(TalkingPointDto model, Employee loggedInEmployee, Guid tenantId) + //{ + // var serviceProject = await _context.ServiceProjects.AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == model.ServiceProjectId && sp.TenantId == tenantId); + // if (serviceProject == null) + // { + // return ApiResponse.ErrorResponse("Service project not found", "Service project not found", 404); + // } + // var talkingPoint = _mapper.Map(model); + // talkingPoint.Id = Guid.NewGuid(); + // talkingPoint.IsActive = true; + // talkingPoint.CreatedAt = DateTime.UtcNow; + // talkingPoint.CreatedById = loggedInEmployee.Id; + // talkingPoint.TenantId = tenantId; + + // _context.TalkingPoints.Add(talkingPoint); + + // var documents = new List(); + + // // Handle attachments if provided + // if (model.Attachments?.Any() ?? false) + // { + // var batchId = Guid.NewGuid(); + // var attachments = new List(); + + // foreach (var attachment in model.Attachments) + // { + // string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; + // if (string.IsNullOrWhiteSpace(base64)) + // { + // return ApiResponse.ErrorResponse("Bad Request", "Attachment image data is missing or invalid.", 400); + // } + + // // Determine content type and generate storage keys + // var fileType = _s3Service.GetContentTypeFromBase64(base64); + // var fileName = _s3Service.GenerateFileName(fileType, tenantId, "talking_point"); + // var objectKey = $"tenant-{tenantId}/ServiceProject/{serviceProject.Id}/TalkingPoint/{talkingPoint.Id}/{fileName}"; + + // // Upload file asynchronously to S3 + // await _s3Service.UploadFileAsync(base64, fileType, objectKey); + + // // Create document record for uploaded file + // var document = new Document + // { + // Id = Guid.NewGuid(), + // BatchId = batchId, + // FileName = attachment.FileName ?? fileName, + // ContentType = fileType, + // S3Key = objectKey, + // FileSize = attachment.FileSize, + // UploadedAt = DateTime.UtcNow, + // UploadedById = loggedInEmployee.Id, + // TenantId = tenantId + // }; + // documents.Add(document); + + // // Link document as attachment to the comment + // attachments.Add(new TalkingPointAttachment + // { + // Id = Guid.NewGuid(), + // DocumentId = document.Id, + // TalkingPointId = talkingPoint.Id, + // TenantId = tenantId + // }); + // } + // _context.Documents.AddRange(documents); + // _context.TalkingPointAttachments.AddRange(attachments); + // } + + // var response = _mapper.Map(talkingPoint); + // response.ServiceProject = _mapper.Map(serviceProject); + // response.CreatedBy = _mapper.Map(loggedInEmployee); + + // if (documents.Any()) + // { + // response.Attachments = documents.Select(d => + // { + // var result = _mapper.Map(d); + + // var preSignedUrl = _s3Service.GeneratePreSignedUrl(d.S3Key); + // var thumbPreSignedUrl = !string.IsNullOrWhiteSpace(d.ThumbS3Key) + // ? _s3Service.GeneratePreSignedUrl(d.ThumbS3Key) + // : preSignedUrl; + // result.PreSignedUrl = preSignedUrl; + // result.ThumbPreSignedUrl = thumbPreSignedUrl; + + // return result; + // }).ToList(); + // } + + // return ApiResponse.SuccessResponse(response, "Talking point added to service project", 201); + //} + //public async Task> UpdateTalkingPointAsync(Guid id, TalkingPointDto model, Employee loggedInEmployee, Guid tenantId) + //{ + // // Transaction ensures atomic update of comment and attachments. + // await using var transaction = await _context.Database.BeginTransactionAsync(); + + // if (!model.Id.HasValue || id != model.Id) + // { + // return ApiResponse.ErrorResponse("The Id in the path does not match the Id in the request body.", "The Id in the path does not match the Id in the request body.", 400); + // } + // var serviceProject = await _context.ServiceProjects.AsNoTracking().FirstOrDefaultAsync(sp => sp.Id == model.ServiceProjectId && sp.TenantId == tenantId); + // if (serviceProject == null) + // { + // return ApiResponse.ErrorResponse("Service project not found", "Service project not found", 404); + // } + + // var talkingPoint = await _context.TalkingPoints.AsNoTracking().FirstOrDefaultAsync(tp => tp.Id == model.Id && tp.TenantId == tenantId); + // if (talkingPoint == null) + // { + // return ApiResponse.ErrorResponse("Talking point not found", "Talking point not found", 404); + // } + // _mapper.Map(model, talkingPoint); + + // _context.TalkingPoints.Update(talkingPoint); + + // // Attachment: Add new or remove deleted as specified in DTO + // if (model.Attachments?.Any() == true) + // { + // // New attachments + // var newBillAttachments = model.Attachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList(); + // if (newBillAttachments.Any()) + // { + // var batchId = Guid.NewGuid(); + // var documents = new List(); + // var attachments = new List(); + + // foreach (var attachment in newBillAttachments) + // { + // string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? ""; + // if (string.IsNullOrWhiteSpace(base64)) + // { + // _logger.LogWarning("Missing base64 data for new attachment in comment {CommentId}", id); + // return ApiResponse.ErrorResponse("Bad Request", "Attachment image data is missing or invalid.", 400); + // } + + // // File upload and document creation + // var fileType = _s3Service.GetContentTypeFromBase64(base64); + // var fileName = _s3Service.GenerateFileName(fileType, tenantId, "job_comment"); + // var objectKey = $"tenant-{tenantId}/ServiceProject/{serviceProject.Id}/TalkingPoint/{talkingPoint.Id}/{fileName}"; + // await _s3Service.UploadFileAsync(base64, fileType, objectKey); + + // var document = new Document + // { + // Id = Guid.NewGuid(), + // BatchId = batchId, + // FileName = attachment.FileName ?? fileName, + // ContentType = fileType, + // S3Key = objectKey, + // FileSize = attachment.FileSize, + // UploadedAt = DateTime.UtcNow, + // UploadedById = loggedInEmployee.Id, + // TenantId = tenantId + // }; + // documents.Add(document); + + // // Link document as attachment to the comment + // attachments.Add(new TalkingPointAttachment + // { + // Id = Guid.NewGuid(), + // DocumentId = document.Id, + // TalkingPointId = talkingPoint.Id, + // TenantId = tenantId + // }); + // } + // _context.Documents.AddRange(documents); + // _context.TalkingPointAttachments.AddRange(attachments); + + // try + // { + // await _context.SaveChangesAsync(); + // _logger.LogInfo("{Count} new attachments added to comment {CommentId} by employee {EmployeeId}", newBillAttachments.Count, id, loggedInEmployee.Id); + // } + // catch (DbUpdateException dbEx) + // { + // await transaction.RollbackAsync(); + // _logger.LogError(dbEx, "Database error adding new attachments for comment {CommentId}", id); + // return ApiResponse.ErrorResponse("Database Error", dbEx.Message, 500); + // } + // } + + // // Attachments for deletion + // var deleteBillAttachments = model.Attachments.Where(ba => ba.DocumentId != null && !ba.IsActive).ToList(); + // if (deleteBillAttachments.Any()) + // { + // var documentIds = deleteBillAttachments.Select(d => d.DocumentId!.Value).ToList(); + // try + // { + // await DeleteTalkingPointAttachments(documentIds); + // _logger.LogInfo("{Count} attachments deleted for comment {CommentId} by employee {EmployeeId}", deleteBillAttachments.Count, id, loggedInEmployee.Id); + // } + // catch (DbUpdateException dbEx) + // { + // await transaction.RollbackAsync(); + // _logger.LogError(dbEx, "Database error deleting attachments during comment update {CommentId}", id); + // return ApiResponse.ErrorResponse("Database Error", dbEx.Message, 500); + // } + // catch (Exception ex) + // { + // await transaction.RollbackAsync(); + // _logger.LogError(ex, "General error deleting attachments during comment update {CommentId}", id); + // return ApiResponse.ErrorResponse("Attachment Deletion Error", ex.Message, 500); + // } + // } + // } + + // var talkingPointTask = Task.Run(async () => + // { + // await using var context = await _dbContextFactory.CreateDbContextAsync(); + // return context.TalkingPoints + // .Include(tp => tp.ServiceProject) + // .Include(tp => tp.CreatedBy).ThenInclude(e => e!.JobRole) + // .Include(tp => tp.UpdatedBy).ThenInclude(e => e!.JobRole) + // .AsNoTracking() + // .FirstOrDefaultAsync(tp => tp.Id == id && tp.TenantId == tenantId && tp.IsActive); + // }); + + // var attachmentTask = Task.Run(async () => + // { + // await using var context = await _dbContextFactory.CreateDbContextAsync(); + // return context.TalkingPointAttachments + // .Include(tpa => tpa.Document) + // }); + + // return ApiResponse.SuccessResponse(new { }, "Talking point updated successfully", 200); + //} + //#endregion + #region =================================================================== Job Tickets Functions =================================================================== /// @@ -997,12 +1225,14 @@ namespace Marco.Pms.Services.Service // If no attendance record exists or last record is tagged out or for a different day, prepare a default response with next action TAG_IN if (jobAttendance == null || (jobAttendance.TaggedOutTime.HasValue && jobAttendance.TaggedInTime.Date != DateTime.UtcNow.Date)) { + response.AttendanceId = null; response.NextTaggingAction = TAGGING_MARK_TYPE.TAG_IN; _logger.LogInfo("No current active attendance found for EmployeeId: {EmployeeId}. Prompting to TAG_IN.", loggedInEmployee.Id); } else { // Active attendance exists + response.AttendanceId = jobAttendance.Id; response.TaggingAction = jobAttendance.Action; response.NextTaggingAction = jobAttendance.Action == TAGGING_MARK_TYPE.TAG_IN ? TAGGING_MARK_TYPE.TAG_OUT : TAGGING_MARK_TYPE.TAG_IN; _logger.LogInfo("Latest attendance fetched for EmployeeId: {EmployeeId} on JobTicketId: {JobTicketId}", loggedInEmployee.Id, jobTicket.Id);