Added the API to get list of jobs

This commit is contained in:
ashutosh.nehete 2025-11-13 11:12:04 +05:30
parent 6e945cf6c1
commit 4a0144c23b
8 changed files with 8655 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Removed_Assignee_From_JobTicket_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_JobTickets_Employees_AssigneeId",
table: "JobTickets");
migrationBuilder.DropIndex(
name: "IX_JobTickets_AssigneeId",
table: "JobTickets");
migrationBuilder.DropColumn(
name: "AssigneeId",
table: "JobTickets");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "AssigneeId",
table: "JobTickets",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_JobTickets_AssigneeId",
table: "JobTickets",
column: "AssigneeId");
migrationBuilder.AddForeignKey(
name: "FK_JobTickets_Employees_AssigneeId",
table: "JobTickets",
column: "AssigneeId",
principalTable: "Employees",
principalColumn: "Id");
}
}
}

View File

@ -5291,9 +5291,6 @@ namespace Marco.Pms.DataAccess.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid?>("AssigneeId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@ -5334,8 +5331,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasKey("Id");
b.HasIndex("AssigneeId");
b.HasIndex("CreatedById");
b.HasIndex("ProjectId");
@ -8086,10 +8081,6 @@ namespace Marco.Pms.DataAccess.Migrations
modelBuilder.Entity("Marco.Pms.Model.ServiceProject.JobTicket", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "Assignee")
.WithMany()
.HasForeignKey("AssigneeId");
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
@ -8118,8 +8109,6 @@ namespace Marco.Pms.DataAccess.Migrations
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("Assignee");
b.Navigation("CreatedBy");
b.Navigation("Project");

View File

@ -15,11 +15,6 @@ namespace Marco.Pms.Model.ServiceProject
[ValidateNever]
[ForeignKey("ProjectId")]
public ServiceProject? Project { get; set; }
public Guid? AssigneeId { get; set; }
[ValidateNever]
[ForeignKey("AssigneeId")]
public Employee? Assignee { get; set; }
public Guid StatusId { get; set; }
[ValidateNever]

View File

@ -1,6 +1,5 @@
using Marco.Pms.Model.Dtos.ServiceProject;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.ServiceProject;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
@ -35,11 +34,6 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("list")]
public async Task<IActionResult> GetServiceProjectList([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetServiceProjectListAsync(pageNumber, pageSize, loggedInEmploee, tenantId);
return StatusCode(response.StatusCode, response);
@ -49,11 +43,6 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("details/{id}")]
public async Task<IActionResult> GetServiceProjectDetails(Guid id)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetServiceProjectDetailsAsync(id, loggedInEmploee, tenantId);
return StatusCode(response.StatusCode, response);
@ -63,11 +52,6 @@ namespace Marco.Pms.Services.Controllers
[HttpPost("create")]
public async Task<IActionResult> CreateProject([FromBody] ServiceProjectDto serviceProject)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.CreateServiceProjectAsync(serviceProject, loggedInEmploee, tenantId);
if (response.Success)
@ -83,11 +67,6 @@ namespace Marco.Pms.Services.Controllers
[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateServicecProject(Guid id, [FromBody] ServiceProjectDto serviceProject)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.UpdateServiceProjectAsync(id, serviceProject, loggedInEmploee, tenantId);
if (response.Success)
@ -102,11 +81,6 @@ namespace Marco.Pms.Services.Controllers
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeActivateServiceProject(Guid id, bool isActive = false)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.DeActivateServiceProjectAsync(id, isActive, loggedInEmploee, tenantId);
if (response.Success)
@ -120,14 +94,18 @@ namespace Marco.Pms.Services.Controllers
#region =================================================================== Job Tickets Functions ===================================================================
[HttpGet("job/list")]
public async Task<IActionResult> GetJobTicketsList([FromQuery] Guid? projectId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20, [FromQuery] bool isActive = true)
{
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetJobTicketsListAsync(projectId, pageNumber, pageSize, isActive, tenantId, loggedInEmploee);
return StatusCode(response.StatusCode, response);
}
[HttpPost("job/create")]
public async Task<IActionResult> CreateJobTicket(CreateJobTicketDto model)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmploee, tenantId);
if (response.Success)

View File

@ -16,6 +16,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
#endregion
#region =================================================================== Job Tickets Functions ===================================================================
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
}

View File

