Added the manage job tagging APi and get List of job tagging per job

This commit is contained in:
ashutosh.nehete 2025-11-17 11:38:25 +05:30
parent 4cec5860ec
commit 08a336be37
12 changed files with 9493 additions and 0 deletions

View File

@ -225,8 +225,11 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<JobTag> JobTags { get; set; }
public DbSet<JobTagMapping> JobTagMappings { get; set; }
public DbSet<JobAttachment> JobAttachments { get; set; }
public DbSet<JobAttendance> JobAttendance { get; set; }
public DbSet<JobAttendanceLog> JobAttendanceLogs { get; set; }
#endregion
#endregion

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,158 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_JobAttendance_Related_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "JobAttendance",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
JobTcketId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Action = table.Column<int>(type: "int", nullable: false),
EmployeeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TaggedInTime = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TaggedOutTime = table.Column<DateTime>(type: "datetime(6)", nullable: true),
TaggedInAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TaggedOutAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_JobAttendance", x => x.Id);
table.ForeignKey(
name: "FK_JobAttendance_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_JobAttendance_JobTickets_JobTcketId",
column: x => x.JobTcketId,
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_JobAttendance_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "JobAttendanceLogs",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
JobAttendanceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
JobTcketId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
DocumentId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
Latitude = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Longitude = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Action = table.Column<int>(type: "int", nullable: false),
Comment = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
EmployeeId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
MarkedTIme = table.Column<DateTime>(type: "datetime(6)", nullable: false),
MarkedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_JobAttendanceLogs", x => x.Id);
table.ForeignKey(
name: "FK_JobAttendanceLogs_Documents_DocumentId",
column: x => x.DocumentId,
principalTable: "Documents",
principalColumn: "Id");
table.ForeignKey(
name: "FK_JobAttendanceLogs_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_JobAttendanceLogs_JobAttendance_JobAttendanceId",
column: x => x.JobAttendanceId,
principalTable: "JobAttendance",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_JobAttendanceLogs_JobTickets_JobTcketId",
column: x => x.JobTcketId,
principalTable: "JobTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_JobAttendanceLogs_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_JobAttendance_EmployeeId",
table: "JobAttendance",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendance_JobTcketId",
table: "JobAttendance",
column: "JobTcketId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendance_TenantId",
table: "JobAttendance",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendanceLogs_DocumentId",
table: "JobAttendanceLogs",
column: "DocumentId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendanceLogs_EmployeeId",
table: "JobAttendanceLogs",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendanceLogs_JobAttendanceId",
table: "JobAttendanceLogs",
column: "JobAttendanceId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendanceLogs_JobTcketId",
table: "JobAttendanceLogs",
column: "JobTcketId");
migrationBuilder.CreateIndex(
name: "IX_JobAttendanceLogs_TenantId",
table: "JobAttendanceLogs",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "JobAttendanceLogs");
migrationBuilder.DropTable(
name: "JobAttendance");
}
}
}

View File

