Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

236 select week #244

Merged
merged 3 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Litt usikker på å plassere denne filen i data-mappen. Forstår ulempen med en utils-mappe, men jeg tenker personlig det hadde vært enklere å lokalisere denne filen i en mappe som heter utils enn en som heter data 😅 Hva tenker dere?


export function stringToWeek(urlString?: string): Week | undefined {
if (!urlString) return;
try {
const args = urlString.split("-");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kan du kanskje bruke destrukturering her for å gjøre koden lettere å lese? Eks:

const [yearStr, weekStr] = urlString.split("-");
  const year = Number.parseInt(yearStr);
  const week = Number.parseInt(weekStr);

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
Loading