Skip to content

Commit

Permalink
Fix org dependency (#106)
Browse files Browse the repository at this point in the history
Co-authored-by: Magnus Gule <[email protected]>
  • Loading branch information
jonasbjoralt and yelodevopsi authored Oct 6, 2023
1 parent 8c37d8d commit 2801e0b
Show file tree
Hide file tree
Showing 24 changed files with 868 additions and 376 deletions.
29 changes: 18 additions & 11 deletions backend/Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,35 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.10"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.10"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.16.0" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.8" />
<PackageReference Include="PublicHoliday" Version="2.31.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10"/>
<PackageReference Include="Microsoft.Identity.Web" Version="1.16.0"/>
<PackageReference Include="Microsoft.OpenApi" Version="1.6.8"/>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.10"/>
<PackageReference Include="PublicHoliday" Version="2.31.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.11"/>
</ItemGroup>


<ItemGroup>
<ProjectReference Include="..\Database\Database.csproj" />
<ProjectReference Include="..\Database\Database.csproj"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Database\Database.csproj" />
<ProjectReference Include="..\Database\Database.csproj"/>
</ItemGroup>

<ItemGroup>
<_ContentIncludedByDefault Remove="Routes\obj\Api.csproj.nuget.dgspec.json"/>
<_ContentIncludedByDefault Remove="Routes\obj\project.assets.json"/>
<_ContentIncludedByDefault Remove="Routes\obj\project.packagespec.json"/>
</ItemGroup>


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Api.Cache;
using Api.Validators;
using Core.DomainModels;
using Core.Services;
using Database.DatabaseContext;
Expand All @@ -8,71 +7,92 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

namespace Api.Routes;
namespace Api.Consultants;

public static class ConsultantApi
[Route("v0/variants")]
[ApiController]
public class ConsultantController : ControllerBase
{
public static void MapConsultantApi(this RouteGroupBuilder group)
private readonly IMemoryCache _cache;
private readonly ConsultantService _consultantService;
private readonly ApplicationContext _context;

public ConsultantController(ApplicationContext context, IMemoryCache cache, ConsultantService consultantService)
{
group.MapGet("/", GetAllConsultants);
group.MapGet("/{id}", GetConsultantById);
group.MapPost("/", AddBasicConsultant);
_context = context;
_cache = cache;
_consultantService = consultantService;
}

private static Ok<List<ConsultantReadModel>> GetAllConsultants(ApplicationContext context, IMemoryCache cache,
[HttpGet]
public ActionResult<List<ConsultantReadModel>> Get(
[FromQuery(Name = "weeks")] int numberOfWeeks = 8,
[FromQuery(Name = "includeOccupied")] bool includeOccupied = false)
{
var consultants = GetConsultantsWithAvailability(context, cache, numberOfWeeks)
var consultants = GetConsultantsWithAvailability(numberOfWeeks)
.Where(c =>
includeOccupied
|| c.HasAvailability
).ToList();
|| c.IsOccupied
)
.ToList();

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


private static Ok<ConsultantReadModel> GetConsultantById(ApplicationContext context, IMemoryCache cache, int id,
[HttpGet("{id}")]
public Ok<ConsultantReadModel> GetConsultantById(int id,
[FromQuery(Name = "weeks")] int numberOfWeeks = 8)
{
var consultants = GetConsultantsWithAvailability(context, cache, numberOfWeeks);
var consultants = GetConsultantsWithAvailability(numberOfWeeks);
return TypedResults.Ok(consultants.Single(c => c.Id == id));
}

private static ConsultantReadModel MapToReadModel(this Consultant consultant, int weeks)
[HttpPost]
public async Task<Results<Created<ConsultantWriteModel>, ProblemHttpResult, ValidationProblem>> AddBasicConsultant(
[FromBody] ConsultantWriteModel basicVariant)
{
const double tolerance = 0.1;
var bookedHours = consultant.GetBookedHoursForWeeks(weeks);

var hasAvailability = bookedHours.Any(b => b.BookedHours <= consultant.GetHoursPrWeek() - tolerance);

return new ConsultantReadModel(
consultant.Id,
consultant.Name,
consultant.Email,
consultant.Competences.Select(comp => comp.Name).ToList(),
consultant.Department.Name,
bookedHours,
hasAvailability);
try
{
var selectedDepartment = await GetDepartmentByIdAsync(basicVariant.DepartmentId);
if (selectedDepartment == null) return TypedResults.Problem("Department does not exist", statusCode: 400);

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

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

var newConsultant = CreateConsultantFromModel(basicVariant, selectedDepartment);
await AddConsultantToDatabaseAsync(_context, newConsultant);
ClearConsultantCache();

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

private static List<ConsultantReadModel> GetConsultantsWithAvailability(ApplicationContext context,
IMemoryCache cache, int numberOfWeeks)
private List<ConsultantReadModel> GetConsultantsWithAvailability(int numberOfWeeks)
{
if (numberOfWeeks == 8)
{
cache.TryGetValue(CacheKeys.ConsultantAvailability8Weeks, out List<ConsultantReadModel>? cachedConsultants);
_cache.TryGetValue(CacheKeys.ConsultantAvailability8Weeks,
out List<ConsultantReadModel>? cachedConsultants);
if (cachedConsultants != null) return cachedConsultants;
}

var consultants = LoadConsultantAvailability(context, numberOfWeeks);
var consultants = LoadConsultantAvailability(numberOfWeeks)
.Select(c => _consultantService.MapConsultantToReadModel(c, numberOfWeeks)).ToList();

cache.Set(CacheKeys.ConsultantAvailability8Weeks, consultants);

_cache.Set(CacheKeys.ConsultantAvailability8Weeks, consultants);
return consultants;
}

private static List<ConsultantReadModel> LoadConsultantAvailability(ApplicationContext context, int numberOfWeeks)
private List<Consultant> LoadConsultantAvailability(int numberOfWeeks)
{
var applicableWeeks = DateService.GetNextWeeks(numberOfWeeks);

Expand All @@ -95,59 +115,23 @@ private static List<ConsultantReadModel> LoadConsultantAvailability(ApplicationC
var maxWeekB = weeksInB.Max();


return context.Consultant
return _context.Consultant
.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)))
.Include(c => c.Department)
.ThenInclude(d => d.Organization)
.Include(c => c.Staffings.Where(s =>
(s.Year <= yearA && minWeekA <= s.Week && s.Week <= maxWeekA)
|| (yearB <= s.Year && minWeekB <= s.Week && s.Week <= maxWeekB)))
.Select(c => c.MapToReadModel(numberOfWeeks))
.ToList();
}


private static async Task<Results<Created<ConsultantWriteModel>, ProblemHttpResult, ValidationProblem>> AddBasicConsultant(
ApplicationContext db,
IMemoryCache cache,
[FromBody] ConsultantWriteModel basicVariant)
{
try
{
var selectedDepartment = await GetDepartmentByIdAsync(db, basicVariant.DepartmentId);
if (selectedDepartment == null)
{
return TypedResults.Problem("Department does not exist", statusCode: 400);
}

var consultantList = await GetAllConsultantsAsync(db);
var validationResults = ConsultantValidators.ValidateUniqueness(consultantList, basicVariant);

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

var newConsultant = CreateConsultantFromModel(basicVariant, selectedDepartment);
await AddConsultantToDatabaseAsync(db, newConsultant);
ClearConsultantCache(cache);

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

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

private static async Task<List<Consultant>> GetAllConsultantsAsync(ApplicationContext db)
Expand All @@ -172,15 +156,11 @@ private static async Task AddConsultantToDatabaseAsync(ApplicationContext db, Co
await db.SaveChangesAsync();
}

private static void ClearConsultantCache(IMemoryCache cache)
private void ClearConsultantCache()
{
cache.Remove(CacheKeys.ConsultantAvailability8Weeks);
_cache.Remove(CacheKeys.ConsultantAvailability8Weeks);
}


public record ConsultantWriteModel(string Name, string Email, string DepartmentId);

private record ConsultantReadModel(int Id, string Name, string Email, List<string> Competences, string Department,
List<BookedHoursPerWeek> Bookings, bool HasAvailability);

}
6 changes: 6 additions & 0 deletions backend/Api/Consultants/ConsultantReadModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Core.DomainModels;

namespace Api.Consultants;

public record ConsultantReadModel(int Id, string Name, string Email, List<string> Competences, string Department,
List<BookedHoursPerWeek> Bookings, bool IsOccupied);
80 changes: 80 additions & 0 deletions backend/Api/Consultants/ConsultantService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Api.Options;
using Core.DomainModels;
using Core.Services;
using Microsoft.Extensions.Options;

namespace Api.Consultants;

public class ConsultantService
{
private readonly HolidayService _holidayService;
private readonly OrganizationOptions _organizationOptions;

public ConsultantService(IOptions<OrganizationOptions> orgOptions, HolidayService holidayService)
{
_organizationOptions = orgOptions.Value;
_holidayService = holidayService;
}

public ConsultantReadModel MapConsultantToReadModel(Consultant consultant, int weeks)
{
const double tolerance = 0.1;
var bookedHours = GetBookedHoursForWeeks(consultant, weeks);

var isOccupied = bookedHours.All(b => b.BookedHours >= GetHoursPrWeek() - tolerance);

return new ConsultantReadModel(
consultant.Id,
consultant.Name,
consultant.Email,
consultant.Competences.Select(comp => comp.Name).ToList(),
consultant.Department.Name,
bookedHours,
isOccupied
);
}

public double GetBookedHours(Consultant consultant, int year, int week)
{
var hoursPrWorkDay = _organizationOptions.HoursPerWorkday;

var holidayHours = _holidayService.GetTotalHolidaysOfWeek(year, week) * hoursPrWorkDay;
var vacationHours = consultant.Vacations.Count(v => DateService.DateIsInWeek(v.Date, year, week)) *
hoursPrWorkDay;

var plannedAbsenceHours = consultant.PlannedAbsences
.Where(pa => pa.Year == year && pa.WeekNumber == week)
.Select(pa => pa.Hours)
.Sum();

var staffedHours = consultant.Staffings
.Where(s => s.Year == year && s.Week == week)
.Select(s => s.Hours)
.Sum();

var bookedHours = holidayHours + vacationHours + plannedAbsenceHours + staffedHours;
return Math.Min(bookedHours, 5 * hoursPrWorkDay);
}

public List<BookedHoursPerWeek> GetBookedHoursForWeeks(Consultant consultant, int weeksAhead)
{
return Enumerable.Range(0, weeksAhead)
.Select(offset =>
{
var year = DateTime.Today.AddDays(7 * offset).Year;
var week = DateService.GetWeekAhead(offset);

return new BookedHoursPerWeek(
year,
week,
GetBookedHours(consultant, year, week)
);
})
.ToList();
}

public double GetHoursPrWeek()
{
return _organizationOptions.HoursPerWorkday * 5;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.Net.Mail;
using Api.Routes;
using Core.DomainModels;

namespace Api.Validators;
namespace Api.Consultants;

public static class ConsultantValidators
{
Expand All @@ -11,8 +10,8 @@ public static bool IsValidEmail(string email)
return MailAddress.TryCreate(email, out _);
}

public static IDictionary<string, string []> ValidateUniqueness(List<Consultant> consultantList,
ConsultantApi.ConsultantWriteModel body)
public static IDictionary<string, string[]> ValidateUniqueness(List<Consultant> consultantList,
ConsultantController.ConsultantWriteModel body)
{
var results = new Dictionary<string, string[]>();

Expand Down
Loading

0 comments on commit 2801e0b

Please sign in to comment.