Skip to content

Commit

Permalink
its cleaner and works for real
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbjoralt committed Nov 6, 2023
1 parent bc7b644 commit 849312a
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 126 deletions.
3 changes: 2 additions & 1 deletion backend/Api/Cache/CacheKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ namespace Api.Cache;
public enum CacheKeys
{
ConsultantAvailability8Weeks,
OrganisationsPrConsultant
OrganisationsPrConsultant,
ConsultantReadModels
}
230 changes: 128 additions & 102 deletions backend/Api/Consultants/ConsultantController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using Api.Cache;
using Core.DomainModels;
using Core.Services;
Expand Down Expand Up @@ -32,47 +31,77 @@ public ActionResult<List<ConsultantReadModel>> Get(
[FromQuery(Name = "weeks")] int numberOfWeeks = 8,
[FromQuery(Name = "includeOccupied")] bool includeOccupied = true)
{
var watch = Stopwatch.StartNew();

var selectedYear = selectedYearParam ?? DateTime.Now.Year;
var selectedWeekNumber = selectedWeekParam ?? DateService.GetWeekNumber(DateTime.Now);
var selectedWeek = new Week(selectedYear, selectedWeekNumber);
var consultants = GetConsultantsWithAvailability(orgUrlKey, selectedWeek, numberOfWeeks)
var weekSet = DateService.GetNextWeeks(selectedWeek, numberOfWeeks);

var consultants = GetConsultantReadModels(orgUrlKey, weekSet)
.Where(c =>
includeOccupied
|| c.IsOccupied
)
.ToList();

watch.Stop();
return Ok(consultants);
}

private List<ConsultantReadModel> GetConsultantReadModels(string orgUrlKey, List<Week> weeks)
{
var initialWeekKey = weeks.First().ToSortableInt();
var cacheKey = $"{orgUrlKey}/{weeks.Count}/{initialWeekKey}/{CacheKeys.ConsultantReadModels}";
var cacheHadReadModel = _cache.TryGetValue(cacheKey,
out List<ConsultantReadModel>? consultantReadModels);

if (cacheHadReadModel && consultantReadModels is not null) return consultantReadModels;

return Ok(new { Time = watch.ElapsedMilliseconds, Data = consultants });
var loadedReadModels = LoadReadModelFromDb(orgUrlKey, weeks);
_cache.Set(cacheKey, loadedReadModels);
return loadedReadModels;
}

[HttpGet("perf")]
public ActionResult<List<ConsultantReadModel>> GetFast()

private List<IGrouping<StaffingGroupKey, Staffing>> LoadStaffingByProjectTypeForWeeks(List<Week> weeks,
ProjectState state)
{
var watch = Stopwatch.StartNew();
var year = weeks[0].Year;
var minWeek = weeks.Select(w => w.WeekNumber).Min();
var maxWeek = weeks.Select(w => w.WeekNumber).Max();

var weekSet = DateService.GetNextWeeks(new Week(2023, 44), 8);

var minDate = new DateOnly(2023, 11, 1);
var maxDate = new DateOnly(2023, 12, 1);
return _context.Staffing
.Where(staffing => staffing.Year == year && minWeek <= staffing.Week && staffing.Week <= maxWeek)
.Where(staffing =>
staffing.Project.State == state)
.Include(s => s.Consultant)
.Include(staffing => staffing.Project)
.GroupBy(s => new StaffingGroupKey(s.Consultant.Id, s.Project.Id, s.Year, s.Week))
.ToList();
}

