Skip to content

Commit

Permalink
236 select week (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasbjoralt authored Nov 2, 2023
1 parent aaa75dd commit 61fb6f2
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 137 deletions.
104 changes: 23 additions & 81 deletions backend/Api/Consultants/ConsultantController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Core.Services;
using Database.DatabaseContext;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
Expand All @@ -27,10 +26,15 @@ public ConsultantController(ApplicationContext context, IMemoryCache cache)
[HttpGet]
public ActionResult<List<ConsultantReadModel>> Get(
[FromRoute] string orgUrlKey,
[FromQuery(Name = "Year")] int? selectedYearParam = null,
[FromQuery(Name = "Week")] int? selectedWeekParam = null,
[FromQuery(Name = "weeks")] int numberOfWeeks = 8,
[FromQuery(Name = "includeOccupied")] bool includeOccupied = true)
{
var consultants = GetConsultantsWithAvailability(orgUrlKey, numberOfWeeks)
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)
.Where(c =>
includeOccupied
|| c.IsOccupied
Expand All @@ -41,53 +45,27 @@ public ActionResult<List<ConsultantReadModel>> Get(
}


[HttpPost]
public async Task<Results<Created<ConsultantWriteModel>, ProblemHttpResult, ValidationProblem>> AddBasicConsultant(
[FromBody] ConsultantWriteModel basicConsultant)
{
try
{
var selectedDepartment = await GetDepartmentByIdAsync(basicConsultant.DepartmentId);
if (selectedDepartment == null) return TypedResults.Problem("Department does not exist", statusCode: 400);

var consultantList = await GetAllConsultantsAsync(_context);
var validationResults = ConsultantValidators.ValidateUniqueness(consultantList, basicConsultant);

if (validationResults.Count > 0) return TypedResults.ValidationProblem(validationResults);

var newConsultant = CreateConsultantFromModel(basicConsultant, selectedDepartment);
await AddConsultantToDatabaseAsync(_context, newConsultant);
ClearConsultantCache(selectedDepartment.Organization.UrlKey);

return TypedResults.Created($"/consultant/{newConsultant.Id}", basicConsultant);
}
catch
{
// Adding exception handling later
return TypedResults.Problem("An error occurred while processing the request", statusCode: 500);
}
}

private List<ConsultantReadModel> GetConsultantsWithAvailability(string orgUrlKey, int numberOfWeeks)
private List<ConsultantReadModel> GetConsultantsWithAvailability(string orgUrlKey, Week initialWeekNumber,
int numberOfWeeks)
{
if (numberOfWeeks == 8)
{
_cache.TryGetValue(
$"{orgUrlKey}/{CacheKeys.ConsultantAvailability8Weeks}",
$"{orgUrlKey}/{initialWeekNumber}/{CacheKeys.ConsultantAvailability8Weeks}",
out List<ConsultantReadModel>? cachedConsultants);
if (cachedConsultants != null) return cachedConsultants;
}

var consultants = LoadConsultantAvailability(orgUrlKey, numberOfWeeks)
.Select(c => c.MapConsultantToReadModel(numberOfWeeks)).ToList();
var consultants = LoadConsultantAvailability(orgUrlKey, initialWeekNumber, numberOfWeeks)
.Select(c => c.MapConsultantToReadModel(initialWeekNumber, numberOfWeeks)).ToList();

_cache.Set($"{orgUrlKey}/{CacheKeys.ConsultantAvailability8Weeks}", consultants);
_cache.Set($"{orgUrlKey}/{initialWeekNumber}/{CacheKeys.ConsultantAvailability8Weeks}", consultants);
return consultants;
}

private List<Consultant> LoadConsultantAvailability(string orgUrlKey, int numberOfWeeks)
private List<Consultant> LoadConsultantAvailability(string orgUrlKey, Week selectedWeek, int numberOfWeeks)
{
var applicableWeeks = DateService.GetNextWeeks(numberOfWeeks);
var applicableWeeks = DateService.GetNextWeeks(selectedWeek, numberOfWeeks);
var firstDayOfCurrentWeek = DateService.GetFirstDayOfWeekContainingDate(DateTime.Now);
var firstWorkDayOutOfScope =
DateService.GetFirstDayOfWeekContainingDate(DateTime.Now.AddDays(numberOfWeeks * 7));
Expand All @@ -96,17 +74,17 @@ private List<Consultant> LoadConsultantAvailability(string orgUrlKey, int number
// 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.week).Min();
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.week).Where(w => w < minWeekNum + numberOfWeeks).ToList();
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.week).Where(w => w < minWeekNum + numberOfWeeks).ToList();
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();

Expand All @@ -119,52 +97,16 @@ private List<Consultant> LoadConsultantAvailability(string orgUrlKey, int number
.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)
.ThenInclude(pa => pa.Absence)
.Include(c => c.Department)
.ThenInclude(d => d.Organization)
.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)
.ThenInclude(s => s.Project)
.ThenInclude(p => p.Customer)
.OrderBy(c => c.Name)
.ToList();
}


