343 lines
16 KiB
C#

using System.Data;
using System.Globalization;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Mail;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mail;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Report;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ReportController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly IEmailSender _emailSender;
private readonly ILoggingService _logger;
private readonly UserHelper _userHelper;
private readonly IWebHostEnvironment _env;
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env)
{
_context = context;
_emailSender = emailSender;
_logger = logger;
_userHelper = userHelper;
_env = env;
}
[HttpPost("set-mail")]
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
{
Guid tenantId = _userHelper.GetTenantId();
MailDetails mailDetails = new MailDetails
{
ProjectId = mailDetailsDto.ProjectId,
Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId,
TenantId = tenantId
};
_context.MailDetails.Add(mailDetails);
await _context.SaveChangesAsync();
return Ok("Success");
}
[HttpPost("mail-template")]
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
{
Guid tenantId = _userHelper.GetTenantId();
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
{
_logger.LogWarning("User tries to set email template but send invalid data");
return BadRequest(ApiResponse<object>.ErrorResponse("Provided Invalid data", "Provided Invalid data", 400));
}
var existngTemalate = await _context.MailingList.FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemeplateDto.Title.ToLower());
if (existngTemalate != null)
{
_logger.LogWarning("User tries to set email template, but title already existed in database");
return BadRequest(ApiResponse<object>.ErrorResponse("Email title is already existed", "Email title is already existed", 400));
}
MailingList mailingList = new MailingList
{
Title = mailTemeplateDto.Title,
Body = mailTemeplateDto.Body,
Subject = mailTemeplateDto.Subject,
Keywords = mailTemeplateDto.Keywords,
TenantId = tenantId
};
_context.MailingList.Add(mailingList);
await _context.SaveChangesAsync();
return Ok("Success");
}
[HttpGet("project-statistics")]
public async Task<IActionResult> SendProjectReport()
{
Guid tenantId = _userHelper.GetTenantId();
// Use AsNoTracking() for read-only queries to improve performance
List<MailDetails> mailDetails = await _context.MailDetails
.AsNoTracking()
.Include(m => m.MailBody)
.Where(m => m.TenantId == tenantId)
.ToListAsync();
int successCount = 0;
int notFoundCount = 0;
int invalidIdCount = 0;
var groupedMails = mailDetails
.GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new
{
ProjectId = g.Key.ProjectId,
MailListId = g.Key.MailListId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
})
.ToList();
var semaphore = new SemaphoreSlim(1);
// Using Task.WhenAll to send reports concurrently for better performance
var sendTasks = groupedMails.Select(async mailDetail =>
{
await semaphore.WaitAsync();
try
{
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId);
if (response.StatusCode == 200)
Interlocked.Increment(ref successCount);
else if (response.StatusCode == 404)
Interlocked.Increment(ref notFoundCount);
else if (response.StatusCode == 400)
Interlocked.Increment(ref invalidIdCount);
}
finally
{
semaphore.Release();
}
}).ToList();
await Task.WhenAll(sendTasks);
//var response = await GetProjectStatistics(Guid.Parse("2618eb89-2823-11f0-9d9e-bc241163f504"), "ashutosh.nehete@marcoaiot.com", tenantId);
_logger.LogInfo(
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}",
tenantId, successCount, notFoundCount, invalidIdCount);
return Ok(ApiResponse<object>.SuccessResponse(
new { },
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.",
200));
}
/// <summary>
/// Retrieves project statistics for a given project ID and sends an email report.
/// </summary>
/// <param name="projectId">The ID of the project.</param>
/// <param name="recipientEmail">The email address of the recipient.</param>
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
{
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
if (projectId == Guid.Empty)
{
_logger.LogError("Provided empty project ID while fetching project report.");
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
}
var project = await _context.Projects
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null)
{
_logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId);
return ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404);
}
var statisticReport = new ProjectStatisticReport
{
Date = reportDate,
ProjectName = project.Name ?? "",
TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture)
};
// Preload relevant data
var projectAllocations = await _context.ProjectAllocations
.Include(p => p.Employee)
.Where(p => p.ProjectId == project.Id && p.IsActive)
.ToListAsync();
var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet();
var attendances = await _context.Attendes
.AsNoTracking()
.Where(a => a.ProjectID == project.Id && a.InTime != null && a.InTime.Value.Date == reportDate)
.ToListAsync();
var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet();
var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet();
var regularizationIds = attendances
.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
.Select(a => a.EmployeeID).Distinct().ToHashSet();
// Preload buildings, floors, areas
var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync();
var areaIds = areas.Select(a => a.Id).ToList();
var workItems = await _context.WorkItems
.Include(w => w.ActivityMaster)
.Where(w => areaIds.Contains(w.WorkAreaId))
.ToListAsync();
var itemIds = workItems.Select(i => i.Id).ToList();
var tasks = await _context.TaskAllocations
.Where(t => itemIds.Contains(t.WorkItemId))
.ToListAsync();
var taskIds = tasks.Select(t => t.Id).ToList();
var taskMembers = await _context.TaskMembers
.Include(m => m.Employee)
.Where(m => taskIds.Contains(m.TaskAllocationId))
.ToListAsync();
// Aggregate data
double totalPlannedWork = workItems.Sum(w => w.PlannedWork);
double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask);
var jobRoles = await _context.JobRoles
.Where(j => j.TenantId == project.TenantId)
.ToListAsync();
// Team on site
var teamOnSite = jobRoles
.Select(role =>
{
var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId));
return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count };
})
.OrderByDescending(t => t.NumberofEmployees)
.ToList();
// Task details
var performedTasks = todayAssignedTasks.Select(task =>
{
var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId);
var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId);
var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId);
var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId);
string activityName = workItem?.ActivityMaster?.ActivityName ?? "";
string location = $"{building?.Name} > {floor?.FloorName}</span><br/><span style=\"color: gray; font-size: small; padding-left: 10px;\"> {floor?.FloorName}-{area?.AreaName}";
double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0);
var taskTeam = taskMembers
.Where(m => m.TaskAllocationId == task.Id)
.Select(m =>
{
string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}";
var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId);
return new TaskTeam { Name = name, RoleName = role?.Name ?? "" };
})
.ToList();
return new PerformedTask
{
Activity = activityName,
Location = location,
AssignedToday = task.PlannedTask,
CompletedToday = task.CompletedTask,
Pending = pending,
Comment = task.Description,
Team = taskTeam
};
}).ToList();
// Attendance details
var performedAttendance = attendances.Select(att =>
{
var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID);
var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId);
string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}";
return new PerformedAttendance
{
Name = name,
RoleName = role?.Name ?? "",
InTime = att.InTime ?? DateTime.UtcNow,
OutTime = att.OutTime,
Comment = att.Comment
};
}).ToList();
// Fill report
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
statisticReport.TotalEmployees = assignedEmployeeIds.Count;
statisticReport.RegularizationPending = regularizationIds.Count;
statisticReport.CheckoutPending = checkoutPendingIds.Count;
statisticReport.TotalPlannedWork = totalPlannedWork;
statisticReport.TotalCompletedWork = totalCompletedWork;
statisticReport.TotalPlannedTask = totalPlannedTask;
statisticReport.TotalCompletedTask = totalCompletedTask;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks;
statisticReport.PerformedAttendance = performedAttendance;
// Send Email
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee();
List<MailLog> mailLogs = new List<MailLog>();
foreach (var recipientEmail in recipientEmails)
{
mailLogs.Add(
new MailLog
{
ProjectId = projectId,
EmailId = recipientEmail,
Body = emailBody,
EmployeeId = employee.Id,
TimeStamp = DateTime.UtcNow,
TenantId = tenantId
});
}
_context.MailLogs.AddRange(mailLogs);
await _context.SaveChangesAsync();
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200);
}
}
}