private List<ConsultantReadModel> LoadReadModelFromDb(string orgUrlKey, List<Week> weekSet)
{
var year = weekSet[0].Year;
var minWeek = weekSet.Select(w => w.WeekNumber).Min();
var maxWeek = weekSet.Select(w => w.WeekNumber).Min();

var firstDayInScope = DateService.FirstDayOfWorkWeek(weekSet.First());
var firstWorkDayOutOfScope = DateService.LastWorkDayOfWeek(weekSet.Last()).AddDays(1);

var consultants = _context.Consultant.Include(consultant => consultant.Department)
.ThenInclude(department => department.Organization).ToList();
.ThenInclude(department => department.Organization)
.Where(c => c.EndDate == null || c.EndDate > firstDayInScope)
.Where(c => c.StartDate == null || c.StartDate <= firstWorkDayOutOfScope)
.Where(c => c.Department.Organization.UrlKey == orgUrlKey)
.OrderBy(c => c.Name)
.ToList();

var projects = _context.Project.Include(p => p.Customer)
.ToDictionary(project => project.Id, project => project);
var absences = _context.Absence.ToDictionary(absence => absence.Id, absence => absence);

var billableStaffings = LoadStaffingByProjectTypeForWeeks(weekSet, ProjectState.Active);
var offeredStaffings = LoadStaffingByProjectTypeForWeeks(weekSet, ProjectState.Offer);
var billableStaffing = LoadStaffingByProjectTypeForWeeks(weekSet, ProjectState.Active);
var offeredStaffing = LoadStaffingByProjectTypeForWeeks(weekSet, ProjectState.Offer);

var plannedAbsences = _context.PlannedAbsence
.Include(plannedAbsence => plannedAbsence.Consultant)
Expand All @@ -85,116 +114,113 @@ public ActionResult<List<ConsultantReadModel>> GetFast()


var vacations = _context.Vacation
.Where(vacation => minDate <= vacation.Date && vacation.Date <= maxDate)
.Where(vacation => firstDayInScope <= vacation.Date && vacation.Date <= firstWorkDayOutOfScope)
.Include(vacation => vacation.Consultant)
.GroupBy(vacation => vacation.Consultant.Id)
.ToList();

var a = consultants.Select(c =>
var consultantReadModels = consultants.Select(c =>
{
var billableSet = billableStaffings.Where(g =>
g.Key.ConsultantId == c.Id);
var offeredSet = offeredStaffings
.Where(g => g.Key.ConsultantId == c.Id);
var billableSet = billableStaffing.Where(g => g.Key.ConsultantId == c.Id);
var offeredSet = offeredStaffing.Where(g => g.Key.ConsultantId == c.Id);
var absenceSet = plannedAbsences.Where(g => g.Key.ConsultantId == c.Id);

var consultantVacations = vacations.Where(g => g.Key == c.Id).Aggregate(new List<Vacation>(),
(list, grouping) => list.Concat(grouping.Select(v => v)).ToList());

return ConsultantReadModel.FromController(c, projects, absences, billableSet, offeredSet, absenceSet,
consultantVacations, weekSet);
if (c.Id == 33)
Console.Out.Write("Her");

var detailedBookings =
DetailedBookings(c, projects, absences, billableSet, offeredSet, absenceSet, consultantVacations,
weekSet);

return c.MapToReadModelList(detailedBookings, weekSet);
}).ToList();
watch.Stop();

return Ok(new { Time = watch.ElapsedMilliseconds, Data = a });
return consultantReadModels;
}


private List<ConsultantReadModel> GetConsultantsWithAvailability(string orgUrlKey, Week initialWeekNumber,
int numberOfWeeks)
private static List<DetailedBooking> DetailedBookings(Consultant consultant,
Dictionary<int, Project> projects, Dictionary<int, Absence> absences,
IEnumerable<IGrouping<StaffingGroupKey, Staffing>> billableStaffings,
IEnumerable<IGrouping<StaffingGroupKey, Staffing>> offeredStaffings,
IEnumerable<IGrouping<StaffingGroupKey, PlannedAbsence>> plannedAbsences,
List<Vacation> vacations,
List<Week> weekSet)
{
if (numberOfWeeks == 8 && false)
weekSet.Sort();

var billableArray = billableStaffings as IGrouping<StaffingGroupKey, Staffing>[] ?? billableStaffings.ToArray();
var billableProjects = billableArray.Select(a => a.Key.WorkTypeId).Distinct().Select(id => projects[id]);
var offeredArray = offeredStaffings as IGrouping<StaffingGroupKey, Staffing>[] ?? offeredStaffings.ToArray();
var offeredProjects = offeredArray.Select(a => a.Key.WorkTypeId).Distinct().Select(id => projects[id])
.ToList();

var absenceArray = plannedAbsences as IGrouping<StaffingGroupKey, PlannedAbsence>[] ??
plannedAbsences.ToArray();
var plannedAbsenceTypes =
absenceArray.Select(a => a.Key.WorkTypeId).Distinct().Select(id => absences[id]).ToList();


// TODO: These can probably be made smaller
var billableBookings = billableProjects.Select(project =>
{
_cache.TryGetValue(
$"{orgUrlKey}/{initialWeekNumber}/{CacheKeys.ConsultantAvailability8Weeks}",
out List<ConsultantReadModel>? cachedConsultants);
if (cachedConsultants != null) return cachedConsultants;
}
var bookings = weekSet.Select(week => new WeeklyHours(week.ToSortableInt(), billableArray
.Where(g => g.Key.ConsultantId == consultant.Id && g.Key.WorkTypeId == project.Id &&
g.Key.Year == week.Year &&
g.Key.Week == week.WeekNumber)
.Select(g => g.Select(staffing => staffing.Hours).Sum())
.Sum()
)).ToList();

return new DetailedBooking(new BookingDetails(project.Customer.Name, BookingType.Booking), bookings);
}).ToList();

var consultants = LoadConsultantAvailability(orgUrlKey, initialWeekNumber, numberOfWeeks)
.Select(c => c.MapConsultantToReadModel(initialWeekNumber, numberOfWeeks)).ToList();
var offeredBookings = offeredProjects.Select(project =>
{
var bookings = weekSet.Select(week => new WeeklyHours(week.ToSortableInt(), offeredArray
.Where(g => g.Key.ConsultantId == consultant.Id && g.Key.WorkTypeId == project.Id &&
g.Key.Year == week.Year &&
g.Key.Week == week.WeekNumber)
.Select(g => g.Select(staffing => staffing.Hours).Sum())
.Sum()
)).ToList();

return new DetailedBooking(new BookingDetails(project.Customer.Name, BookingType.Offer), bookings);
}).ToList();

_cache.Set($"{orgUrlKey}/{initialWeekNumber}/{CacheKeys.ConsultantAvailability8Weeks}", consultants);
return consultants;
}
var plannedAbsencesPrWeek = plannedAbsenceTypes.Select(absenceType =>
{
var bookings = weekSet.Select(week => new WeeklyHours(week.ToSortableInt(), absenceArray
.Where(g => g.Key.ConsultantId == consultant.Id && g.Key.WorkTypeId == absenceType.Id &&
g.Key.Year == week.Year &&
g.Key.Week == week.WeekNumber)
.Select(g => g.Select(staffing => staffing.Hours).Sum())
.Sum()
)).ToList();

return new DetailedBooking(new BookingDetails(absenceType.Name, BookingType.PlannedAbsence), bookings);
}).ToList();