private async Task<Department?> GetDepartmentByIdAsync(string departmentId)
{
return await _context.Department.SingleOrDefaultAsync(d => d.Id == departmentId);
}

private static async Task<List<Consultant>> GetAllConsultantsAsync(ApplicationContext db)
{
return await db.Consultant.ToListAsync();
}

private static Consultant CreateConsultantFromModel(ConsultantWriteModel basicConsultant,
Department selectedDepartment)
{
return new Consultant
{
Name = basicConsultant.Name,
Email = basicConsultant.Email,
Department = selectedDepartment
};
}

private static async Task AddConsultantToDatabaseAsync(ApplicationContext db, Consultant newConsultant)
{
await db.Consultant.AddAsync(newConsultant);
await db.SaveChangesAsync();
}

private void ClearConsultantCache(string orgUrlKey)
{
_cache.Remove($"{orgUrlKey}/{CacheKeys.ConsultantAvailability8Weeks}");
}


public record ConsultantWriteModel(string Name, string Email, string DepartmentId);
}
33 changes: 15 additions & 18 deletions backend/Api/Consultants/ConsultantExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ namespace Api.Consultants;

public static class ConsultantExtensions
{
public static ConsultantReadModel MapConsultantToReadModel(this Consultant consultant, int weeks)
public static ConsultantReadModel MapConsultantToReadModel(this Consultant consultant, Week firstWeek, int weeks)
{
const double tolerance = 0.1;
var currentYear = DateTime.Now.Year;
var yearsOfExperience =
currentYear - (consultant.GraduationYear is null or 0 ? currentYear : consultant.GraduationYear) ?? 0;

var bookedHours = GetBookedHoursForWeeks(consultant, weeks);
var bookedHours = GetBookedHoursForWeeks(consultant, firstWeek, weeks);

var isOccupied = bookedHours.All(b => b.BookingModel.TotalSellableTime <= 0 + tolerance);

Expand Down Expand Up @@ -77,23 +77,20 @@ public static WeeklyBookingReadModel GetBookingModelForWeek(this Consultant cons
bookingList);
}

private static List<BookedHoursPerWeek> GetBookedHoursForWeeks(this Consultant consultant, int weeksAhead)
private static List<BookedHoursPerWeek> GetBookedHoursForWeeks(this Consultant consultant, Week firstWeek,
int weeksAhead)
{
return Enumerable.Range(0, weeksAhead)
.Select(offset =>
{
var year = DateTime.Today.AddDays(7 * offset).Year;
var week = DateService.GetWeekAhead(offset);
var datestring = DateService.GetDatesInWorkWeek(year, week)[0].ToString("dd.MM") + "-" + DateService
.GetDatesInWorkWeek(year, week)[^1].ToString("dd.MM");
return new BookedHoursPerWeek(
year,
week,
datestring,
GetBookingModelForWeek(consultant, year, week)
);
})
var nextWeeks = DateService.GetNextWeeks(firstWeek, weeksAhead);
var datestring = DateService.GetDatesInWorkWeek(firstWeek.Year, firstWeek.WeekNumber)[0].ToString("dd.MM") +
"-" + DateService
.GetDatesInWorkWeek(firstWeek.Year, firstWeek.WeekNumber)[^1].ToString("dd.MM");

return nextWeeks
.Select(week => new BookedHoursPerWeek(
week.Year,
week.WeekNumber,
datestring,
GetBookingModelForWeek(consultant, week.Year, week.WeekNumber)))
.ToList();
}
}
30 changes: 0 additions & 30 deletions backend/Api/Consultants/ConsultantValidators.cs

