Skip to content

Commit

Permalink
Add Repository for Staffing (#530)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbjoralt authored Oct 11, 2024
1 parent 9506428 commit 37b315c
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 109 deletions.
95 changes: 1 addition & 94 deletions backend/Api/Common/StorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)
.OrderBy(consultant => consultant.Name)
.ToList();

var staffingPrConsultant = _dbContext.Staffing
.Include(s => s.Consultant)
.Include(staffing => staffing.Engagement)
.ThenInclude(project => project.Customer)
.GroupBy(staffing => staffing.Consultant.Id)
.ToDictionary(group => group.Key, grouping => grouping.ToList());

var plannedAbsencePrConsultant = _dbContext.PlannedAbsence
.Include(absence => absence.Absence)
Expand All @@ -120,16 +114,12 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)

var hydratedConsultants = consultantList.Select(consultant =>
{
consultant.Staffings = staffingPrConsultant.TryGetValue(consultant.Id, out var staffing)
? staffing
: new List<Staffing>();

consultant.PlannedAbsences =
plannedAbsencePrConsultant.TryGetValue(consultant.Id, out var plannedAbsences)
? plannedAbsences
: new List<PlannedAbsence>();

consultant.Vacations = vacationsPrConsultant.TryGetValue(consultant.Id, out var vacations)
consultant.Vacations = vacationsPrConsultant.TryGetValue(consultant.Id, out List<Vacation>? vacations)
? vacations
: new List<Vacation>();

Expand All @@ -139,23 +129,6 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)
return hydratedConsultants;
}

private Staffing CreateStaffing(StaffingKey staffingKey, double hours)
{
var consultant = _dbContext.Consultant.Single(c => c.Id == staffingKey.ConsultantId);
var project = _dbContext.Project.Single(p => p.Id == staffingKey.EngagementId);

var staffing = new Staffing
{
EngagementId = staffingKey.EngagementId,
Engagement = project,
ConsultantId = staffingKey.ConsultantId,
Consultant = consultant,
Hours = hours,
Week = staffingKey.Week
};
return staffing;
}