private List<IGrouping<StaffingGroupKey, Staffing>> LoadStaffingByProjectTypeForWeeks(List<Week> weeks,
ProjectState state)
{
var year = weeks[0].Year;
var minWeek = weeks.Select(w => w.WeekNumber).Min();
var maxWeek = weeks.Select(w => w.WeekNumber).Min();

var detailedBookings = billableBookings.Concat(offeredBookings).Concat(plannedAbsencesPrWeek);

return _context.Staffing
.Where(staffing => staffing.Year == year && minWeek <= staffing.Week && staffing.Week <= maxWeek)
.Where(staffing =>
staffing.Project.State == state)
.Include(s => s.Consultant)
.Include(staffing => staffing.Project)
.GroupBy(s => new StaffingGroupKey(s.Consultant.Id, s.Project.Id, s.Year, s.Week))
.ToList();
}

if (vacations.Count > 0)
{
var vacationsPrWeek = weekSet.Select(week => new WeeklyHours(
week.ToSortableInt(),
vacations.Count(vacation => DateService.DateIsInWeek(vacation.Date, week)) *
consultant.Department.Organization.HoursPerWorkday
)).ToList();
detailedBookings = detailedBookings.Append(new DetailedBooking(
new BookingDetails("Ferie", BookingType.Vacation),
vacationsPrWeek));
}