This file was deleted.

3 changes: 3 additions & 0 deletions backend/Core/DomainModels/Week.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Core.DomainModels;

public record Week(int Year, int WeekNumber);
10 changes: 6 additions & 4 deletions backend/Core/Services/DateService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using Core.DomainModels;

namespace Core.Services;

Expand Down Expand Up @@ -34,15 +35,16 @@ public static bool DateIsInWeek(DateOnly day, int year, int week)
return day.Year == year && GetWeekNumber(day.ToDateTime(TimeOnly.MinValue)) == week;
}

public static List<(int year, int week)> GetNextWeeks(int weeksAhead)
public static List<Week> GetNextWeeks(Week firstWeek, int weeksAhead)
{
var a = FirstWorkDayOfWeek(firstWeek.Year, firstWeek.WeekNumber);
return Enumerable.Range(0, weeksAhead)
.Select(offset =>
{
var year = DateTime.Today.AddDays(7 * offset).Year;
var week = GetWeekAhead(offset);
var year = a.AddDays(7 * offset).Year;
var week = GetWeekNumber(a.AddDays(7 * offset));
return (year, week);
return new Week(year, week);
}).ToList();
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"autoprefixer": "^10.4.16",
"eslint": "8.48.0",
"eslint-config-next": "^13.5.2",
"luxon": "^3.4.3",
"next": "^13.5.2",
"next-auth": "^4.23.2",
"nextjs-toploader": "^1.5.3",
Expand All @@ -30,6 +31,7 @@
},
"devDependencies": {
"@playwright/test": "^1.38.1",
"@types/luxon": "^3.3.3",
"eslint-config-prettier": "^9.0.0",
"prettier": "3.0.3"
}
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/app/[organisation]/bemanning/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,24 @@ import FilteredConsultantsList from "@/components/FilteredConsultantsList";
import { fetchWithToken } from "@/data/fetchWithToken";
import { Consultant, Department } from "@/types";
import { ConsultantFilterProvider } from "@/components/FilteredConsultantProvider";
import { stringToWeek } from "@/data/urlUtils";

export default async function Bemanning({
params,
searchParams,
}: {
params: { organisation: string };
searchParams: { selectedWeek?: string };
}) {
const selectedWeek = stringToWeek(searchParams.selectedWeek || undefined);

const consultants =
(await fetchWithToken<Consultant[]>(
`${params.organisation}/consultants`,
`${params.organisation}/consultants${
selectedWeek
? `?Year=${selectedWeek.year}&Week=${selectedWeek.weekNumber}`
: ""
}`,
)) ?? [];

const departments =
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/FilteredConsultantsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import ConsultantRows from "./ConsultantRows";
import ActiveFilters from "./ActiveFilters";
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";
import WeekSelection from "@/components/WeekSelection";

export default function FilteredConsultantList() {
const { filteredConsultants: consultants } = useFilteredConsultants();
Expand All @@ -10,6 +11,7 @@ export default function FilteredConsultantList() {
<div>
<div>
<ActiveFilters />
<WeekSelection />
</div>
<table className="w-full table-auto border-separate border-spacing-1">
<thead>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/WeekSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";
import { useFilteredConsultants } from "@/hooks/useFilteredConsultants";
import SecondaryButton from "@/components/SecondaryButton";

export default function WeekSelection() {
const { incrementSelectedWeek, resetSelectedWeek, decrementSelectedWeek } =
useFilteredConsultants();

return (
<div className="flex flex-row gap-1">
<SecondaryButton label={"<"} onClick={decrementSelectedWeek} />
<SecondaryButton label={"Denne uka"} onClick={resetSelectedWeek} />
<SecondaryButton label={">"} onClick={incrementSelectedWeek} />
</div>
);
}
22 changes: 22 additions & 0 deletions frontend/src/data/urlUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Week } from "@/types";

export function stringToWeek(urlString?: string): Week | undefined {
if (!urlString) return;
try {
const args = urlString.split("-");
const year = Number.parseInt(args[0]);
const week = Number.parseInt(args[1]);
if (year && week) {
return {
year: year,
weekNumber: week,
};
}
} catch {
return;
}
}

export function weekToString(week: Week) {
return `${week.year}-${week.weekNumber}`;
}
Loading

0 comments on commit 61fb6f2

Please sign in to comment.