private PlannedAbsence CreateAbsence(PlannedAbsenceKey plannedAbsenceKey, double hours)
{
var consultant = _dbContext.Consultant.Single(c => c.Id == plannedAbsenceKey.ConsultantId);
Expand All @@ -173,23 +146,6 @@ private PlannedAbsence CreateAbsence(PlannedAbsenceKey plannedAbsenceKey, double
return plannedAbsence;
}

public void UpdateOrCreateStaffing(StaffingKey staffingKey, double hours, string orgUrlKey)
{
var staffing = _dbContext.Staffing
.FirstOrDefault(s => s.EngagementId.Equals(staffingKey.EngagementId)
&& s.ConsultantId.Equals(staffingKey.ConsultantId)
&& s.Week.Equals(staffingKey.Week));


if (staffing is null)
_dbContext.Add(CreateStaffing(staffingKey, hours));
else
staffing.Hours = hours;

_dbContext.SaveChanges();
ClearConsultantCache(orgUrlKey);
}


public void UpdateOrCreatePlannedAbsence(PlannedAbsenceKey plannedAbsenceKey, double hours, string orgUrlKey)
{
Expand All @@ -207,55 +163,6 @@ public void UpdateOrCreatePlannedAbsence(PlannedAbsenceKey plannedAbsenceKey, do
ClearConsultantCache(orgUrlKey);
}

public void UpdateOrCreateStaffings(int consultantId, int projectId, List<Week> weeks, double hours,
string orgUrlKey)
{
var consultant = _dbContext.Consultant.Single(c => c.Id == consultantId);
var project = _dbContext.Project.Single(p => p.Id == projectId);

var org = _dbContext.Organization.FirstOrDefault(o => o.UrlKey == orgUrlKey);

foreach (var week in weeks)
{
var newHours = hours;
if (org != null)
{
var holidayHours = org.GetTotalHolidayHoursOfWeek(week);
var vacations = _dbContext.Vacation.Where(v => v.ConsultantId.Equals(consultantId)).ToList();
var vacationHours = vacations.Count(v => week.ContainsDate(v.Date)) * org.HoursPerWorkday;
var plannedAbsenceHours = _dbContext.PlannedAbsence
.Where(pa => pa.Week.Equals(week) && pa.ConsultantId.Equals(consultantId))
.Select(pa => pa.Hours).Sum();

var total = holidayHours + vacationHours + plannedAbsenceHours;

newHours = hours + total > org.HoursPerWorkday * 5
? Math.Max(org.HoursPerWorkday * 5 - total, 0)
: hours;
}

var staffing = _dbContext.Staffing
.FirstOrDefault(s => s.EngagementId.Equals(projectId)
&& s.ConsultantId.Equals(consultantId)
&& s.Week.Equals(week));
if (staffing is null)
_dbContext.Add(new Staffing
{
EngagementId = projectId,
Engagement = project,
ConsultantId = consultantId,
Consultant = consultant,
Hours = newHours,
Week = week
});
else
staffing.Hours = newHours;
}

_dbContext.SaveChanges();
ClearConsultantCache(orgUrlKey);
}


public void UpdateOrCreatePlannedAbsences(int consultantId, int absenceId, List<Week> weeks, double hours,
string orgUrlKey)
Expand Down
4 changes: 2 additions & 2 deletions backend/Api/StaffingController/ReadModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ public ReadModelFactory(StorageService storageService)
_storageService = storageService;
}

public List<StaffingReadModel> GetConsultantReadModelsForWeeks(string orgUrlKey, List<Week> weeks)
public List<StaffingReadModel> GetConsultantReadModelsForWeeks(List<Consultant> consultants, List<Week> weeks)
{
var firstDayInScope = weeks.First().FirstDayOfWorkWeek();
var firstWorkDayOutOfScope = weeks.Last().LastWorkDayOfWeek();

return _storageService.LoadConsultants(orgUrlKey)
return consultants
.Where(c => c.EndDate == null || c.EndDate > firstDayInScope)
.Where(c => c.StartDate == null || c.StartDate < firstWorkDayOutOfScope)
.Select(consultant => MapToReadModelList(consultant, weeks))
Expand Down
139 changes: 126 additions & 13 deletions backend/Api/StaffingController/StaffingController.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Api.Common;
using Core.Consultants;
using Core.DomainModels;
using Core.PlannedAbsences;
using Core.Staffings;
using Infrastructure.DatabaseContext;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;

Expand All @@ -12,11 +14,13 @@ namespace Api.StaffingController;
[Authorize]
[Route("/v0/{orgUrlKey}/staffings")]
[ApiController]
public class StaffingController(ApplicationContext context, IMemoryCache cache) : ControllerBase
public class StaffingController(ApplicationContext context, IMemoryCache cache, IStaffingRepository staffingRepository)
: ControllerBase
{
[HttpGet]
public ActionResult<List<StaffingReadModel>> Get(
public async Task<Ok<List<StaffingReadModel>>> Get(
[FromRoute] string orgUrlKey,
CancellationToken ct,
[FromQuery(Name = "Year")] int? selectedYearParam = null,
[FromQuery(Name = "Week")] int? selectedWeekParam = null,
[FromQuery(Name = "WeekSpan")] int numberOfWeeks = 8,
Expand All @@ -29,8 +33,13 @@ public ActionResult<List<StaffingReadModel>> Get(
var weekSet = selectedWeek.GetNextWeeks(numberOfWeeks);

var service = new StorageService(cache, context);
var readModels = new ReadModelFactory(service).GetConsultantReadModelsForWeeks(orgUrlKey, weekSet);
return Ok(readModels);
var consultants = service.LoadConsultants(orgUrlKey);
consultants = await AddRelationalDataToConsultant(consultants, ct);

var readModels = new ReadModelFactory(service)
.GetConsultantReadModelsForWeeks(consultants, weekSet);

return TypedResults.Ok(readModels);
}

[HttpGet]
Expand Down Expand Up @@ -80,9 +89,10 @@ public ActionResult<List<StaffingReadModel>> GetConsultantsInProject(

[HttpPut]
[Route("update")]
public ActionResult<StaffingReadModel> Put(
public async Task<ActionResult<StaffingReadModel>> Put(
[FromRoute] string orgUrlKey,
[FromBody] StaffingWriteModel staffingWriteModel
[FromBody] StaffingWriteModel staffingWriteModel,
CancellationToken ct
)
{
var service = new StorageService(cache, context);
Expand All @@ -96,10 +106,14 @@ [FromBody] StaffingWriteModel staffingWriteModel
{
case BookingType.Booking:
case BookingType.Offer:
service.UpdateOrCreateStaffing(
new StaffingKey(staffingWriteModel.EngagementId, staffingWriteModel.ConsultantId, selectedWeek),
staffingWriteModel.Hours, orgUrlKey
);
var updatedStaffing = CreateStaffing(
new StaffingKey(staffingWriteModel.EngagementId,
staffingWriteModel.ConsultantId, selectedWeek), staffingWriteModel.Hours);

await staffingRepository.UpsertStaffing(updatedStaffing, ct);

//TODO: Remove this once repositories for planned absence and vacations are done too
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.PlannedAbsence:
service.UpdateOrCreatePlannedAbsence(
Expand All @@ -125,9 +139,10 @@ [FromBody] StaffingWriteModel staffingWriteModel

[HttpPut]
[Route("update/several")]
public ActionResult<StaffingReadModel> Put(
public async Task<ActionResult<StaffingReadModel>> Put(
[FromRoute] string orgUrlKey,
[FromBody] SeveralStaffingWriteModel severalStaffingWriteModel
[FromBody] SeveralStaffingWriteModel severalStaffingWriteModel,
CancellationToken ct
)
{
var service = new StorageService(cache, context);
Expand All @@ -147,8 +162,13 @@ [FromBody] SeveralStaffingWriteModel severalStaffingWriteModel
{
case BookingType.Booking:
case BookingType.Offer:
service.UpdateOrCreateStaffings(severalStaffingWriteModel.ConsultantId,
var updatedStaffings = UpsertMultipleStaffings(severalStaffingWriteModel.ConsultantId,
severalStaffingWriteModel.EngagementId, weekSet, severalStaffingWriteModel.Hours, orgUrlKey);

await staffingRepository.UpsertMultipleStaffings(updatedStaffings, ct);

//TODO: Remove this once repositories for planned absence and vacations are done too
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.PlannedAbsence:
service.UpdateOrCreatePlannedAbsences(severalStaffingWriteModel.ConsultantId,
Expand All @@ -170,6 +190,99 @@ [FromBody] SeveralStaffingWriteModel severalStaffingWriteModel
return new ReadModelFactory(service).GetConsultantReadModelForWeeks(
severalStaffingWriteModel.ConsultantId, weekSet);
}

// TODO: Move this to a future application layer. This is to consolidate data from various repositories such as Staffing or PlannedAbsence
private async Task<List<Consultant>> AddRelationalDataToConsultant(List<Consultant> consultants,
CancellationToken ct)
{
var consultantStaffings =
await staffingRepository.GetStaffingForConsultants(consultants.Select(c => c.Id).ToList(), ct);
return consultants.Select(c =>
{
var hasStaffing = consultantStaffings.TryGetValue(c.Id, out var staffings);
if (!hasStaffing || staffings is null)
staffings = new List<Staffing>();
c.Staffings = staffings;
return c;
}).ToList();
}

//TODO: Divide this more neatly into various functions for readability.
// This is skipped for now to avoid massive scope-creep. Comments are added for a temporary readability-buff
private List<Staffing> UpsertMultipleStaffings(int consultantId, int engagementId,
List<Week> weeks,
double hours,
string orgUrlKey)
{
// Get base data we need.
var consultant = context.Consultant.Single(c => c.Id == consultantId);
var project = context.Project.Single(p => p.Id == engagementId);

var org = context.Organization.FirstOrDefault(o => o.UrlKey == orgUrlKey);

// Create one new staffing for each week
var staffingsToUpsert = weeks.Select(week =>
{
// This is a variable as we may change it to adapt to maximum possible booking
var newHours = hours;
if (org != null)
{
// Calculates the max hours we can add without overbooking due to vacations, absences, etc
var holidayHours = org.GetTotalHolidayHoursOfWeek(week);
var vacations = context.Vacation.Where(v => v.ConsultantId.Equals(consultantId)).ToList();
var vacationHours = vacations.Count(v => week.ContainsDate(v.Date)) * org.HoursPerWorkday;
var plannedAbsenceHours = context.PlannedAbsence
.Where(pa => pa.Week.Equals(week) && pa.ConsultantId.Equals(consultantId))
.Select(pa => pa.Hours).Sum();

var total = holidayHours + vacationHours + plannedAbsenceHours;

newHours = hours + total > org.HoursPerWorkday * 5
? Math.Max(org.HoursPerWorkday * 5 - total, 0)
: hours;
}


var staffing = context.Staffing
.FirstOrDefault(s => s.EngagementId.Equals(engagementId)
&& s.ConsultantId.Equals(consultantId)
&& s.Week.Equals(week));

if (staffing is null)
return new Staffing
{
EngagementId = engagementId,
Engagement = project,
ConsultantId = consultantId,
Consultant = consultant,
Hours = newHours,
Week = week
};

// Set it again in case it was found in query above
staffing.Hours = newHours;
return staffing;
}).ToList();

return staffingsToUpsert;
}

private Staffing CreateStaffing(StaffingKey staffingKey, double hours)
{
// TODO; Rewrite this to not query relations
var consultant = context.Consultant.Single(c => c.Id == staffingKey.ConsultantId);
var project = context.Project.Single(p => p.Id == staffingKey.EngagementId);

return new Staffing
{
EngagementId = staffingKey.EngagementId,
Engagement = project,
ConsultantId = staffingKey.ConsultantId,
Consultant = consultant,
Hours = hours,
Week = staffingKey.Week
};
}
}

public record StaffingWriteModel(
Expand Down
13 changes: 13 additions & 0 deletions backend/Core/Staffings/IStaffingRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Core.Staffings;

public interface IStaffingRepository
{
public Task<Dictionary<int, List<Staffing>>> GetStaffingForConsultants(List<int> consultantIds,
CancellationToken ct);

public Task<List<Staffing>> GetStaffingForConsultant(int consultantId, CancellationToken ct);

public Task UpsertStaffing(Staffing staffing, CancellationToken ct);

public Task UpsertMultipleStaffings(List<Staffing> staffings, CancellationToken ct);
}
5 changes: 5 additions & 0 deletions backend/Infrastructure/Repositories/RepositoryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Core.Consultants;
using Core.Engagements;
using Core.Organizations;
using Core.Staffings;
using Infrastructure.Repositories.Consultants;
using Infrastructure.Repositories.Engagement;
using Infrastructure.Repositories.Organization;
using Infrastructure.Repositories.Staffings;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -19,6 +21,9 @@ public static void AddRepositories(this WebApplicationBuilder builder)
builder.Services.AddScoped<IEngagementRepository, EngagementDbRepository>();
builder.Services.AddScoped<IDepartmentRepository, DepartmentDbRepository>();

builder.Services.AddScoped<IStaffingRepository, StaffingDbRepository>();
builder.Services.Decorate<IStaffingRepository, StaffingCacheRepository>();

builder.Services.AddScoped<IConsultantRepository, ConsultantDbRepository>();
}
}
Loading

0 comments on commit 37b315c

Please sign in to comment.