@ -362,6 +362,139 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Job Tickets Functions ===================================================================
/// <summary>
/// Retrieves a paginated, filtered list of job tickets for a tenant, including related project, status, assignees, and tags.
/// </summary>
/// <param name="projectId">Optional project filter.</param>
/// <param name="pageNumber">Page index (1-based).</param>
/// <param name="pageSize">Page size.</param>
/// <param name="isActive">Active filter.</param>
/// <param name="tenantId">Tenant context.</param>
/// <param name="loggedInEmployee">Employee requesting data.</param>
/// <returns>Paged list of JobTicketVM plus metadata, or error response.</returns>
public async Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Guid tenantId, Employee loggedInEmployee)
{
if (tenantId == Guid.Empty)
{
_logger.LogWarning("TenantId missing for job ticket fetch by employee {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse(
"Access Denied",
"Missing or invalid tenant context.",
403);
}
if (pageNumber < 1 || pageSize < 1)
{
_logger.LogInfo("Invalid paging parameters for job ticket fetch. PageNumber: {PageNumber}, PageSize: {PageSize}", pageNumber, pageSize);
return ApiResponse<object>.ErrorResponse(
"Bad Request",
"Page number and size must be greater than zero.",
400);
}
try
{
// Build filtered query with necessary includes for eager loading
var jobTicketQuery = _context.JobTickets
.Include(jt => jt.Status)
.Include(jt => jt.Project)
.Include(jt => jt.CreatedBy).ThenInclude(e => e!.JobRole)
.AsNoTracking()
.Where(jt =>
jt.TenantId == tenantId &&
jt.IsActive == isActive &&
jt.Project != null &&
jt.Status != null &&
jt.CreatedBy != null &&
jt.CreatedBy.JobRole != null);
// Optionally filter by project
if (projectId.HasValue)
{
var projectExists = await _context.ServiceProjects
.AnyAsync(sp => sp.Id == projectId && sp.TenantId == tenantId && sp.IsActive);
if (!projectExists)
{
_logger.LogWarning("Requested service project not found. ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId);
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found for this tenant.", 404);
}
jobTicketQuery = jobTicketQuery.Where(jt => jt.ProjectId == projectId.Value);
}
// Total results and paging
var totalEntities = await jobTicketQuery.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
// Fetch filtered/paged tickets
var jobTickets = await jobTicketQuery
.OrderByDescending(e => e.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var jobTicketIds = jobTickets.Select(jt => jt.Id).ToList();
// Fetch assignee and tag mappings concurrently using separate DbContexts for parallel IO
var assigneeTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.JobEmployeeMappings
.Include(jem => jem.Assignee).ThenInclude(e => e!.JobRole)
.Where(jem => jobTicketIds.Contains(jem.JobTicketId) && jem.Assignee != null && jem.Assignee.JobRole != null && jem.TenantId == tenantId)
.ToListAsync();
});
var tagTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.JobTagMappings
.Include(jtm => jtm.JobTag)
.Where(jtm => jobTicketIds.Contains(jtm.JobTicketId) && jtm.JobTag != null && jtm.TenantId == tenantId)
.ToListAsync();
});
await Task.WhenAll(assigneeTask, tagTask);
var assigneeMappings = assigneeTask.Result;
var tagMappings = tagTask.Result;
// Map tickets to view models and inject assignees/tags per ticket
var jobTicketVMs = jobTickets.Select(jt =>
{
var vm = _mapper.Map<JobTicketVM>(jt);
vm.Assignees = assigneeMappings
.Where(jem => jem.JobTicketId == jt.Id)
.Select(jem => _mapper.Map<BasicEmployeeVM>(jem.Assignee))
.ToList();
vm.Tags = tagMappings
.Where(jtm => jtm.JobTicketId == jt.Id)
.Select(jtm => _mapper.Map<TagVM>(jtm.JobTag))
.ToList();
return vm;
}).ToList();
var response = new
{
CurrentPage = pageNumber,
TotalPages = totalPages,
TotalEntities = totalEntities,
Data = jobTicketVMs,
};
_logger.LogInfo("Job tickets fetched: {Count} tickets for tenant {TenantId}, page {PageNumber}", jobTicketVMs.Count, tenantId, pageNumber);
return ApiResponse<object>.SuccessResponse(response, $"{jobTicketVMs.Count} job records fetched successfully.", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in fetching job tickets list for tenant {TenantId} by employee {EmployeeId}", tenantId, loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Internal Server Error", "Unable to fetch job tickets list. Please try again later.", 500);
}
}
/// <summary>
/// Creates a new job ticket with optional assignees and tags within a transactional scope.
/// </summary>

View File

@ -9,7 +9,7 @@
"Title": "Dev"
},
"ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMSOrg"
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1"
},
"SmtpSettings": {
"SmtpServer": "smtp.gmail.com",