@ -5046,6 +5046,101 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("JobAttachments");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobAttendance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<int>("Action")
.HasColumnType("int");
b.Property<Guid>("EmployeeId")
.HasColumnType("char(36)");
b.Property<Guid>("JobTcketId")
.HasColumnType("char(36)");
b.Property<DateTime>("TaggedInAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("TaggedInTime")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("TaggedOutAt")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("TaggedOutTime")
.HasColumnType("datetime(6)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("EmployeeId");
b.HasIndex("JobTcketId");
b.HasIndex("TenantId");
b.ToTable("JobAttendance");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobAttendanceLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<int>("Action")
.HasColumnType("int");
b.Property<string>("Comment")
.HasColumnType("longtext");
b.Property<Guid?>("DocumentId")
.HasColumnType("char(36)");
b.Property<Guid>("EmployeeId")
.HasColumnType("char(36)");
b.Property<Guid>("JobAttendanceId")
.HasColumnType("char(36)");
b.Property<Guid>("JobTcketId")
.HasColumnType("char(36)");
b.Property<string>("Latitude")
.HasColumnType("longtext");
b.Property<string>("Longitude")
.HasColumnType("longtext");
b.Property<DateTime>("MarkedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("MarkedTIme")
.HasColumnType("datetime(6)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("DocumentId");
b.HasIndex("EmployeeId");
b.HasIndex("JobAttendanceId");
b.HasIndex("JobTcketId");
b.HasIndex("TenantId");
b.ToTable("JobAttendanceLogs");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobComment", b =>
{
b.Property<Guid>("Id")
@ -8108,6 +8203,74 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobAttendance", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee")
.WithMany()
.HasForeignKey("EmployeeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.ServiceProject.JobTicket", "JobTicket")
.WithMany()
.HasForeignKey("JobTcketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Employee");
b.Navigation("JobTicket");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobAttendanceLog", b =>
{
b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document")
.WithMany()
.HasForeignKey("DocumentId");
b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee")
.WithMany()
.HasForeignKey("EmployeeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.ServiceProject.JobAttendance", "JobAttendance")
.WithMany()
.HasForeignKey("JobAttendanceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.ServiceProject.JobTicket", "JobTicket")
.WithMany()
.HasForeignKey("JobTcketId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Document");
b.Navigation("Employee");
b.Navigation("JobAttendance");
b.Navigation("JobTicket");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobComment", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")

View File

@ -0,0 +1,16 @@
using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.ServiceProject
{
public class JobAttendanceDto
{
public Guid JobTcketId { get; set; }
public TAGGING_MARK_TYPE Action { get; set; }
public string? Latitude { get; set; }
public string? Longitude { get; set; }
public string? Comment { get; set; }
public DateTime? MarkedAt { get; set; }
public FileUploadModel? Attachment { get; set; }
}
}

View File

@ -0,0 +1,32 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.ServiceProject
{
public class JobAttendance : TenantRelation
{
public Guid Id { get; set; }
public Guid JobTcketId { get; set; }
[ValidateNever]
[ForeignKey("JobTcketId")]
public JobTicket? JobTicket { get; set; }
public TAGGING_MARK_TYPE Action { get; set; }
public Guid EmployeeId { get; set; }
[ValidateNever]
[ForeignKey("EmployeeId")]
public Employee? Employee { get; set; }
public DateTime TaggedInTime { get; set; }
public DateTime? TaggedOutTime { get; set; }
public DateTime TaggedInAt { get; set; }
public DateTime? TaggedOutAt { get; set; }
}
public enum TAGGING_MARK_TYPE
{
TAG_IN = 0, TAG_OUT = 1
}
}

View File

@ -0,0 +1,39 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.ServiceProject
{
public class JobAttendanceLog : TenantRelation
{
public Guid Id { get; set; }
public Guid JobAttendanceId { get; set; }
[ValidateNever]
[ForeignKey("JobAttendanceId")]
public JobAttendance? JobAttendance { get; set; }
public Guid JobTcketId { get; set; }
[ValidateNever]
[ForeignKey("JobTcketId")]
public JobTicket? JobTicket { get; set; }
public Guid? DocumentId { get; set; }
[ValidateNever]
[ForeignKey("DocumentId")]
public Document? Document { get; set; }
public string? Latitude { get; set; }
public string? Longitude { get; set; }
public TAGGING_MARK_TYPE Action { get; set; }
public string? Comment { get; set; }
public Guid EmployeeId { get; set; }
[ValidateNever]
[ForeignKey("EmployeeId")]
public Employee? Employee { get; set; }
public DateTime MarkedTIme { get; set; }
public DateTime MarkedAt { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.ServiceProject
{
public class JobAttendanceVM
{
public Guid Id { get; set; }
public Guid JobTcketId { get; set; }
public BasicJobTicketVM? JobTicket { get; set; }
public TAGGING_MARK_TYPE Action { get; set; }
public TAGGING_MARK_TYPE? NextAction { get; set; }
public BasicEmployeeVM? Employee { get; set; }
public DateTime TaggedInTime { get; set; }
public DateTime? TaggedOutTime { get; set; }
public DateTime TaggedInAt { get; set; }
public DateTime? TaggedOutAt { get; set; }
}
}

View File

@ -259,5 +259,30 @@ namespace Marco.Pms.Services.Controllers
}
#endregion
#region =================================================================== Job Tagging Functions ===================================================================
[HttpGet("job/attendance/team")]
public async Task<IActionResult> GetAttendanceForJobTeam([FromQuery] Guid jobTicketId, [FromQuery] DateTime? fromDate, [FromQuery] DateTime? toDate)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetAttendanceForJobTeamAsync(jobTicketId, fromDate, toDate, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("job/attendance")]
public async Task<IActionResult> ManageJobTagging(JobAttendanceDto model)
{
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.ManageJobTaggingAsync(model, loggedInEmployee, tenantId);
if (response.Success)
{
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket_Attendance", Response = response.Data };
await _signalR.SendNotificationAsync(notification);
}
return StatusCode(response.StatusCode, response);
}
#endregion
}
}

View File

@ -212,6 +212,11 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<JobComment, JobCommentVM>();
CreateMap<JobAttendance, JobAttendanceVM>()
.ForMember(
dest => dest.NextAction,
opt => opt.MapFrom(src => src.Action == TAGGING_MARK_TYPE.TAG_IN ? TAGGING_MARK_TYPE.TAG_OUT : TAGGING_MARK_TYPE.TAG_IN));
#endregion
#endregion

View File

@ -36,6 +36,11 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> UpdateCommentAsync(Guid id, JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Job Tagging Functions ===================================================================
Task<ApiResponse<object>> GetAttendanceForJobTeamAsync(Guid jobTicketId, DateTime? startDate, DateTime? endDate, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ManageJobTaggingAsync(JobAttendanceDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Helper Functions ===================================================================
Task<JobTicket?> GetJobTicketByIdAsync(Guid id, Guid tenantId);
#endregion

View File

@ -2011,6 +2011,240 @@ namespace Marco.Pms.Services.Service
}
}
#endregion
#region =================================================================== Job Tagging Functions ===================================================================
public async Task<ApiResponse<object>> GetAttendanceForJobTeamAsync(Guid jobTicketId, DateTime? startDate, DateTime? endDate, Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("GetAttendanceForJobTeamAsync called for JobTicketId: {JobTicketId}, TenantId: {TenantId}, EmployeeId: {EmployeeId}", jobTicketId, tenantId, loggedInEmployee.Id);
try
{
// Validate the existence and active status of the job ticket including its status related data
var jobTicket = await _context.JobTickets
.AsNoTracking()
.Include(jt => jt.Status)
.FirstOrDefaultAsync(jt => jt.Id == jobTicketId && jt.TenantId == tenantId && jt.IsActive);
if (jobTicket == null)
{
_logger.LogWarning("JobTicket not found. JobTicketId: {JobTicketId}, TenantId: {TenantId}", jobTicketId, tenantId);
return ApiResponse<object>.ErrorResponse("Job is not found", "Job is not found", 404);
}
// Define date range for attendance query: default from today to next 7 days if not provided
DateTime fromDate = startDate?.Date ?? DateTime.UtcNow.Date;
DateTime toDate = endDate?.Date ?? fromDate.AddDays(7);
// Fetch attendance records within the date range for the specified job ticket and tenant
var attendances = await _context.JobAttendance
.AsNoTracking()
.Include(ja => ja.JobTicket).ThenInclude(jt => jt.Status)
.Include(ja => ja.Employee).ThenInclude(e => e.JobRole)
.Where(ja => ja.JobTcketId == jobTicketId
&& ja.TaggedInTime.Date >= fromDate
&& ja.TaggedInTime.Date <= toDate
&& ja.TenantId == tenantId)
.ToListAsync();
// Map attendance entities to view models
var response = attendances.Select(ja =>
{
var result = _mapper.Map<JobAttendanceVM>(ja);
// Determine if current attendance record is not the latest, if so clear NextAction
var isNotLast = attendances.Any(attendance => attendance.TaggedInTime.Date > ja.TaggedInTime.Date);
if (isNotLast || (ja.TaggedOutTime.HasValue && ja.TaggedInTime.Date != DateTime.UtcNow.Date))
{
result.NextAction = null;
}
return result;
}).ToList();
_logger.LogInfo("Attendance for job team fetched successfully. JobTicketId: {JobTicketId}, RecordsCount: {Count}", jobTicketId, response.Count);
return ApiResponse<object>.SuccessResponse(response, "Attendance for job team fetched successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred in GetAttendanceForJobTeamAsync for JobTicketId: {JobTicketId}, TenantId: {TenantId}", jobTicketId, tenantId);
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500);
}
}
public async Task<ApiResponse<object>> ManageJobTaggingAsync(JobAttendanceDto model, Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("ManageJobTaggingAsync called for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
try
{
// Validate the job ticket existence and status
var jobTicket = await _context.JobTickets
.AsNoTracking()
.FirstOrDefaultAsync(jt => jt.Id == model.JobTcketId && jt.TenantId == tenantId && jt.IsActive);
if (jobTicket == null)
{
_logger.LogWarning("JobTicket not found. JobTicketId: {JobTicketId}, TenantId: {TenantId}", model.JobTcketId, tenantId);
return ApiResponse<object>.ErrorResponse("Job not found", "Job not found", 404);
}
// Check if the current user is part of the job team
var jobEmployeeMapping = await _context.JobEmployeeMappings
.AsNoTracking()
.FirstOrDefaultAsync(jem => jem.JobTicketId == model.JobTcketId && jem.AssigneeId == loggedInEmployee.Id && jem.TenantId == tenantId);
if (jobEmployeeMapping == null)
{
_logger.LogWarning("User is not part of job team. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("User is not part of job team", "User is not part of job team", 400);
}
// Get the last attendance record for the user and job
var jobAttendance = await _context.JobAttendance
.AsNoTracking()
.Where(ja => ja.EmployeeId == loggedInEmployee.Id && ja.JobTcketId == model.JobTcketId)
.OrderByDescending(ja => ja.TaggedInAt)
.FirstOrDefaultAsync();
JobAttendance updateJobAttendance;
DateTime markedAt = model.MarkedAt ?? DateTime.UtcNow;
DateTime currentTime = DateTime.UtcNow;
// Handle TAG_IN action
if (model.Action == TAGGING_MARK_TYPE.TAG_IN)
{
var isLastTaggedOut = jobAttendance != null && jobAttendance.Action == TAGGING_MARK_TYPE.TAG_OUT && jobAttendance.TaggedOutTime != null;
if (jobAttendance == null || (isLastTaggedOut && jobAttendance.TaggedInTime.Date != currentTime.Date))
{
// Create new JobAttendance record for Tag In
var newJobAttendance = new JobAttendance
{
Id = Guid.NewGuid(),
JobTcketId = model.JobTcketId,
EmployeeId = loggedInEmployee.Id,
Action = TAGGING_MARK_TYPE.TAG_IN,
TaggedInTime = markedAt,
TaggedInAt = currentTime,
TenantId = tenantId
};
_context.JobAttendance.Add(newJobAttendance);
updateJobAttendance = newJobAttendance;
_logger.LogInfo("New Tag In created for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
}
else if (isLastTaggedOut && jobAttendance.TaggedInTime.Date == currentTime.Date)
{
// Update the existing last JobAttendance to Tag In
jobAttendance.Action = TAGGING_MARK_TYPE.TAG_IN;
jobAttendance.TaggedInTime = markedAt;
jobAttendance.TaggedInAt = currentTime;
jobAttendance.TaggedOutTime = null;
jobAttendance.TaggedOutAt = null;
_context.JobAttendance.Update(jobAttendance);
updateJobAttendance = jobAttendance;
_logger.LogInfo("Existing JobAttendance updated to Tag In for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
}
else
{
_logger.LogWarning("Attempted to Tag In without tagging out last session. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("First, mark the last tag as out before tagging in.", "First, mark the last tag as out before tagging in.", 400);
}
}
// Handle TAG_OUT action
else if (model.Action == TAGGING_MARK_TYPE.TAG_OUT)
{
if (jobAttendance != null && jobAttendance.Action == TAGGING_MARK_TYPE.TAG_IN && jobAttendance.TaggedOutTime == null)
{
jobAttendance.Action = TAGGING_MARK_TYPE.TAG_OUT;
jobAttendance.TaggedOutTime = markedAt;
jobAttendance.TaggedOutAt = currentTime;
_context.JobAttendance.Update(jobAttendance);
updateJobAttendance = jobAttendance;
_logger.LogInfo("JobAttendance updated to Tag Out for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
}
else
{
_logger.LogWarning("Attempted to Tag Out without previous Tag In. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("First, mark the last tag as in before tagging out.", "First, mark the last tag as in before tagging out.", 400);
}
}
else
{
_logger.LogWarning("Invalid action provided: {Action}. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", model.Action, loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("Provided invalid action", "Provided invalid action", 400);
}
Document? document = null;
// Handle attachment upload if present
if (model.Attachment != null && model.Attachment.ContentType != null)
{
string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
_logger.LogWarning("Base64 data missing in attachment. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("Base64 data is missing", "Attachment data missing", 400);
}
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "job_attendance");
var objectKey = $"tenant-{tenantId}/ServiceProject/{jobTicket.ProjectId}/Job/{jobTicket.Id}/Attendance/{updateJobAttendance.Id}/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
document = new Document
{
BatchId = Guid.NewGuid(),
UploadedById = loggedInEmployee.Id,
FileName = model.Attachment.FileName ?? "",
ContentType = model.Attachment.ContentType,
S3Key = objectKey,
FileSize = model.Attachment.FileSize,
UploadedAt = currentTime,
TenantId = tenantId
};
_context.Documents.Add(document);
_logger.LogInfo("Attachment uploaded and document record created. EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}, DocumentId: {DocumentId}", loggedInEmployee.Id, model.JobTcketId, document.Id);
}
// Create attendance log entry for audit trail
var attendanceLog = new JobAttendanceLog
{
Id = Guid.NewGuid(),
EmployeeId = updateJobAttendance.EmployeeId,
Action = updateJobAttendance.Action,
MarkedTIme = markedAt,
MarkedAt = currentTime,
Latitude = model.Latitude,
Longitude = model.Longitude,
Comment = model.Comment,
JobAttendanceId = updateJobAttendance.Id,
JobTcketId = model.JobTcketId,
DocumentId = document?.Id,
TenantId = tenantId
};
_context.JobAttendanceLogs.Add(attendanceLog);
// Persist all changes in one save operation
await _context.SaveChangesAsync();
var response = _mapper.Map<JobAttendanceVM>(updateJobAttendance);
response.JobTicket = _mapper.Map<BasicJobTicketVM>(jobTicket);
response.Employee = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
_logger.LogInfo("Tagging managed successfully for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.SuccessResponse(response, "Tagging managed successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred in ManageJobTaggingAsync for EmployeeId: {EmployeeId}, JobTicketId: {JobTicketId}", loggedInEmployee.Id, model.JobTcketId);
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500);
}
}
#endregion