private List<Consultant> LoadConsultantAvailability(string orgUrlKey, Week selectedWeek, int numberOfWeeks)
{
var applicableWeeks = DateService.GetNextWeeks(selectedWeek, numberOfWeeks);
var firstDayOfCurrentWeek = DateService.GetFirstDayOfWeekContainingDate(DateTime.Now);
var firstWorkDayOutOfScope =
DateService.GetFirstDayOfWeekContainingDate(DateTime.Now.AddDays(numberOfWeeks * 7));

// Needed to filter planned absence and staffing.
// From november, we will span two years.
// Given a 5-week span, the set of weeks can look like this: (2022) 51, 52, 53, 1, 2 (2023)
// Then we can filter as follows: Either the staffing has year 2022 and a week between 51 and 53, or year 2023 with weeks 1 and 2.
var minWeekNum = applicableWeeks.Select(w => w.WeekNumber).Min();

// Set A will be either the weeks in the next year (2023 in the above example), or have all the weeks in a mid-year case
var yearA = applicableWeeks.Select(w => w.Year).Max();
var weeksInA = applicableWeeks.Select(w => w.WeekNumber).Where(w => w < minWeekNum + numberOfWeeks).ToList();
var minWeekA = weeksInA.Min();
var maxWeekA = weeksInA.Max();

// Set B will be either the weeks in the current year (2022 in the above example), or and empty set in a mid-year case.
var yearB = applicableWeeks.Select(w => w.Year).Min();
var weeksInB = applicableWeeks.Select(w => w.WeekNumber).Where(w => w < minWeekNum + numberOfWeeks).ToList();
var minWeekB = weeksInB.Min();
var maxWeekB = weeksInB.Max();


return _context.Consultant
.Where(c => c.EndDate == null || c.EndDate > firstDayOfCurrentWeek)
.Where(c => c.StartDate == null || c.StartDate <= firstWorkDayOutOfScope)
.Include(c => c.Vacations)
.Include(c => c.Competences)
.Include(c => c.PlannedAbsences.Where(pa =>
(pa.Year <= yearA && minWeekA <= pa.WeekNumber && pa.WeekNumber <= maxWeekA)
|| (yearB <= pa.Year && minWeekB <= pa.WeekNumber && pa.WeekNumber <= maxWeekB)))
.ThenInclude(pa => pa.Absence)
.Include(c => c.Department)
.ThenInclude(d => d.Organization)
.Where(c => c.Department.Organization.UrlKey == orgUrlKey)
.Include(c => c.Staffings.Where(s =>
(s.Year <= yearA && minWeekA <= s.Week && s.Week <= maxWeekA)
|| (yearB <= s.Year && minWeekB <= s.Week && s.Week <= maxWeekB)))
.ThenInclude(s => s.Project)
.ThenInclude(p => p.Customer)
.OrderBy(c => c.Name)
.ToList();
return detailedBookings.ToList();
}
}

public record StaffingGroupKey(int ConsultantId, int WorkTypeId, int Year, int Week);

public record WeeklyBooking(int YearWeek, double TotalBillable, double TotalOffered);
public record StaffingGroupKey(int ConsultantId, int WorkTypeId, int Year, int Week);
Loading

0 comments on commit 849312a

Please sign in to comment.