Skip to content

Commit

Permalink
PlannedAbsenceRepo (#531)
Browse files Browse the repository at this point in the history
Co-authored-by: Ida Marie Andreassen <[email protected]>
  • Loading branch information
jonasbjoralt and idamand authored Oct 11, 2024
1 parent 37b315c commit a09fcd9
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 106 deletions.
88 changes: 1 addition & 87 deletions backend/Api/Common/StorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,14 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)
.OrderBy(consultant => consultant.Name)
.ToList();


var plannedAbsencePrConsultant = _dbContext.PlannedAbsence
.Include(absence => absence.Absence)
.Include(absence => absence.Consultant)
.GroupBy(absence => absence.Consultant.Id)
.ToDictionary(grouping => grouping.Key, grouping => grouping.ToList());

var vacationsPrConsultant = _dbContext.Vacation
.Include(vacation => vacation.Consultant)
.GroupBy(vacation => vacation.Consultant.Id)
.ToDictionary(grouping => grouping.Key, grouping => grouping.ToList());

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

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

Expand All @@ -129,81 +117,7 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)
return hydratedConsultants;
}

private PlannedAbsence CreateAbsence(PlannedAbsenceKey plannedAbsenceKey, double hours)
{
var consultant = _dbContext.Consultant.Single(c => c.Id == plannedAbsenceKey.ConsultantId);
var absence = _dbContext.Absence.Single(a => a.Id == plannedAbsenceKey.AbsenceId);

var plannedAbsence = new PlannedAbsence
{
AbsenceId = plannedAbsenceKey.AbsenceId,
Absence = absence,
ConsultantId = plannedAbsenceKey.ConsultantId,
Consultant = consultant,
Hours = hours,
Week = plannedAbsenceKey.Week
};
return plannedAbsence;
}


public void UpdateOrCreatePlannedAbsence(PlannedAbsenceKey plannedAbsenceKey, double hours, string orgUrlKey)
{
var plannedAbsence = _dbContext.PlannedAbsence
.FirstOrDefault(pa => pa.AbsenceId.Equals(plannedAbsenceKey.AbsenceId)
&& pa.ConsultantId.Equals(plannedAbsenceKey.ConsultantId)
&& pa.Week.Equals(plannedAbsenceKey.Week));

if (plannedAbsence is null)
_dbContext.Add(CreateAbsence(plannedAbsenceKey, hours));
else
plannedAbsence.Hours = hours;

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


public void UpdateOrCreatePlannedAbsences(int consultantId, int absenceId, List<Week> weeks, double hours,
string orgUrlKey)
{
var consultant = _dbContext.Consultant.Single(c => c.Id == consultantId);
var absence = _dbContext.Absence.Single(a => a.Id == absenceId);

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);
newHours = holidayHours + hours > org.HoursPerWorkday * 5
? Math.Max(org.HoursPerWorkday * 5 - holidayHours, 0)
: hours;
}

var plannedAbsence = _dbContext.PlannedAbsence
.FirstOrDefault(pa => pa.AbsenceId.Equals(absenceId)
&& pa.ConsultantId.Equals(consultantId)
&& pa.Week.Equals(week));

if (plannedAbsence is null)
_dbContext.Add(new PlannedAbsence
{
AbsenceId = absenceId,
Absence = absence,
ConsultantId = consultantId,
Consultant = consultant,
Hours = newHours,
Week = week
});
else
plannedAbsence.Hours = newHours;
}

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

