diff --git a/src/dotnet/HQ.Abstractions/Emails/EmployeeHoursEmail.cs b/src/dotnet/HQ.Abstractions/Emails/EmployeeHoursEmail.cs new file mode 100644 index 00000000..8de8f960 --- /dev/null +++ b/src/dotnet/HQ.Abstractions/Emails/EmployeeHoursEmail.cs @@ -0,0 +1,62 @@ + +using HQ.Abstractions.Staff; +using HQ.Abstractions.Times; + +namespace HQ.Abstractions.Emails +{ + public class EmployeeHoursEmail : NotificationEmail + { + //This will contain staff name and hours + public List Staff { get; set; } = null!; + public DateOnly Date { get; set; } + + public DateOnly PeriodBegin { get; set; } + public DateOnly PeriodEnd { get; set; } + + public static new EmployeeHoursEmail Sample = new() + { + Heading = "Staff Hours", + Message = "Staff with red signify they have less working hours than expected", + ButtonLabel = "Open HQ", + ButtonUrl = new Uri("http://hq.localhost:4200/dashboard"), + Date = new DateOnly(2024, 7, 17), + PeriodBegin = new DateOnly(2024, 7, 17), + PeriodEnd = new DateOnly(2024, 7, 23), + + Staff = new List{ + new StaffHoursModel{ + StaffName = "Amr", + HoursLastWeek= 0, + HoursLastMonth = 0, + HoursThisMonth = 4, + MissingHours = true + }, + new StaffHoursModel{ + StaffName = "Ryan", + HoursLastWeek= 5, + HoursLastMonth = 6, + HoursThisMonth = 7, + LessThanExpectedHours = true + }, + new StaffHoursModel{ + StaffName = "Pri", + HoursLastWeek= 8, + HoursLastMonth = 9, + HoursThisMonth = 10 + } + } + + }; + public class StaffHoursModel + { + public decimal HoursLastWeek { get; set; } + public decimal HoursLastMonth { get; set; } + public decimal HoursThisMonth { get; set; } + public string? StaffName { get; set; } + public bool MissingHours { get; set; } + public bool LessThanExpectedHours { get; set; } + public int WorkHours { get; set; } + + } + } +} \ No newline at end of file diff --git a/src/dotnet/HQ.Abstractions/Enumerations/EmailMessage.cs b/src/dotnet/HQ.Abstractions/Enumerations/EmailMessage.cs index 267bc367..b79da451 100644 --- a/src/dotnet/HQ.Abstractions/Enumerations/EmailMessage.cs +++ b/src/dotnet/HQ.Abstractions/Enumerations/EmailMessage.cs @@ -4,5 +4,6 @@ public enum EmailMessage { Notification = 1, RejectTimeEntry = 2, - UpdatedPlanningPoints = 3 + UpdatedPlanningPoints = 3, + EmployeeHours = 4 } \ No newline at end of file diff --git a/src/dotnet/HQ.Email/Views/Emails/HTML/EmployeeHours.cshtml b/src/dotnet/HQ.Email/Views/Emails/HTML/EmployeeHours.cshtml new file mode 100644 index 00000000..46464e72 --- /dev/null +++ b/src/dotnet/HQ.Email/Views/Emails/HTML/EmployeeHours.cshtml @@ -0,0 +1,37 @@ +@model HQ.Abstractions.Emails.EmployeeHoursEmail +@{ + Layout = "_LayoutEmailNotificationHTML"; +} + +
Dates: @Model.PeriodBegin - @Model.PeriodEnd
+
+ + + Staff + Last Week + This Month + Last Month + + @foreach (var staff in Model.Staff) + { + @if (staff.LessThanExpectedHours || staff.MissingHours) + { + + @staff.StaffName + @staff.HoursLastWeek + @staff.HoursThisMonth + @staff.HoursLastMonth + + } + else + { + + @staff.StaffName + @staff.HoursLastWeek + @staff.HoursThisMonth + @staff.HoursLastMonth + + } + } + + \ No newline at end of file diff --git a/src/dotnet/HQ.Email/Views/Emails/Text/EmployeeHours.cshtml b/src/dotnet/HQ.Email/Views/Emails/Text/EmployeeHours.cshtml new file mode 100644 index 00000000..6f7d37ae --- /dev/null +++ b/src/dotnet/HQ.Email/Views/Emails/Text/EmployeeHours.cshtml @@ -0,0 +1,53 @@ +@model HQ.Abstractions.Emails.EmployeeHoursEmail +@if (!String.IsNullOrEmpty(Model.Heading)) +{ + @Model.Heading + --- + +} + +@if (!String.IsNullOrEmpty(Model.Message)) +{ + @Model.Message + +} + +Dates: @Model.PeriodBegin - @Model.PeriodEnd + + + + + + + + + @foreach (var staff in @Model.Staff) + { + @if (staff.LessThanExpectedHours || staff.MissingHours) + { + + + + + + + + + } + else + { + + + + + + + } + + } + + @if (!String.IsNullOrEmpty(Model.ButtonLabel) && Model.ButtonUrl != null) + { + @Model.ButtonLabel: @Model.ButtonUrl + } +
StaffLast WeekThis MonthLast Month
@staff.StaffName@staff.HoursLastWeek@staff.HoursThisMonth@staff.HoursLastMonth
@staff.StaffName@staff.HoursLastWeek@staff.HoursThisMonth@staff.HoursLastMonth
\ No newline at end of file diff --git a/src/dotnet/HQ.Server/Controllers/EmailTemplateTestControllerV1.cs b/src/dotnet/HQ.Server/Controllers/EmailTemplateTestControllerV1.cs index 83436522..2a335f75 100644 --- a/src/dotnet/HQ.Server/Controllers/EmailTemplateTestControllerV1.cs +++ b/src/dotnet/HQ.Server/Controllers/EmailTemplateTestControllerV1.cs @@ -103,6 +103,26 @@ public Task UpdatedPlanningPointsSendEmail([FromForm] string to, C _emailService.SendEmail(EmailMessage.UpdatedPlanningPoints, UpdatedPlanningPointsEmail.Sample, to, "Updated Points Test", System.Net.Mail.MailPriority.Low, null, ct) .ToActionResult(new HQResultEndpointProfile()); + [HttpGet(nameof(StaffHoursText))] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + public Task StaffHoursText(CancellationToken ct = default) => + GetEmailTemplate(EmailMessageOutput.Text, EmailMessage.EmployeeHours, EmployeeHoursEmail.Sample, ct); + + [HttpGet(nameof(StaffHoursMJML))] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + public Task StaffHoursMJML(CancellationToken ct = default) => + GetEmailTemplate(EmailMessageOutput.MJML, EmailMessage.EmployeeHours, EmployeeHoursEmail.Sample, ct); + + [HttpGet(nameof(StaffHoursHTML))] + [ProducesResponseType(StatusCodes.Status200OK, "text/html")] + public Task StaffHoursHTML(CancellationToken ct = default) => + GetEmailTemplate(EmailMessageOutput.HTML, EmailMessage.EmployeeHours, EmployeeHoursEmail.Sample, ct); + + [HttpPost(nameof(StaffHoursSendEmail))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public Task StaffHoursSendEmail([FromForm] string to, CancellationToken ct = default) => + _emailService.SendEmail(EmailMessage.EmployeeHours, EmployeeHoursEmail.Sample, to, "Staff Hours Test", System.Net.Mail.MailPriority.Low, null, ct) + .ToActionResult(new HQResultEndpointProfile()); private async Task GetEmailTemplate(EmailMessageOutput output, EmailMessage emailMessage, T model, CancellationToken ct = default) where T : BaseEmail { var request = new GetEmailTemplateV1.Request() diff --git a/src/dotnet/HQ.Server/Program.cs b/src/dotnet/HQ.Server/Program.cs index 784eb9cb..7a0cf094 100644 --- a/src/dotnet/HQ.Server/Program.cs +++ b/src/dotnet/HQ.Server/Program.cs @@ -373,6 +373,12 @@ Cron.Weekly(DayOfWeek.Monday, 12), recurringJobOptions); +recurringJobManager.AddOrUpdate( + nameof(EmailMessageService.SendEmployeeHoursEmail), + (t) => t.SendEmployeeHoursEmail(CancellationToken.None), + "15 12 * * 1", + recurringJobOptions); + // Friday morning recurringJobManager.AddOrUpdate( nameof(HolidayServiceV1.BackgroundAutoGenerateHolidayTimeEntryV1), @@ -387,10 +393,10 @@ recurringJobOptions); recurringJobManager.AddOrUpdate( -nameof(PointServiceV1.BackgroundAutoGenerateVacationPlanningPointsV1), -(t) => t.BackgroundAutoGenerateVacationPlanningPointsV1(CancellationToken.None), -Cron.Weekly(DayOfWeek.Friday, 8), -recurringJobOptions); + nameof(PointServiceV1.BackgroundAutoGenerateVacationPlanningPointsV1), + (t) => t.BackgroundAutoGenerateVacationPlanningPointsV1(CancellationToken.None), + Cron.Weekly(DayOfWeek.Friday, 8), + recurringJobOptions); recurringJobManager.AddOrUpdate( nameof(TimeEntryServiceV1.BackgroundSendTimeEntryReminderEmail), diff --git a/src/dotnet/HQ.Server/Services/EmailMessageService.cs b/src/dotnet/HQ.Server/Services/EmailMessageService.cs index ff2ebb85..5c59db5b 100644 --- a/src/dotnet/HQ.Server/Services/EmailMessageService.cs +++ b/src/dotnet/HQ.Server/Services/EmailMessageService.cs @@ -9,6 +9,7 @@ using HQ.Abstractions.Enumerations; using HQ.Abstractions.Services; using HQ.API; +using HQ.Abstractions.Times; using HQ.Server.Data; using HQ.Server.Services; @@ -353,5 +354,52 @@ public async Task SendUpdatedPlanningPointsEmail(Guid staffId, Guid modifiedBySt await SendEmail(EmailMessage.UpdatedPlanningPoints, model, staff.Email, "[HQ] Planning Points Updated", MailPriority.High, null, ct); } } + public async Task SendEmployeeHoursEmail(CancellationToken ct) + { + var date = DateOnly.FromDateTime(DateTime.UtcNow); + + var staff = await _context.Staff + .AsNoTracking() + .Where(t => t.EndDate == null) + .Select(t => new EmployeeHoursEmail.StaffHoursModel() + { + HoursLastWeek = t.Times.Where(x => x.Date >= date.GetPeriodStartDate(Period.LastWeek) && x.Date <= date.GetPeriodEndDate(Period.LastWeek)).Sum(x => x.Hours), + HoursLastMonth = t.Times.Where(x => x.Date >= date.GetPeriodStartDate(Period.LastMonth) && x.Date <= date.GetPeriodEndDate(Period.LastMonth)).Sum(x => x.Hours), + HoursThisMonth = t.Times.Where(x => x.Date >= date.GetPeriodStartDate(Period.Month) && x.Date <= date.GetPeriodEndDate(Period.Month)).Sum(x => x.Hours), + StaffName = t.Name, + WorkHours = t.WorkHours + }) + .OrderBy(t => t.HoursLastWeek) + .ToListAsync(ct); + foreach (var employee in staff) + { + employee.MissingHours = employee.HoursLastWeek == 0; + employee.LessThanExpectedHours = employee.HoursLastWeek < employee.WorkHours; + } + + var managersToNotify = await _context.Projects + .AsNoTracking() + .Where(t => t.EndDate == null && t.ProjectManager!.Email != null) + .Select(t => t.ProjectManager!.Email) + .Distinct() + .ToListAsync(ct); + + var model = new EmployeeHoursEmail() + { + Date = date, + PeriodBegin = date.GetPeriodStartDate(Period.LastWeek), + PeriodEnd = date.GetPeriodEndDate(Period.LastWeek), + ButtonLabel = "Open HQ", + ButtonUrl = _options.CurrentValue.WebUrl, + Staff = staff + }; + foreach (var manager in managersToNotify) + { + if (manager != null) + { + await SendEmail(EmailMessage.EmployeeHours, model, manager, "[HQ] Staff Hours", MailPriority.High, null, ct); + } + } + } } } \ No newline at end of file