Skip to content

Commit

Permalink
Support multiple organisations in one database (#150)
Browse files Browse the repository at this point in the history
Co-authored-by: Magnus Gule <mg@variant.no>
Co-authored-by: Sigrid Elnan <sge@variant.no>
  • Loading branch information
3 people authored Oct 19, 2023
1 parent 679f163 commit 670a6d8
Show file tree
Hide file tree
Showing 40 changed files with 751 additions and 2,242 deletions.
9 changes: 0 additions & 9 deletions STYLEGUIDE.md

This file was deleted.

3 changes: 2 additions & 1 deletion backend/Api/Cache/CacheKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ namespace Api.Cache;

public enum CacheKeys
{
ConsultantAvailability8Weeks
ConsultantAvailability8Weeks,
OrganisationsPrConsultant
}
27 changes: 16 additions & 11 deletions backend/Api/Consultants/ConsultantController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
using Core.DomainModels;
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;

namespace Api.Consultants;

[Route("v0/consultants")]
[Authorize]
[Route("/v0/{orgUrlKey}/consultants")]
[ApiController]
public class ConsultantController : ControllerBase
{
Expand All @@ -26,10 +28,11 @@ public ConsultantController(ApplicationContext context, IMemoryCache cache, Cons

[HttpGet]
public ActionResult<List<ConsultantReadModel>> Get(
[FromRoute] string orgUrlKey,
[FromQuery(Name = "weeks")] int numberOfWeeks = 8,
[FromQuery(Name = "includeOccupied")] bool includeOccupied = true)
{
var consultants = GetConsultantsWithAvailability(numberOfWeeks)
var consultants = GetConsultantsWithAvailability(orgUrlKey, numberOfWeeks)
.Where(c =>
includeOccupied
|| c.IsOccupied
Expand All @@ -56,7 +59,7 @@ public async Task<Results<Created<ConsultantWriteModel>, ProblemHttpResult, Vali

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

return TypedResults.Created($"/consultant/{newConsultant.Id}", basicConsultant);
}
Expand All @@ -67,24 +70,24 @@ public async Task<Results<Created<ConsultantWriteModel>, ProblemHttpResult, Vali
}
}

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

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


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

private List<Consultant> LoadConsultantAvailability(int numberOfWeeks)
private List<Consultant> LoadConsultantAvailability(string orgUrlKey, int numberOfWeeks)
{
var applicableWeeks = DateService.GetNextWeeks(numberOfWeeks);
var firstDayOfCurrentWeek = DateService.GetFirstDayOfWeekContainingDate(DateTime.Now);
Expand Down Expand Up @@ -119,6 +122,8 @@ private List<Consultant> LoadConsultantAvailability(int numberOfWeeks)
(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)
.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)))
Expand Down Expand Up @@ -154,9 +159,9 @@ private static async Task AddConsultantToDatabaseAsync(ApplicationContext db, Co
await db.SaveChangesAsync();
}

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


Expand Down
9 changes: 5 additions & 4 deletions backend/Api/Consultants/ConsultantService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public ConsultantReadModel MapConsultantToReadModel(Consultant consultant, int w
const double tolerance = 0.1;
var bookedHours = GetBookedHoursForWeeks(consultant, weeks);

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

return new ConsultantReadModel(
consultant.Id,
Expand All @@ -36,7 +36,8 @@ public ConsultantReadModel MapConsultantToReadModel(Consultant consultant, int w

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

var holidayHours = _holidayService.GetTotalHolidaysOfWeek(year, week) * hoursPrWorkDay;
var vacationHours = consultant.Vacations.Count(v => DateService.DateIsInWeek(v.Date, year, week)) *
Expand Down Expand Up @@ -73,8 +74,8 @@ public List<BookedHoursPerWeek> GetBookedHoursForWeeks(Consultant consultant, in
.ToList();
}

public double GetHoursPrWeek()
public double GetHoursPrWeek(Consultant consultant)
{
return _organizationOptions.HoursPerWorkday * 5;
return consultant.Department.Organization.HoursPerWorkday * 5;
}
}
16 changes: 0 additions & 16 deletions backend/Api/Departments/DeparmentController.cs

This file was deleted.

42 changes: 42 additions & 0 deletions backend/Api/Organisation/DeparmentController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Database.DatabaseContext;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Api.Organisation;

[Route("/v0/organisations")]
[ApiController]
public class OrganisationController : ControllerBase
{
private readonly ApplicationContext _applicationContext;

public OrganisationController(ApplicationContext applicationContext)
{
_applicationContext = applicationContext;
}

[HttpGet]
public ActionResult<List<OrganisationReadModel>> Get()
{
return _applicationContext.Organization
.Select(organization => new OrganisationReadModel(organization.Name, organization.UrlKey))
.ToList();
}


[HttpGet]
[Route("{orgUrlKey}/departments")]
public ActionResult<List<DepartmentReadModel>> GetDepartment([FromRoute] string orgUrlKey)
{
return _applicationContext.Organization
.Include(o => o.Departments)
.Single(o => o.UrlKey == orgUrlKey)
.Departments
.Select(d => new DepartmentReadModel(d.Id, d.Name))
.ToList();
}
}

public record DepartmentReadModel(string Id, string Name);

public record OrganisationReadModel(string Name, string UrlKey);
3 changes: 2 additions & 1 deletion backend/Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Api.Options;
using Database.DatabaseContext;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using Microsoft.OpenApi.Models;
Expand All @@ -15,7 +16,7 @@

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
builder.Services.AddAuthorization(opt => opt.FallbackPolicy = opt.DefaultPolicy);
builder.Services.AddAuthorization(opt => { opt.FallbackPolicy = opt.DefaultPolicy; });

builder.Services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(connection));

Expand Down
1 change: 1 addition & 0 deletions backend/Core/DomainModels/Absence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class Absence

public required string Name { get; set; }
public required bool ExcludeFromBillRate { get; set; } = false;
public required Organization Organization { get; set; }
}
1 change: 1 addition & 0 deletions backend/Core/DomainModels/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public class Customer
public int Id { get; set; }