public Consultant? CreateConsultant(Organization org, ConsultantWriteModel body)
{
Expand Down
113 changes: 98 additions & 15 deletions backend/Api/StaffingController/StaffingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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 @@ -14,11 +13,15 @@ namespace Api.StaffingController;
[Authorize]
[Route("/v0/{orgUrlKey}/staffings")]
[ApiController]
public class StaffingController(ApplicationContext context, IMemoryCache cache, IStaffingRepository staffingRepository)
public class StaffingController(
ApplicationContext context,
IMemoryCache cache,
IStaffingRepository staffingRepository,
IPlannedAbsenceRepository plannedAbsenceRepository)
: ControllerBase
{
[HttpGet]
public async Task<Ok<List<StaffingReadModel>>> Get(
public async Task<ActionResult> Get(
[FromRoute] string orgUrlKey,
CancellationToken ct,
[FromQuery(Name = "Year")] int? selectedYearParam = null,
Expand All @@ -39,7 +42,7 @@ public async Task<Ok<List<StaffingReadModel>>> Get(
var readModels = new ReadModelFactory(service)
.GetConsultantReadModelsForWeeks(consultants, weekSet);

return TypedResults.Ok(readModels);
return Ok(readModels);
}

[HttpGet]
Expand Down Expand Up @@ -116,9 +119,14 @@ CancellationToken ct
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.PlannedAbsence:
service.UpdateOrCreatePlannedAbsence(
new PlannedAbsenceKey(staffingWriteModel.EngagementId, staffingWriteModel.ConsultantId,
selectedWeek), staffingWriteModel.Hours, orgUrlKey);
var updatedAbsence = CreateAbsence(new PlannedAbsenceKey(staffingWriteModel.EngagementId,
staffingWriteModel.ConsultantId,
selectedWeek), staffingWriteModel.Hours);

await plannedAbsenceRepository.UpsertPlannedAbsence(updatedAbsence, ct);

//TODO: Remove this once repositories for planned absence and vacations are done too
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.Vacation:
break;
Expand Down Expand Up @@ -162,7 +170,7 @@ CancellationToken ct
{
case BookingType.Booking:
case BookingType.Offer:
var updatedStaffings = UpsertMultipleStaffings(severalStaffingWriteModel.ConsultantId,
var updatedStaffings = GenerateUpdatedStaffings(severalStaffingWriteModel.ConsultantId,
severalStaffingWriteModel.EngagementId, weekSet, severalStaffingWriteModel.Hours, orgUrlKey);

await staffingRepository.UpsertMultipleStaffings(updatedStaffings, ct);
Expand All @@ -171,8 +179,13 @@ CancellationToken ct
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.PlannedAbsence:
service.UpdateOrCreatePlannedAbsences(severalStaffingWriteModel.ConsultantId,
var updatedAbsences = GenerateUpdatedAbsences(severalStaffingWriteModel.ConsultantId,
severalStaffingWriteModel.EngagementId, weekSet, severalStaffingWriteModel.Hours, orgUrlKey);

await plannedAbsenceRepository.UpsertMultiplePlannedAbsences(updatedAbsences, ct);

//TODO: Remove this once repositories for planned absence and vacations are done too
service.ClearConsultantCache(orgUrlKey);
break;
case BookingType.Vacation:
break;
Expand All @@ -195,21 +208,33 @@ CancellationToken ct
private async Task<List<Consultant>> AddRelationalDataToConsultant(List<Consultant> consultants,
CancellationToken ct)
{
var consultantIds = consultants.Select(c => c.Id).Distinct().ToList();

var consultantStaffings =
await staffingRepository.GetStaffingForConsultants(consultants.Select(c => c.Id).ToList(), ct);
await staffingRepository.GetStaffingForConsultants(consultantIds, ct);
var consultantAbsences = await plannedAbsenceRepository.GetPlannedAbsenceForConsultants(consultantIds, 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;
c.Staffings = GetFromDictOrDefault(c.Id, consultantStaffings);
c.PlannedAbsences = GetFromDictOrDefault(c.Id, consultantAbsences);

return c;
}).ToList();
}

private static List<T> GetFromDictOrDefault<T>(int key, Dictionary<int, List<T>> dict)
{
var hasValue = dict.TryGetValue(key, out var value);
if (hasValue && value is not null) return value;

return [];
}


//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,
private List<Staffing> GenerateUpdatedStaffings(int consultantId, int engagementId,
List<Week> weeks,
double hours,
string orgUrlKey)
Expand Down Expand Up @@ -267,6 +292,47 @@ private List<Staffing> UpsertMultipleStaffings(int consultantId, int engagementI
return staffingsToUpsert;
}

private List<PlannedAbsence> GenerateUpdatedAbsences(int consultantId, int absenceId, List<Week> weeks,
double hours,
string orgUrlKey)
{
var consultant = context.Consultant.Single(c => c.Id == consultantId);
var absence = context.Absence.Single(a => a.Id == absenceId);

var org = context.Organization.FirstOrDefault(o => o.UrlKey == orgUrlKey);
return weeks.Select(week =>
{
var newHours = hours;
if (org != null)
{
var holidayHours = org.GetTotalHolidayHoursOfWeek(week);
newHours = holidayHours + hours > org.HoursPerWorkday * 5
? Math.Max(org.HoursPerWorkday * 5 - holidayHours, 0)
: hours;
}

var plannedAbsence = context.PlannedAbsence
.FirstOrDefault(pa => pa.AbsenceId.Equals(absenceId)
&& pa.ConsultantId.Equals(consultantId)
&& pa.Week.Equals(week));

if (plannedAbsence is null)
plannedAbsence = new PlannedAbsence
{
AbsenceId = absenceId,
Absence = absence,
ConsultantId = consultantId,
Consultant = consultant,
Hours = newHours,
Week = week
};
else
plannedAbsence.Hours = newHours;

return plannedAbsence;
}).ToList();
}

private Staffing CreateStaffing(StaffingKey staffingKey, double hours)
{
// TODO; Rewrite this to not query relations
Expand All @@ -283,6 +349,23 @@ private Staffing CreateStaffing(StaffingKey staffingKey, double hours)
Week = staffingKey.Week
};
}

private PlannedAbsence CreateAbsence(PlannedAbsenceKey plannedAbsenceKey, double hours)
{
var consultant = context.Consultant.Single(c => c.Id == plannedAbsenceKey.ConsultantId);
var absence = context.Absence.Single(a => a.Id == plannedAbsenceKey.AbsenceId);

var plannedAbsence = new PlannedAbsence
{
AbsenceId = plannedAbsenceKey.AbsenceId,
Absence = absence,
ConsultantId = plannedAbsenceKey.ConsultantId,
Consultant = consultant,
Hours = hours,
Week = plannedAbsenceKey.Week
};
return plannedAbsence;
}
}

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

public interface IPlannedAbsenceRepository
{
public Task<Dictionary<int, List<PlannedAbsence>>> GetPlannedAbsenceForConsultants(List<int> consultantIds,
CancellationToken ct);

public Task<List<PlannedAbsence>> GetPlannedAbsenceForConsultant(int consultantId, CancellationToken ct);

public Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken ct);

public Task UpsertMultiplePlannedAbsences(List<PlannedAbsence> plannedAbsences, CancellationToken ct);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Core.PlannedAbsences;
using Microsoft.Extensions.Caching.Memory;

namespace Infrastructure.Repositories.PlannedAbsences;

public class PlannedAbsenceCacheRepository(IPlannedAbsenceRepository sourceRepository, IMemoryCache cache) : IPlannedAbsenceRepository
{
public async Task<Dictionary<int, List<PlannedAbsence>>> GetPlannedAbsenceForConsultants(List<int> consultantIds, CancellationToken ct)
{
var nonCachedIds = new List<int>();
var result = new Dictionary<int, List<PlannedAbsence>>();

foreach (var consultantId in consultantIds.Distinct())
{
var plannedAbsenceList = GetPlannedAbsencesFromCache(consultantId);
if (plannedAbsenceList is null)
nonCachedIds.Add(consultantId);
else
result.Add(consultantId, plannedAbsenceList);
}

var queriedPlannedAbsenceLists = await sourceRepository.GetPlannedAbsenceForConsultants(nonCachedIds, ct);
foreach (var (cId, plannedAbsences) in queriedPlannedAbsenceLists)
{
result.Add(cId, plannedAbsences);
cache.Set(PlannedAbsenceCacheKey(cId), plannedAbsences);
}

return result;
}

public async Task<List<PlannedAbsence>> GetPlannedAbsenceForConsultant(int consultantId, CancellationToken ct)
{
var plannedAbsenceList = GetPlannedAbsencesFromCache(consultantId);
if (plannedAbsenceList is not null) return plannedAbsenceList;

plannedAbsenceList = await sourceRepository.GetPlannedAbsenceForConsultant(consultantId, ct);
cache.Set(PlannedAbsenceCacheKey(consultantId), plannedAbsenceList);
return plannedAbsenceList;
}

public async Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken ct)
{
await sourceRepository.UpsertPlannedAbsence(plannedAbsence, ct);
ClearPlannedAbsenceCache(plannedAbsence.ConsultantId);
}

public async Task UpsertMultiplePlannedAbsences(List<PlannedAbsence> plannedAbsences, CancellationToken ct)
{
await sourceRepository.UpsertMultiplePlannedAbsences(plannedAbsences, ct);

var consultantIds = plannedAbsences.Select(pa => pa.ConsultantId).Distinct();
foreach (var consultantId in consultantIds) ClearPlannedAbsenceCache(consultantId);

}

private List<PlannedAbsence>? GetPlannedAbsencesFromCache(int consultantId)
{
if (cache.TryGetValue<List<PlannedAbsence>>(PlannedAbsenceCacheKey(consultantId), out var plannedAbsenceList))
if (plannedAbsenceList is not null)
return plannedAbsenceList;

return null;
}

private void ClearPlannedAbsenceCache(int consultantId)
{
cache.Remove(PlannedAbsenceCacheKey(consultantId));
}

private static string PlannedAbsenceCacheKey(int consultantId)
{
return $"PlannedAbsenceCacheRepository/{consultantId}";
}
}
Loading

0 comments on commit a09fcd9

Please sign in to comment.