public required string Name { get; set; }
public required Organization Organization { get; set; }
public required List<Project> Projects { get; set; }
}
2 changes: 2 additions & 0 deletions backend/Core/DomainModels/Department.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ public class Department
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public required string Id { get; set; }
public required string Name { get; set; }
public int? Hotkey { get; set; }
public required Organization Organization { get; set; }
[JsonIgnore] public required List<Consultant> Consultants { get; set; }
}
20 changes: 20 additions & 0 deletions backend/Core/DomainModels/Organization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;

namespace Core.DomainModels;

public class Organization
{
public required string Id { get; set; } // guid ? Decide What to set here first =>
public required string Name { get; set; }
public required string UrlKey { get; set; } // "variant-as", "variant-sverige"
public required string Country { get; set; }
public required int NumberOfVacationDaysInYear { get; set; }
public required bool HasVacationInChristmas { get; set; }
public required double HoursPerWorkday { get; set; }

[JsonIgnore] public List<Department> Departments { get; set; }

Check warning on line 15 in backend/Core/DomainModels/Organization.cs

View workflow job for this annotation

GitHub Actions / Build and deploy Backend

Non-nullable property 'Departments' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 15 in backend/Core/DomainModels/Organization.cs

View workflow job for this annotation

GitHub Actions / Build and deploy Backend

Non-nullable property 'Departments' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

public required List<Customer> Customers { get; set; }

public List<Absence> AbsenceTypes { get; set; }

Check warning on line 19 in backend/Core/DomainModels/Organization.cs

View workflow job for this annotation

GitHub Actions / Build and deploy Backend

Non-nullable property 'AbsenceTypes' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
4 changes: 4 additions & 0 deletions backend/Database/Database.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@
<ProjectReference Include="..\Core\Core.csproj"/>
</ItemGroup>

<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>

</Project>
30 changes: 28 additions & 2 deletions backend/Database/DatabaseContext/ApplicationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public ApplicationContext(DbContextOptions options) : base(options)
public DbSet<Consultant> Consultant { get; set; } = null!;
public DbSet<Competence> Competence { get; set; } = null!;
public DbSet<Department> Department { get; set; } = null!;
public DbSet<Organization> Organization { get; set; } = null!;
public DbSet<PlannedAbsence> PlannedAbsence { get; set; } = null!;
public DbSet<Vacation> Vacation { get; set; } = null!;
public DbSet<Customer> Customer { get; set; } = null!;
Expand All @@ -29,6 +30,19 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Organization>()
.HasMany(org => org.Departments)
.WithOne(dept => dept.Organization);

modelBuilder.Entity<Organization>()
.HasMany(organization => organization.AbsenceTypes)
.WithOne(absence => absence.Organization);

modelBuilder.Entity<Organization>()
.HasMany(organization => organization.Customers)
.WithOne(customer => customer.Organization);


modelBuilder.Entity<Customer>()
.HasMany(customer => customer.Projects)
.WithOne(project => project.Customer);
Expand All @@ -41,10 +55,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasMany(p => p.Consultants)
.WithMany(c => c.Projects)
.UsingEntity<Staffing>(
r => r.HasOne<Consultant>(s => s.Consultant)
staffing => staffing.HasOne<Consultant>(s => s.Consultant)
.WithMany(c => c.Staffings)
.OnDelete(DeleteBehavior.ClientCascade),
l => l
staffing => staffing
.HasOne<Project>(s => s.Project)
.WithMany(c => c.Staffings)
.OnDelete(DeleteBehavior.Cascade)
Expand Down Expand Up @@ -100,6 +114,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
new() { Id = "project-mgmt", Name = "Project Management" }
});

modelBuilder.Entity<Organization>()
.HasData(new
{
Id = "variant-as",
Name = "Variant AS",
UrlKey = "variant-as",
Country = "norway",
HoursPerWorkday = 7.5,
HasVacationInChristmas = true,
NumberOfVacationDaysInYear = 25
});

modelBuilder.Entity<Department>()
.HasData(new { Id = "trondheim", Name = "Trondheim", OrganizationId = "variant-as" });

Expand Down
Loading

0 comments on commit 670a6d8

Please sign in to comment.