From a4783e2f2ee73aa408bf78a2d688e17e490d35ab Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 09:03:57 +0100 Subject: [PATCH 01/10] chore: clean up AgreementController --- backend/Api/Agreements/AgreementController.cs | 103 +++++++----------- backend/Api/Agreements/AgreementModels.cs | 12 +- 2 files changed, 48 insertions(+), 67 deletions(-) diff --git a/backend/Api/Agreements/AgreementController.cs b/backend/Api/Agreements/AgreementController.cs index 3b74dd9e..8bb99f40 100644 --- a/backend/Api/Agreements/AgreementController.cs +++ b/backend/Api/Agreements/AgreementController.cs @@ -5,9 +5,10 @@ using Infrastructure.DatabaseContext; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -namespace Api.Projects; +namespace Api.Agreements; [Authorize] [Route("/v0/{orgUrlKey}/agreements")] @@ -18,14 +19,16 @@ public class AgreementController( IOrganisationRepository organisationRepository, IAgreementsRepository agreementsRepository) : ControllerBase { + private const string SelectedOrganizationNotFound = "Selected organization not found"; + [HttpGet] - [Route("get/{agreementId}")] + [Route("get/{agreementId:int}")] public async Task> GetAgreement([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); + if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); var agreement = await agreementsRepository.GetAgreementById(agreementId, ct); @@ -43,28 +46,21 @@ public async Task> GetAgreement([FromRoute] str Notes: agreement.Notes, Options: agreement.Options, PriceAdjustmentProcess: agreement.PriceAdjustmentProcess, - Files: agreement.Files.Select(f => new FileReferenceReadModel( - FileName: f.FileName, - BlobName: f.BlobName, - UploadedOn: f.UploadedOn, - UploadedBy: f.UploadedBy ?? "Unknown" - )).ToList() + Files: agreement.Files.Select(f => new FileReferenceReadModel(f)).ToList() ); return Ok(responseModel); } [HttpGet] - [Route("get/engagement/{engagementId}")] + [Route("get/engagement/{engagementId:int}")] public async Task>> GetAgreementsByEngagement([FromRoute] string orgUrlKey, [FromRoute] int engagementId, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); + if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); var agreements = await agreementsRepository.GetAgreementsByEngagementId(engagementId, ct); - if (agreements is null || !agreements.Any()) return NotFound(); - var responseModels = agreements.Select(agreement => new AgreementReadModel( AgreementId: agreement.Id, Name: agreement.Name, @@ -77,29 +73,22 @@ public async Task>> GetAgreementsByEngagem Notes: agreement.Notes, Options: agreement.Options, PriceAdjustmentProcess: agreement.PriceAdjustmentProcess, - Files: agreement.Files.Select(f => new FileReferenceReadModel( - FileName: f.FileName, - BlobName: f.BlobName, - UploadedOn: f.UploadedOn, - UploadedBy: f.UploadedBy ?? "Unknown" - )).ToList() + Files: agreement.Files.Select(f => new FileReferenceReadModel(f)).ToList() )).ToList(); return Ok(responseModels); } [HttpGet] - [Route("get/customer/{customerId}")] + [Route("get/customer/{customerId:int}")] public async Task>> GetAgreementsByCustomer([FromRoute] string orgUrlKey, [FromRoute] int customerId, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); + if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); var agreements = await agreementsRepository.GetAgreementsByCustomerId(customerId, ct); - if (agreements is null || !agreements.Any()) return NotFound(); - var responseModels = agreements.Select(agreement => new AgreementReadModel( AgreementId: agreement.Id, Name: agreement.Name, @@ -112,12 +101,7 @@ public async Task>> GetAgreementsByCustome Notes: agreement.Notes, Options: agreement.Options, PriceAdjustmentProcess: agreement.PriceAdjustmentProcess, - Files: agreement.Files.Select(f => new FileReferenceReadModel( - FileName: f.FileName, - BlobName: f.BlobName, - UploadedOn: f.UploadedOn, - UploadedBy: f.UploadedBy ?? "Unknown" - )).ToList() + Files: agreement.Files.Select(f => new FileReferenceReadModel(f)).ToList() )).ToList(); return Ok(responseModels); @@ -126,7 +110,7 @@ public async Task>> GetAgreementsByCustome [HttpPost] [Route("create")] public async Task> Post([FromRoute] string orgUrlKey, - [FromBody] AgreementWriteModel body, CancellationToken ct) + [FromBody] AgreementWriteModel body, CancellationToken cancellationToken) { if (!ModelState.IsValid) { @@ -139,24 +123,26 @@ public async Task> Post([FromRoute] string orgU return BadRequest(ModelState); } - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) - return BadRequest("Selected organization not found"); + return NotFound(SelectedOrganizationNotFound); Customer? customer = null; if (body.CustomerId != null) { - customer = await context.Customer.FindAsync(body.CustomerId.Value); + customer = await context.Customer.FirstOrDefaultAsync(c => c.Id == body.CustomerId.Value, + cancellationToken); if (customer == null) - return BadRequest("Customer not found"); + return NotFound("Customer not found"); } Engagement? engagement = null; if (body.EngagementId != null) { - engagement = await context.Project.FindAsync(body.EngagementId.Value); + engagement = + await context.Project.FirstOrDefaultAsync(e => e.Id == body.EngagementId.Value, cancellationToken); if (engagement is null) - return BadRequest("Engagement not found"); + return NotFound("Engagement not found"); } var agreement = new Agreement @@ -182,7 +168,7 @@ public async Task> Post([FromRoute] string orgU }).ToList() }; - await agreementsRepository.AddAgreementAsync(agreement, ct); + await agreementsRepository.AddAgreementAsync(agreement, cancellationToken); var responseModel = new AgreementReadModel( Name: agreement.Name, @@ -196,12 +182,7 @@ public async Task> Post([FromRoute] string orgU Notes: agreement.Notes, Options: agreement.Options, PriceAdjustmentProcess: agreement.PriceAdjustmentProcess, - Files: agreement.Files.Select(f => new FileReferenceReadModel( - FileName: f.FileName, - BlobName: f.BlobName, - UploadedOn: f.UploadedOn, - UploadedBy: f.UploadedBy ?? "Unknown" - )).ToList() + Files: agreement.Files.Select(f => new FileReferenceReadModel(f)).ToList() ); cache.Remove($"consultantCacheKey/{orgUrlKey}"); @@ -209,9 +190,9 @@ public async Task> Post([FromRoute] string orgU } [HttpPut] - [Route("update/{agreementId}")] + [Route("update/{agreementId:int}")] public async Task> Put([FromRoute] string orgUrlKey, - [FromRoute] int agreementId, [FromBody] AgreementWriteModel body, CancellationToken ct) + [FromRoute] int agreementId, [FromBody] AgreementWriteModel body, CancellationToken cancellationToken) { if (!ModelState.IsValid) { @@ -224,20 +205,19 @@ public async Task> Put([FromRoute] string orgUr return BadRequest(ModelState); } - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) - return BadRequest("Selected organization not found"); + return NotFound("Selected organization not found"); - var agreement = await agreementsRepository.GetAgreementById(agreementId, ct); + var agreement = await agreementsRepository.GetAgreementById(agreementId, cancellationToken); if (agreement is null) return NotFound("Agreement not found"); - Customer? customer = null; if (body.CustomerId is not null) { - customer = await context.Customer.FindAsync(body.CustomerId); + var customer = await context.Customer.FirstOrDefaultAsync(c => c.Id == body.CustomerId, cancellationToken); if (customer is null) - return BadRequest("Customer not found"); + return NotFound("Customer not found"); agreement.CustomerId = body.CustomerId; agreement.Customer = customer; @@ -248,12 +228,12 @@ public async Task> Put([FromRoute] string orgUr agreement.Customer = null; } - Engagement? engagement = null; if (body.EngagementId is not null) { - engagement = await context.Project.FindAsync(body.EngagementId); + var engagement = + await context.Project.FirstOrDefaultAsync(e => e.Id == body.EngagementId, cancellationToken); if (engagement is null) - return BadRequest("Engagement not found"); + return NotFound("Engagement not found"); agreement.EngagementId = body.EngagementId; agreement.Engagement = engagement; @@ -280,7 +260,7 @@ public async Task> Put([FromRoute] string orgUr UploadedBy = f.UploadedBy ?? "Unknown" }).ToList(); - await agreementsRepository.UpdateAgreementAsync(agreement, ct); + await agreementsRepository.UpdateAgreementAsync(agreement, cancellationToken); var responseModel = new AgreementReadModel( AgreementId: agreement.Id, @@ -294,12 +274,7 @@ public async Task> Put([FromRoute] string orgUr Notes: agreement.Notes, Options: agreement.Options, PriceAdjustmentProcess: agreement.PriceAdjustmentProcess, - Files: agreement.Files.Select(f => new FileReferenceReadModel( - FileName: f.FileName, - BlobName: f.BlobName, - UploadedOn: f.UploadedOn, - UploadedBy: f.UploadedBy ?? "Unknown" - )).ToList() + agreement.Files.Select(f => new FileReferenceReadModel(f)).ToList() ); cache.Remove($"consultantCacheKey/{orgUrlKey}"); @@ -308,11 +283,11 @@ public async Task> Put([FromRoute] string orgUr } [HttpDelete] - [Route("delete/{agreementId}")] + [Route("delete/{agreementId:int}")] public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); + if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); var agreement = await agreementsRepository.GetAgreementById(agreementId, ct); if (agreement is null) return NotFound(); @@ -328,7 +303,7 @@ public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] public async Task>> GetPriceAdjustmentIndexes([FromRoute] string orgUrlKey, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); + if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); var priceAdjustmentIndexes = await agreementsRepository.GetPriceAdjustmentIndexesAsync(ct); diff --git a/backend/Api/Agreements/AgreementModels.cs b/backend/Api/Agreements/AgreementModels.cs index 96cddf7e..89029826 100644 --- a/backend/Api/Agreements/AgreementModels.cs +++ b/backend/Api/Agreements/AgreementModels.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Core.Agreements; public record AgreementReadModel( int AgreementId, @@ -16,12 +15,19 @@ public record AgreementReadModel( string? PriceAdjustmentProcess, List Files ); + public record FileReferenceReadModel( string FileName, string BlobName, DateTime UploadedOn, string? UploadedBy -); +) +{ + public FileReferenceReadModel(FileReference fileReference) : this(fileReference.FileName, fileReference.BlobName, + fileReference.UploadedOn, fileReference.UploadedBy ?? "Unknown") + { + } +} public record AgreementWriteModel( string? Name, From 1bd12782ffab9db2f18653380a2584d3f905bfd7 Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 09:09:28 +0100 Subject: [PATCH 02/10] chore: fix 'agreements' routes --- backend/Api/Agreements/AgreementController.cs | 11 +++++------ frontend/src/actions/agreementActions.ts | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/backend/Api/Agreements/AgreementController.cs b/backend/Api/Agreements/AgreementController.cs index 8bb99f40..53a17766 100644 --- a/backend/Api/Agreements/AgreementController.cs +++ b/backend/Api/Agreements/AgreementController.cs @@ -23,7 +23,7 @@ public class AgreementController( [HttpGet] - [Route("get/{agreementId:int}")] + [Route("{agreementId:int}")] public async Task> GetAgreement([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken ct) { @@ -52,7 +52,7 @@ public async Task> GetAgreement([FromRoute] str } [HttpGet] - [Route("get/engagement/{engagementId:int}")] + [Route("engagement/{engagementId:int}")] public async Task>> GetAgreementsByEngagement([FromRoute] string orgUrlKey, [FromRoute] int engagementId, CancellationToken ct) { @@ -80,7 +80,7 @@ public async Task>> GetAgreementsByEngagem } [HttpGet] - [Route("get/customer/{customerId:int}")] + [Route("customer/{customerId:int}")] public async Task>> GetAgreementsByCustomer([FromRoute] string orgUrlKey, [FromRoute] int customerId, CancellationToken ct) { @@ -108,7 +108,6 @@ public async Task>> GetAgreementsByCustome } [HttpPost] - [Route("create")] public async Task> Post([FromRoute] string orgUrlKey, [FromBody] AgreementWriteModel body, CancellationToken cancellationToken) { @@ -190,7 +189,7 @@ public async Task> Post([FromRoute] string orgU } [HttpPut] - [Route("update/{agreementId:int}")] + [Route("{agreementId:int}")] public async Task> Put([FromRoute] string orgUrlKey, [FromRoute] int agreementId, [FromBody] AgreementWriteModel body, CancellationToken cancellationToken) { @@ -283,7 +282,7 @@ public async Task> Put([FromRoute] string orgUr } [HttpDelete] - [Route("delete/{agreementId:int}")] + [Route("{agreementId:int}")] public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken ct) { var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); diff --git a/frontend/src/actions/agreementActions.ts b/frontend/src/actions/agreementActions.ts index baf87687..330e9971 100644 --- a/frontend/src/actions/agreementActions.ts +++ b/frontend/src/actions/agreementActions.ts @@ -135,7 +135,7 @@ export async function getAgreementsForProject( ) { try { const res = await fetchWithToken( - `${orgUrlKey}/agreements/get/engagement/${projectId}`, + `${orgUrlKey}/agreements/engagement/${projectId}`, ); let agreementsWithDateTypes: Agreement[] = []; @@ -154,7 +154,7 @@ export async function getAgreementsForCustomer( ) { try { const res = await fetchWithToken( - `${orgUrlKey}/agreements/get/customer/${customerId}`, + `${orgUrlKey}/agreements/customer/${customerId}`, ); let agreementsWithDateTypes: Agreement[] = []; @@ -170,7 +170,7 @@ export async function getAgreementsForCustomer( export async function updateAgreement(agreement: Agreement, orgUrlKey: string) { try { const res = await putWithToken( - `${orgUrlKey}/agreements/update/${agreement.agreementId}`, + `${orgUrlKey}/agreements/${agreement.agreementId}`, agreement, ); @@ -190,7 +190,7 @@ export async function createAgreement( ) { try { const res = await postWithToken( - `${orgUrlKey}/agreements/create`, + `${orgUrlKey}/agreements`, agreement, ); let agreementWithDateTypes: Agreement | null = null; @@ -206,7 +206,7 @@ export async function createAgreement( export async function deleteAgreement(agreementId: number, orgUrlKey: string) { try { const res = await deleteWithToken( - `${orgUrlKey}/agreements/delete/${agreementId}`, + `${orgUrlKey}/agreements/${agreementId}`, ); return res; From f71bd01ed599f7b2b675ca8e502b6601d72a15fc Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 09:13:36 +0100 Subject: [PATCH 03/10] chore: move into namespace --- backend/Api/Agreements/AgreementModels.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/Api/Agreements/AgreementModels.cs b/backend/Api/Agreements/AgreementModels.cs index 89029826..e38fc16d 100644 --- a/backend/Api/Agreements/AgreementModels.cs +++ b/backend/Api/Agreements/AgreementModels.cs @@ -1,5 +1,8 @@ using System.ComponentModel.DataAnnotations; using Core.Agreements; +// ReSharper disable NotAccessedPositionalProperty.Global + +namespace Api.Agreements; public record AgreementReadModel( int AgreementId, @@ -55,9 +58,10 @@ public IEnumerable Validate(ValidationContext validationContex } +// ReSharper disable once ClassNeverInstantiated.Global public record FileReferenceWriteModel( string FileName, string BlobName, DateTime UploadedOn, string? UploadedBy -); +); \ No newline at end of file From 4303a7fc897f742fa55a7ccb5d22816f037bf6f5 Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 09:16:45 +0100 Subject: [PATCH 04/10] chore: cleanup ApplicationContext --- .../DatabaseContext/ApplicationContext.cs | 35 +++++++------------ .../ApplicationContextModelSnapshot.cs | 28 +++++++-------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs index c06bbb60..25b2c1b9 100644 --- a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs +++ b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs @@ -13,25 +13,20 @@ namespace Infrastructure.DatabaseContext; -public class ApplicationContext : DbContext +public class ApplicationContext(DbContextOptions options) : DbContext(options) { - public ApplicationContext(DbContextOptions options) : base(options) - { - } - - public DbSet Consultant { get; set; } = null!; - public DbSet Competence { get; set; } = null!; - public DbSet CompetenceConsultant { get; set; } = null!; - public DbSet Department { get; set; } = null!; - public DbSet Organization { get; set; } = null!; - public DbSet Absence { get; set; } = null!; - - public DbSet PlannedAbsence { get; set; } = null!; - public DbSet Vacation { get; set; } = null!; - public DbSet Customer { get; set; } = null!; - public DbSet Project { get; set; } = null!; - public DbSet Staffing { get; set; } = null!; - public DbSet Agreements { get; set; } = null!; + public DbSet Consultant { get; init; } = null!; + public DbSet Competence { get; init; } = null!; + public DbSet CompetenceConsultant { get; init; } = null!; + public DbSet Department { get; init; } = null!; + public DbSet Organization { get; init; } = null!; + public DbSet Absence { get; init; } = null!; + public DbSet PlannedAbsence { get; init; } = null!; + public DbSet Vacation { get; init; } = null!; + public DbSet Customer { get; init; } = null!; + public DbSet Project { get; init; } = null!; + public DbSet Staffing { get; init; } = null!; + public DbSet Agreements { get; init; } = null!; protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) @@ -135,10 +130,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(v => v.EndDate) .HasConversion(); - /*modelBuilder.Entity() - .HasMany(v => v.CompetenceConsultant) - .WithMany();*/ - modelBuilder.Entity() .Property(c => c.TransferredVacationDays) .HasDefaultValue(0); diff --git a/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs b/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs index b4fc0430..0b56970d 100644 --- a/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs +++ b/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs @@ -45,7 +45,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId"); - b.ToTable("Absence"); + b.ToTable("Absence", (string)null); }); modelBuilder.Entity("Core.Agreements.Agreement", b => @@ -92,7 +92,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("EngagementId"); - b.ToTable("Agreements"); + b.ToTable("Agreements", (string)null); }); modelBuilder.Entity("Core.Consultants.Competence", b => @@ -106,7 +106,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Competence"); + b.ToTable("Competence", (string)null); b.HasData( new @@ -148,7 +148,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CompetencesId"); - b.ToTable("CompetenceConsultant"); + b.ToTable("CompetenceConsultant", (string)null); }); modelBuilder.Entity("Core.Consultants.Consultant", b => @@ -192,7 +192,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("DepartmentId"); - b.ToTable("Consultant"); + b.ToTable("Consultant", (string)null); b.HasData( new @@ -228,7 +228,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId", "Name") .IsUnique(); - b.ToTable("Customer"); + b.ToTable("Customer", (string)null); }); modelBuilder.Entity("Core.Engagements.Engagement", b => @@ -258,7 +258,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("CustomerId", "Name") .IsUnique(); - b.ToTable("Project"); + b.ToTable("Project", (string)null); }); modelBuilder.Entity("Core.Organizations.Department", b => @@ -282,7 +282,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId"); - b.ToTable("Department"); + b.ToTable("Department", (string)null); b.HasData( new @@ -321,7 +321,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Organization"); + b.ToTable("Organization", (string)null); b.HasData( new @@ -354,7 +354,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ConsultantId"); - b.ToTable("PlannedAbsence"); + b.ToTable("PlannedAbsence", (string)null); }); modelBuilder.Entity("Core.Staffings.Staffing", b => @@ -375,7 +375,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ConsultantId"); - b.ToTable("Staffing"); + b.ToTable("Staffing", (string)null); }); modelBuilder.Entity("Core.Vacations.Vacation", b => @@ -396,7 +396,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ConsultantId"); - b.ToTable("Vacation"); + b.ToTable("Vacation", (string)null); }); modelBuilder.Entity("Core.Absences.Absence", b => @@ -422,7 +422,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("EngagementId") .OnDelete(DeleteBehavior.Restrict); - b.OwnsMany("Core.Agreements.FileReference", "Files", b1 => + b.OwnsMany("Core.Agreements.Agreement.Files#Core.Agreements.FileReference", "Files", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() @@ -451,7 +451,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasIndex("AgreementId"); - b1.ToTable("FileReference"); + b1.ToTable("FileReference", (string)null); b1.WithOwner() .HasForeignKey("AgreementId"); From 208d26b5b379d7ce46da999f3f0880eacd3323d5 Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 10:11:46 +0100 Subject: [PATCH 05/10] specify SDK 8.0 and C# 13 --- backend/Api/Api.csproj | 2 +- backend/Core/Core.csproj | 1 + backend/Infrastructure/Infrastructure.csproj | 2 +- backend/Tests/Tests.csproj | 1 + backend/global.json | 7 +++++++ 5 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 backend/global.json diff --git a/backend/Api/Api.csproj b/backend/Api/Api.csproj index 65af9b5a..1db3e689 100644 --- a/backend/Api/Api.csproj +++ b/backend/Api/Api.csproj @@ -7,7 +7,7 @@ Linux false false - latest + default diff --git a/backend/Core/Core.csproj b/backend/Core/Core.csproj index f2291091..461ae4a1 100644 --- a/backend/Core/Core.csproj +++ b/backend/Core/Core.csproj @@ -6,6 +6,7 @@ enable false false + default diff --git a/backend/Infrastructure/Infrastructure.csproj b/backend/Infrastructure/Infrastructure.csproj index 98bff962..257af2bb 100644 --- a/backend/Infrastructure/Infrastructure.csproj +++ b/backend/Infrastructure/Infrastructure.csproj @@ -7,7 +7,7 @@ false false Infrastructure - latest + default diff --git a/backend/Tests/Tests.csproj b/backend/Tests/Tests.csproj index 6d669ca6..b8a7f49a 100644 --- a/backend/Tests/Tests.csproj +++ b/backend/Tests/Tests.csproj @@ -7,6 +7,7 @@ false true + default diff --git a/backend/global.json b/backend/global.json new file mode 100644 index 00000000..dad2db5e --- /dev/null +++ b/backend/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file From e39d07ef9c1386faa217779caa4b7d25d697756f Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 10:11:54 +0100 Subject: [PATCH 06/10] move into namespace --- backend/Api/Common/ErrorResponseBody.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/Api/Common/ErrorResponseBody.cs b/backend/Api/Common/ErrorResponseBody.cs index 57027d2e..8a865c08 100644 --- a/backend/Api/Common/ErrorResponseBody.cs +++ b/backend/Api/Common/ErrorResponseBody.cs @@ -1,3 +1,5 @@ +namespace Api.Common; + public record ErrorResponseBody( string code, string message); \ No newline at end of file From d2d7f439cdeaec5f01afa153888c29214874566e Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 10:12:12 +0100 Subject: [PATCH 07/10] add words to user dict --- backend/backend.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/backend.sln.DotSettings diff --git a/backend/backend.sln.DotSettings b/backend/backend.sln.DotSettings new file mode 100644 index 00000000..99fda74f --- /dev/null +++ b/backend/backend.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From bb4db8f244f6534a837ad61ccdcd936aa83f5823 Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 10:43:32 +0100 Subject: [PATCH 08/10] chore: just fix a bunch of warnings --- backend/Api/Agreements/AgreementController.cs | 32 +-- backend/Api/Common/ErrorResponseBody.cs | 4 +- backend/Api/Common/StorageService.cs | 186 +++++++++--------- .../Api/Consultants/ConsultantController.cs | 30 ++- backend/Api/Program.cs | 2 +- backend/Api/Projects/ProjectController.cs | 122 ++++++------ .../StaffingController/StaffingController.cs | 27 +-- .../VacationsController.cs | 22 +-- .../Core/Consultants/IConsultantRepository.cs | 10 +- backend/Core/Customers/Customer.cs | 16 +- backend/Core/Engagements/Engagement.cs | 1 - backend/Core/Organizations/Organization.cs | 55 +++--- .../IPlannedAbsenceRepository.cs | 10 +- backend/Core/Staffings/IStaffingRepository.cs | 8 +- .../Consultants/ConsultantDbRepository.cs | 13 +- .../Organization/DepartmentDbRepository.cs | 3 +- .../PlannedAbsenceCacheRepository.cs | 26 +-- .../PlannedAbsenceDbRepository.cs | 32 +-- .../Repositories/RepositoryExtensions.cs | 1 - .../Staffings/StaffingCacheRepository.cs | 16 +- .../Staffings/StaffingDbRepository.cs | 18 +- backend/Tests/AbsenceTest.cs | 15 +- backend/backend.sln.DotSettings | 8 + 23 files changed, 343 insertions(+), 314 deletions(-) diff --git a/backend/Api/Agreements/AgreementController.cs b/backend/Api/Agreements/AgreementController.cs index 53a17766..b1a6338a 100644 --- a/backend/Api/Agreements/AgreementController.cs +++ b/backend/Api/Agreements/AgreementController.cs @@ -25,12 +25,12 @@ public class AgreementController( [HttpGet] [Route("{agreementId:int}")] public async Task> GetAgreement([FromRoute] string orgUrlKey, - [FromRoute] int agreementId, CancellationToken ct) + [FromRoute] int agreementId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); - var agreement = await agreementsRepository.GetAgreementById(agreementId, ct); + var agreement = await agreementsRepository.GetAgreementById(agreementId, cancellationToken); if (agreement is null) return NotFound(); @@ -54,12 +54,12 @@ public async Task> GetAgreement([FromRoute] str [HttpGet] [Route("engagement/{engagementId:int}")] public async Task>> GetAgreementsByEngagement([FromRoute] string orgUrlKey, - [FromRoute] int engagementId, CancellationToken ct) + [FromRoute] int engagementId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); - var agreements = await agreementsRepository.GetAgreementsByEngagementId(engagementId, ct); + var agreements = await agreementsRepository.GetAgreementsByEngagementId(engagementId, cancellationToken); var responseModels = agreements.Select(agreement => new AgreementReadModel( AgreementId: agreement.Id, @@ -82,12 +82,12 @@ public async Task>> GetAgreementsByEngagem [HttpGet] [Route("customer/{customerId:int}")] public async Task>> GetAgreementsByCustomer([FromRoute] string orgUrlKey, - [FromRoute] int customerId, CancellationToken ct) + [FromRoute] int customerId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); - var agreements = await agreementsRepository.GetAgreementsByCustomerId(customerId, ct); + var agreements = await agreementsRepository.GetAgreementsByCustomerId(customerId, cancellationToken); var responseModels = agreements.Select(agreement => new AgreementReadModel( AgreementId: agreement.Id, @@ -283,15 +283,15 @@ public async Task> Put([FromRoute] string orgUr [HttpDelete] [Route("{agreementId:int}")] - public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken ct) + public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] int agreementId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); - var agreement = await agreementsRepository.GetAgreementById(agreementId, ct); + var agreement = await agreementsRepository.GetAgreementById(agreementId, cancellationToken); if (agreement is null) return NotFound(); - await agreementsRepository.DeleteAgreementAsync(agreementId, ct); + await agreementsRepository.DeleteAgreementAsync(agreementId, cancellationToken); cache.Remove($"consultantCacheKey/{orgUrlKey}"); return Ok("Deleted"); @@ -299,12 +299,12 @@ public async Task Delete([FromRoute] string orgUrlKey, [FromRoute] [HttpGet] [Route("priceAdjustmentIndexes")] - public async Task>> GetPriceAdjustmentIndexes([FromRoute] string orgUrlKey, CancellationToken ct) + public async Task>> GetPriceAdjustmentIndexes([FromRoute] string orgUrlKey, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return NotFound(SelectedOrganizationNotFound); - var priceAdjustmentIndexes = await agreementsRepository.GetPriceAdjustmentIndexesAsync(ct); + var priceAdjustmentIndexes = await agreementsRepository.GetPriceAdjustmentIndexesAsync(cancellationToken); return Ok(priceAdjustmentIndexes); } diff --git a/backend/Api/Common/ErrorResponseBody.cs b/backend/Api/Common/ErrorResponseBody.cs index 8a865c08..2632af48 100644 --- a/backend/Api/Common/ErrorResponseBody.cs +++ b/backend/Api/Common/ErrorResponseBody.cs @@ -1,5 +1,5 @@ - +// ReSharper disable NotAccessedPositionalProperty.Global namespace Api.Common; -public record ErrorResponseBody( string code, string message); \ No newline at end of file +public record ErrorResponseBody(string Code, string Message); \ No newline at end of file diff --git a/backend/Api/Common/StorageService.cs b/backend/Api/Common/StorageService.cs index 5d9aa87f..d98a136a 100644 --- a/backend/Api/Common/StorageService.cs +++ b/backend/Api/Common/StorageService.cs @@ -4,101 +4,91 @@ using Core.DomainModels; using Core.Engagements; using Core.Organizations; -using Core.PlannedAbsences; -using Core.Staffings; using Core.Vacations; using Infrastructure.DatabaseContext; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; +using NuGet.Packaging; namespace Api.Common; -public class StorageService +public class StorageService(IMemoryCache cache, ApplicationContext context) { private const string ConsultantCacheKey = "consultantCacheKey"; - private readonly IMemoryCache _cache; - private readonly ApplicationContext _dbContext; - - public StorageService(IMemoryCache cache, ApplicationContext context) - { - _cache = cache; - _dbContext = context; - } public void ClearConsultantCache(string orgUrlKey) { - _cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); + cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); } public List LoadConsultants(string orgUrlKey) { - if (_cache.TryGetValue>($"{ConsultantCacheKey}/{orgUrlKey}", out var consultants)) - if (consultants != null) - return consultants; + if (cache.TryGetValue>($"{ConsultantCacheKey}/{orgUrlKey}", out var consultants) && + consultants != null) return consultants; var loadedConsultants = LoadConsultantsFromDb(orgUrlKey); - _cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", loadedConsultants); + cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", loadedConsultants); return loadedConsultants; } public Consultant LoadConsultantForSingleWeek(int consultantId, Week week) { - var consultant = _dbContext.Consultant + var consultant = context.Consultant .Include(c => c.Department) .ThenInclude(d => d.Organization) .Single(c => c.Id == consultantId); - consultant.Staffings = _dbContext.Staffing.Where(staffing => + consultant.Staffings = context.Staffing.Where(staffing => staffing.Week.Equals(week) && staffing.ConsultantId == consultantId) - .Include(s => s.Engagement) - .ThenInclude(p => p.Customer) - .Include(s => s.Engagement) - .ThenInclude(e => e.Agreements).ToList(); + .Include(s => s.Engagement) + .ThenInclude(p => p.Customer) + .Include(s => s.Engagement) + .ThenInclude(e => e.Agreements).ToList(); - consultant.PlannedAbsences = _dbContext.PlannedAbsence + consultant.PlannedAbsences = context.PlannedAbsence .Where(absence => absence.Week.Equals(week) && absence.ConsultantId == consultantId).Include(a => a.Absence) .ToList(); - consultant.Vacations = _dbContext.Vacation.Where(vacation => vacation.ConsultantId == consultantId).ToList(); + consultant.Vacations = context.Vacation.Where(vacation => vacation.ConsultantId == consultantId).ToList(); return consultant; } public Consultant LoadConsultantForWeekSet(int consultantId, List weeks) { - var consultant = _dbContext.Consultant + var consultant = context.Consultant .Include(c => c.Department) .ThenInclude(d => d.Organization) .Single(c => c.Id == consultantId); - consultant.Staffings = _dbContext.Staffing.Where(staffing => + consultant.Staffings = context.Staffing.Where(staffing => weeks.Contains(staffing.Week) && staffing.ConsultantId == consultantId) - .Include(s => s.Engagement) - .ThenInclude(p => p.Customer) - .Include(s => s.Engagement) - .ThenInclude(e => e.Agreements) - .ToList(); + .Include(s => s.Engagement) + .ThenInclude(p => p.Customer) + .Include(s => s.Engagement) + .ThenInclude(e => e.Agreements) + .ToList(); - consultant.PlannedAbsences = _dbContext.PlannedAbsence + consultant.PlannedAbsences = context.PlannedAbsence .Where(absence => weeks.Contains(absence.Week) && absence.ConsultantId == consultantId) .Include(a => a.Absence) .ToList(); - consultant.Vacations = _dbContext.Vacation.Where(vacation => vacation.ConsultantId == consultantId).ToList(); + consultant.Vacations = context.Vacation.Where(vacation => vacation.ConsultantId == consultantId).ToList(); return consultant; } public Consultant? GetBaseConsultantById(int id) { - return _dbContext.Consultant.Include(c => c.Department).ThenInclude(d => d.Organization) + return context.Consultant.Include(c => c.Department).ThenInclude(d => d.Organization) .SingleOrDefault(c => c.Id == id); } private List LoadConsultantsFromDb(string orgUrlKey) { - var consultantList = _dbContext.Consultant + var consultantList = context.Consultant .Include(consultant => consultant.Department) .ThenInclude(department => department.Organization) .Include(c => c.CompetenceConsultant) @@ -107,9 +97,10 @@ private List LoadConsultantsFromDb(string orgUrlKey) .OrderBy(consultant => consultant.Name) .ToList(); - var vacationsPrConsultant = _dbContext.Vacation + var vacationsPrConsultant = context.Vacation .Include(vacation => vacation.Consultant) - .GroupBy(vacation => vacation.Consultant.Id) + .Where(vacation => vacation.Consultant != null) + .GroupBy(vacation => vacation.Consultant!.Id) .ToDictionary(grouping => grouping.Key, grouping => grouping.ToList()); var hydratedConsultants = consultantList.Select(consultant => @@ -125,8 +116,8 @@ private List LoadConsultantsFromDb(string orgUrlKey) } - - public Consultant? CreateConsultant(Organization org, ConsultantWriteModel body) + public async Task CreateConsultant(Organization org, ConsultantWriteModel body, + CancellationToken cancellationToken) { var consultant = new Consultant { @@ -135,69 +126,82 @@ private List LoadConsultantsFromDb(string orgUrlKey) StartDate = body.StartDate.HasValue ? DateOnly.FromDateTime(body.StartDate.Value.Date) : null, EndDate = body.EndDate.HasValue ? DateOnly.FromDateTime(body.EndDate.Value.Date) : null, CompetenceConsultant = new List(), - Staffings = new List(), - PlannedAbsences = new List(), - Vacations = new List(), - Department = _dbContext.Department.Single(d => d.Id == body.Department.Id), + Staffings = [], + PlannedAbsences = [], + Vacations = [], + Department = await context.Department.SingleAsync(d => d.Id == body.Department.Id, cancellationToken), GraduationYear = body.GraduationYear, Degree = body.Degree }; - body?.Competences?.ForEach(c => consultant.CompetenceConsultant.Add(new CompetenceConsultant + + var competenceIds = body.Competences?.Select(c => c.Id).ToArray() ?? []; + var competences = await context.Competence + .Where(c => competenceIds.Contains(c.Id)) + .ToArrayAsync(cancellationToken); + + consultant.CompetenceConsultant.AddRange(competences.Select(c => new CompetenceConsultant { - Competence = _dbContext.Competence.Single(comp => comp.Id == c.Id), + Competence = c, Consultant = consultant, CompetencesId = c.Id, ConsultantId = consultant.Id })); - _dbContext.Consultant.Add(consultant); + context.Consultant.Add(consultant); - _dbContext.SaveChanges(); + await context.SaveChangesAsync(cancellationToken); ClearConsultantCache(org.UrlKey); return consultant; } - public Consultant? UpdateConsultant(Organization org, ConsultantWriteModel body) + public async Task UpdateConsultant(Organization org, ConsultantWriteModel body, + CancellationToken cancellationToken) { - var consultant = _dbContext.Consultant + var consultant = await context.Consultant .Include(c => c.CompetenceConsultant) - .Single(c => c.Id == body.Id); - - if (consultant is not null) - { - consultant.Name = body.Name; - consultant.Email = body.Email; - consultant.StartDate = body.StartDate.HasValue ? DateOnly.FromDateTime(body.StartDate.Value.Date) : null; - consultant.EndDate = body.EndDate.HasValue ? DateOnly.FromDateTime(body.EndDate.Value.Date) : null; - consultant.Department = _dbContext.Department.Single(d => d.Id == body.Department.Id); - consultant.GraduationYear = body.GraduationYear; - consultant.Degree = body.Degree; - - // Clear the CompetenceConsultant collection - consultant.CompetenceConsultant.Clear(); - - // For each new competence, create a new CompetenceConsultant entity - foreach (var competence in body?.Competences) - consultant.CompetenceConsultant.Add(new CompetenceConsultant - { - ConsultantId = consultant.Id, - CompetencesId = competence.Id, - Competence = _dbContext.Competence.Single(comp => comp.Id == competence.Id), - Consultant = consultant - }); - } + .FirstOrDefaultAsync(c => c.Id == body.Id, cancellationToken); + + if (consultant is null) return null; + + var department = + await context.Department.FirstOrDefaultAsync(d => d.Id == body.Department.Id, cancellationToken); + + consultant.Name = body.Name; + consultant.Email = body.Email; + consultant.StartDate = body.StartDate.HasValue ? DateOnly.FromDateTime(body.StartDate.Value.Date) : null; + consultant.EndDate = body.EndDate.HasValue ? DateOnly.FromDateTime(body.EndDate.Value.Date) : null; + if (department is not null) consultant.Department = department; + consultant.GraduationYear = body.GraduationYear; + consultant.Degree = body.Degree; + + // Clear the CompetenceConsultant collection + consultant.CompetenceConsultant.Clear(); + + // For each new competence, create a new CompetenceConsultant entity + var competenceIds = body.Competences?.Select(c => c.Id).ToArray() ?? []; + var competences = await context.Competence.Where(c => competenceIds.Contains(c.Id)) + .ToArrayAsync(cancellationToken); + foreach (var competence in competences) + consultant.CompetenceConsultant.Add(new CompetenceConsultant + { + ConsultantId = consultant.Id, + CompetencesId = competence.Id, + Competence = competence, + Consultant = consultant + }); - _dbContext.SaveChanges(); + await context.SaveChangesAsync(cancellationToken); ClearConsultantCache(org.UrlKey); + return consultant; } public Customer UpdateOrCreateCustomer(Organization org, string customerName, string orgUrlKey) { - var customer = _dbContext.Customer.Where(c => c.OrganizationId == org.Id) + var customer = context.Customer.Where(c => c.OrganizationId == org.Id) .SingleOrDefault(c => c.Name == customerName); if (customer is null) @@ -209,10 +213,10 @@ public Customer UpdateOrCreateCustomer(Organization org, string customerName, st Projects = [] }; - _dbContext.Customer.Add(customer); + context.Customer.Add(customer); } - _dbContext.SaveChanges(); + context.SaveChanges(); ClearConsultantCache(orgUrlKey); return customer; @@ -220,12 +224,12 @@ public Customer UpdateOrCreateCustomer(Organization org, string customerName, st public Engagement? GetProjectById(int id) { - return _dbContext.Project.Find(id); + return context.Project.Find(id); } public Engagement GetProjectWithOrganisationById(int id) { - return _dbContext.Project + return context.Project .Where(p => p.Id == id) .Include(p => p.Customer) .ThenInclude(c => c.Organization) @@ -234,7 +238,7 @@ public Engagement GetProjectWithOrganisationById(int id) public Customer? GetCustomerFromId(string orgUrlKey, int customerId) { - return _dbContext.Customer + return context.Customer .Include(c => c.Organization) .Include(c => c.Projects) .ThenInclude(p => p.Staffings) @@ -243,32 +247,32 @@ public Engagement GetProjectWithOrganisationById(int id) public List LoadConsultantVacation(int consultantId) { - return _dbContext.Vacation.Where(v => v.ConsultantId == consultantId).ToList(); + return context.Vacation.Where(v => v.ConsultantId == consultantId).ToList(); } public void RemoveVacationDay(int consultantId, DateOnly date, string orgUrlKey) { - var vacation = _dbContext.Vacation.Single(v => v.ConsultantId == consultantId && v.Date.Equals(date)); + var vacation = context.Vacation.Single(v => v.ConsultantId == consultantId && v.Date.Equals(date)); - _dbContext.Vacation.Remove(vacation); - _dbContext.SaveChanges(); + context.Vacation.Remove(vacation); + context.SaveChanges(); - _cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); + cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); } public void AddVacationDay(int consultantId, DateOnly date, string orgUrlKey) { - var consultant = _dbContext.Consultant.Single(c => c.Id == consultantId); - if (_dbContext.Vacation.Any(v => v.ConsultantId == consultantId && v.Date.Equals(date))) return; + var consultant = context.Consultant.Single(c => c.Id == consultantId); + if (context.Vacation.Any(v => v.ConsultantId == consultantId && v.Date.Equals(date))) return; var vacation = new Vacation { ConsultantId = consultantId, Consultant = consultant, Date = date }; - _dbContext.Add(vacation); - _dbContext.SaveChanges(); + context.Add(vacation); + context.SaveChanges(); - _cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); + cache.Remove($"{ConsultantCacheKey}/{orgUrlKey}"); } } \ No newline at end of file diff --git a/backend/Api/Consultants/ConsultantController.cs b/backend/Api/Consultants/ConsultantController.cs index 23c7228f..b8364d73 100644 --- a/backend/Api/Consultants/ConsultantController.cs +++ b/backend/Api/Consultants/ConsultantController.cs @@ -17,12 +17,14 @@ public class ConsultantController( IOrganisationRepository organisationRepository, IConsultantRepository consultantRepository) : ControllerBase { + private readonly StorageService _storageService = new(cache, context); + [HttpGet] [Route("{Email}")] - public async Task> Get([FromRoute] string orgUrlKey, CancellationToken ct, + public async Task> Get([FromRoute] string orgUrlKey, CancellationToken cancellationToken, [FromRoute(Name = "Email")] string? email = "") { - var consultant = await consultantRepository.GetConsultantByEmail(orgUrlKey, email ?? "", ct); + var consultant = await consultantRepository.GetConsultantByEmail(orgUrlKey, email ?? "", cancellationToken); if (consultant is null) return NotFound(); @@ -30,9 +32,9 @@ public async Task> Get([FromRoute] strin } [HttpGet] - public async Task GetAll([FromRoute] string orgUrlKey, CancellationToken ct) + public async Task GetAll([FromRoute] string orgUrlKey, CancellationToken cancellationToken) { - var consultants = await consultantRepository.GetConsultantsInOrganizationByUrlKey(orgUrlKey, ct); + var consultants = await consultantRepository.GetConsultantsInOrganizationByUrlKey(orgUrlKey, cancellationToken); var readModels = consultants .Select(c => new SingleConsultantReadModel(c)) @@ -43,9 +45,9 @@ public async Task GetAll([FromRoute] string orgUrlKey, Cancellat [HttpGet] [Route("employment")] - public async Task GetConsultantsEmployment([FromRoute] string orgUrlKey, CancellationToken ct) + public async Task GetConsultantsEmployment([FromRoute] string orgUrlKey, CancellationToken cancellationToken) { - var consultants = await consultantRepository.GetConsultantsInOrganizationByUrlKey(orgUrlKey, ct); + var consultants = await consultantRepository.GetConsultantsInOrganizationByUrlKey(orgUrlKey, cancellationToken); var readModels = consultants .Select(c => new ConsultantsEmploymentReadModel(c)) @@ -58,14 +60,12 @@ public async Task GetConsultantsEmployment([FromRoute] string or [HttpPut] public async Task> Put([FromRoute] string orgUrlKey, [FromBody] ConsultantWriteModel body, - CancellationToken ct) + CancellationToken cancellationToken) { - var service = new StorageService(cache, context); - - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest("Selected org not found"); - var consultant = service.UpdateConsultant(selectedOrg, body); + var consultant = await _storageService.UpdateConsultant(selectedOrg, body, cancellationToken); var responseModel = new SingleConsultantReadModel(consultant); @@ -75,14 +75,12 @@ public async Task> Put([FromRoute] strin [HttpPost] public async Task> Post([FromRoute] string orgUrlKey, - [FromBody] ConsultantWriteModel body, CancellationToken ct) + [FromBody] ConsultantWriteModel body, CancellationToken cancellationToken) { - var service = new StorageService(cache, context); - - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest("Selected org not found"); - var consultant = service.CreateConsultant(selectedOrg, body); + var consultant = await _storageService.CreateConsultant(selectedOrg, body, cancellationToken); var responseModel = new SingleConsultantReadModel(consultant); diff --git a/backend/Api/Program.cs b/backend/Api/Program.cs index 7cb82ccf..0f95775d 100644 --- a/backend/Api/Program.cs +++ b/backend/Api/Program.cs @@ -61,4 +61,4 @@ app.UseAuthorization(); -app.Run(); \ No newline at end of file +await app.RunAsync(); \ No newline at end of file diff --git a/backend/Api/Projects/ProjectController.cs b/backend/Api/Projects/ProjectController.cs index 92e9b369..e47fd171 100644 --- a/backend/Api/Projects/ProjectController.cs +++ b/backend/Api/Projects/ProjectController.cs @@ -25,14 +25,14 @@ public class ProjectController( private const string AbsenceCustomerName = "Variant - Fravær"; [HttpGet] - [Route("get/{projectId}")] + [Route("get/{projectId:int}")] public async Task> GetProject([FromRoute] string orgUrlKey, - [FromRoute] int projectId, CancellationToken ct) + [FromRoute] int projectId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest("Selected org not found"); - var engagement = await engagementRepository.GetEngagementById(projectId, ct); + var engagement = await engagementRepository.GetEngagementById(projectId, cancellationToken); if (engagement is null) return NotFound(); @@ -42,17 +42,19 @@ public async Task> GetProject([FromRoute] [HttpGet] public async Task>> Get( - [FromRoute] string orgUrlKey, CancellationToken ct) + [FromRoute] string orgUrlKey, CancellationToken cancellationToken) { - var selectedOrgId = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrgId = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrgId is null) return BadRequest(); var absenceReadModels = new EngagementPerCustomerReadModel(-1, AbsenceCustomerName, - context.Absence.Where(a => a.Organization.UrlKey == orgUrlKey).Select(absence => - new EngagementReadModel(absence.Id, absence.Name, EngagementState.Absence, false)).ToList()); + await context.Absence.Where(a => a.Organization.UrlKey == orgUrlKey).Select(absence => + new EngagementReadModel(absence.Id, absence.Name, EngagementState.Absence, false)) + .ToListAsync(cancellationToken)); - var projectReadModels = context.Project.Include(project => project.Customer) - .Where(project => project.Customer.Organization.UrlKey == orgUrlKey) + var projectReadModels = await context.Project.Include(project => project.Customer) + .Where(project => + project.Customer.Organization != null && project.Customer.Organization.UrlKey == orgUrlKey) .GroupBy(project => project.Customer) .Select(a => new EngagementPerCustomerReadModel( @@ -60,7 +62,7 @@ public async Task>> Get( a.Key.Name, a.Select(e => new EngagementReadModel(e.Id, e.Name, e.State, e.IsBillable)).ToList())) - .ToList(); + .ToListAsync(cancellationToken); projectReadModels.Add(absenceReadModels); var sortedProjectReadModels = projectReadModels.OrderBy(project => project.CustomerName).ToList(); @@ -69,14 +71,14 @@ public async Task>> Get( [HttpGet] - [Route("{customerId}")] + [Route("{customerId:int}")] public async Task> GetCustomerWithEngagements( [FromRoute] string orgUrlKey, [FromRoute] int customerId, - CancellationToken ct + CancellationToken cancellationToken ) { - var selectedOrgId = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrgId = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrgId is null) return BadRequest(); var thisWeek = Week.FromDateTime(DateTime.Now); @@ -124,7 +126,7 @@ public ActionResult Delete(int id) public ActionResult> Put([FromRoute] string orgUrlKey, [FromBody] UpdateProjectWriteModel projectWriteModel) { - // Merk: Service kommer snart via Dependency Injection, da slipper å lage ny hele tiden + // Note: Service will be injected with DI soon var service = new StorageService(cache, context); if (!ProjectControllerValidator.ValidateUpdateProjectWriteModel(projectWriteModel, service, orgUrlKey)) @@ -169,7 +171,7 @@ public ActionResult> Put([FromRoute] string orgUrlKey, public ActionResult> Put([FromRoute] string orgUrlKey, [FromBody] UpdateEngagementNameWriteModel engagementWriteModel) { - // Merk: Service kommer snart via Dependency Injection, da slipper å lage ny hele tiden + // Note: Service will be injected with DI soon var service = new StorageService(cache, context); if (!ProjectControllerValidator.ValidateUpdateEngagementNameWriteModel(engagementWriteModel, service, @@ -202,6 +204,48 @@ public ActionResult> Put([FromRoute] string orgUrlKey, } } + [HttpPut] + public async Task> Put([FromRoute] string orgUrlKey, + [FromBody] EngagementWriteModel body, CancellationToken cancellationToken) + { + var service = new StorageService(cache, context); + + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); + if (selectedOrg is null) return BadRequest("Selected org not found"); + + if (body.CustomerName == AbsenceCustomerName) + return Ok(HandleAbsenceChange(body, orgUrlKey)); + + var customer = service.UpdateOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey); + + var project = await context.Project + .Include(p => p.Customer) + .FirstOrDefaultAsync(p => p.Customer.Id == customer.Id && p.Name == body.ProjectName, cancellationToken); + + if (project is null) + { + project = new Engagement + { + Customer = customer, + State = body.BookingType, + Staffings = new List(), + Consultants = new List(), + Name = body.ProjectName, + IsBillable = body.IsBillable + }; + + context.Project.Add(project); + } + + await context.SaveChangesAsync(cancellationToken); + service.ClearConsultantCache(orgUrlKey); + + var responseModel = + new ProjectWithCustomerModel(project.Name, customer.Name, project.State, project.IsBillable, project.Id); + + return Ok(responseModel); + } + private bool EngagementHasSoftMatch(int id) { var engagementToChange = context.Project @@ -228,7 +272,6 @@ private Engagement MergeProjects(int id) .Single( p => p.Customer == engagementToChange.Customer - // Tror vi også bør sjekke det, så vi unngå å flette med gamle tapte prosjekter o.l. && (p.State == EngagementState.Offer || p.State == EngagementState.Order) && p.Name == engagementToChange.Name && p.IsBillable == engagementToChange.IsBillable @@ -245,51 +288,6 @@ private Engagement MergeProjects(int id) } - [HttpPut] - public async Task> Put([FromRoute] string orgUrlKey, - [FromBody] EngagementWriteModel body, CancellationToken ct) - { - var service = new StorageService(cache, context); - - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); - - if (body.CustomerName == AbsenceCustomerName) - return Ok(HandleAbsenceChange(body, orgUrlKey)); - - var customer = service.UpdateOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey); - - var project = context.Project - .Include(p => p.Customer) - .SingleOrDefault(p => p.Customer.Id == customer.Id - && p.Name == body.ProjectName - ); - - if (project is null) - { - project = new Engagement - { - Customer = customer, - State = body.BookingType, - Staffings = new List(), - Consultants = new List(), - Name = body.ProjectName, - IsBillable = body.IsBillable - }; - - context.Project.Add(project); - } - - await context.SaveChangesAsync(ct); - service.ClearConsultantCache(orgUrlKey); - - var responseModel = - new ProjectWithCustomerModel(project.Name, customer.Name, project.State, project.IsBillable, project.Id); - - return Ok(responseModel); - } - - private ProjectWithCustomerModel HandleAbsenceChange(EngagementWriteModel body, string orgUrlKey) { var absence = context.Absence.Single(a => a.Name == body.ProjectName && a.Organization.UrlKey == orgUrlKey); diff --git a/backend/Api/StaffingController/StaffingController.cs b/backend/Api/StaffingController/StaffingController.cs index c97f2114..8992ad14 100644 --- a/backend/Api/StaffingController/StaffingController.cs +++ b/backend/Api/StaffingController/StaffingController.cs @@ -23,7 +23,7 @@ public class StaffingController( [HttpGet] public async Task Get( [FromRoute] string orgUrlKey, - CancellationToken ct, + CancellationToken cancellationToken, [FromQuery(Name = "Year")] int? selectedYearParam = null, [FromQuery(Name = "Week")] int? selectedWeekParam = null, [FromQuery(Name = "WeekSpan")] int numberOfWeeks = 8, @@ -37,7 +37,7 @@ public async Task Get( var service = new StorageService(cache, context); var consultants = service.LoadConsultants(orgUrlKey); - consultants = await AddRelationalDataToConsultant(consultants, ct); + consultants = await AddRelationalDataToConsultant(consultants, cancellationToken); var readModels = new ReadModelFactory(service) .GetConsultantReadModelsForWeeks(consultants, weekSet); @@ -95,7 +95,7 @@ public ActionResult> GetConsultantsInProject( public async Task> Put( [FromRoute] string orgUrlKey, [FromBody] StaffingWriteModel staffingWriteModel, - CancellationToken ct + CancellationToken cancellationToken ) { var service = new StorageService(cache, context); @@ -113,7 +113,7 @@ CancellationToken ct new StaffingKey(staffingWriteModel.EngagementId, staffingWriteModel.ConsultantId, selectedWeek), staffingWriteModel.Hours); - await staffingRepository.UpsertStaffing(updatedStaffing, ct); + await staffingRepository.UpsertStaffing(updatedStaffing, cancellationToken); //TODO: Remove this once repositories for planned absence and vacations are done too service.ClearConsultantCache(orgUrlKey); @@ -123,7 +123,7 @@ CancellationToken ct staffingWriteModel.ConsultantId, selectedWeek), staffingWriteModel.Hours); - await plannedAbsenceRepository.UpsertPlannedAbsence(updatedAbsence, ct); + await plannedAbsenceRepository.UpsertPlannedAbsence(updatedAbsence, cancellationToken); //TODO: Remove this once repositories for planned absence and vacations are done too service.ClearConsultantCache(orgUrlKey); @@ -131,7 +131,7 @@ CancellationToken ct case BookingType.Vacation: break; default: - throw new ArgumentOutOfRangeException(nameof(staffingWriteModel.Type), staffingWriteModel.Type, + throw new ArgumentOutOfRangeException(nameof(staffingWriteModel), staffingWriteModel.Type, "Invalid bookingType"); } } @@ -150,7 +150,7 @@ CancellationToken ct public async Task> Put( [FromRoute] string orgUrlKey, [FromBody] SeveralStaffingWriteModel severalStaffingWriteModel, - CancellationToken ct + CancellationToken cancellationToken ) { var service = new StorageService(cache, context); @@ -173,7 +173,7 @@ CancellationToken ct var updatedStaffings = GenerateUpdatedStaffings(severalStaffingWriteModel.ConsultantId, severalStaffingWriteModel.EngagementId, weekSet, severalStaffingWriteModel.Hours, orgUrlKey); - await staffingRepository.UpsertMultipleStaffings(updatedStaffings, ct); + await staffingRepository.UpsertMultipleStaffings(updatedStaffings, cancellationToken); //TODO: Remove this once repositories for planned absence and vacations are done too service.ClearConsultantCache(orgUrlKey); @@ -182,7 +182,7 @@ CancellationToken ct var updatedAbsences = GenerateUpdatedAbsences(severalStaffingWriteModel.ConsultantId, severalStaffingWriteModel.EngagementId, weekSet, severalStaffingWriteModel.Hours, orgUrlKey); - await plannedAbsenceRepository.UpsertMultiplePlannedAbsences(updatedAbsences, ct); + await plannedAbsenceRepository.UpsertMultiplePlannedAbsences(updatedAbsences, cancellationToken); //TODO: Remove this once repositories for planned absence and vacations are done too service.ClearConsultantCache(orgUrlKey); @@ -190,7 +190,7 @@ CancellationToken ct case BookingType.Vacation: break; default: - throw new ArgumentOutOfRangeException(nameof(severalStaffingWriteModel.Type), + throw new ArgumentOutOfRangeException(nameof(severalStaffingWriteModel), severalStaffingWriteModel.Type, "Invalid bookingType"); } } @@ -206,13 +206,14 @@ CancellationToken ct // TODO: Move this to a future application layer. This is to consolidate data from various repositories such as Staffing or PlannedAbsence private async Task> AddRelationalDataToConsultant(List consultants, - CancellationToken ct) + CancellationToken cancellationToken) { var consultantIds = consultants.Select(c => c.Id).Distinct().ToList(); var consultantStaffings = - await staffingRepository.GetStaffingForConsultants(consultantIds, ct); - var consultantAbsences = await plannedAbsenceRepository.GetPlannedAbsenceForConsultants(consultantIds, ct); + await staffingRepository.GetStaffingForConsultants(consultantIds, cancellationToken); + var consultantAbsences = + await plannedAbsenceRepository.GetPlannedAbsenceForConsultants(consultantIds, cancellationToken); return consultants.Select(c => { diff --git a/backend/Api/VacationsController/VacationsController.cs b/backend/Api/VacationsController/VacationsController.cs index e2be30c0..001c1710 100644 --- a/backend/Api/VacationsController/VacationsController.cs +++ b/backend/Api/VacationsController/VacationsController.cs @@ -22,9 +22,9 @@ public class VacationsController( [HttpGet] [Route("publicHolidays")] public async Task>> GetPublicHolidays([FromRoute] string orgUrlKey, - CancellationToken ct) + CancellationToken cancellationToken) { - var organization = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var organization = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (organization is null) return BadRequest(); var year = DateOnly.FromDateTime(DateTime.Now).Year; @@ -40,15 +40,15 @@ public async Task>> GetPublicHolidays([FromRoute] st [HttpGet] [Route("{consultantId}/get")] public async Task> GetVacations([FromRoute] string orgUrlKey, - [FromRoute] int consultantId, CancellationToken ct) + [FromRoute] int consultantId, CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest(); var service = new StorageService(cache, context); var vacationDays = service.LoadConsultantVacation(consultantId); - var consultant = await consultantRepository.GetConsultantById(consultantId, ct); + var consultant = await consultantRepository.GetConsultantById(consultantId, cancellationToken); if (consultant is null || !VacationsValidator.ValidateVacation(consultant, orgUrlKey)) return NotFound(); @@ -62,15 +62,15 @@ public async Task> GetVacations([FromRoute] stri public async Task> DeleteVacation([FromRoute] string orgUrlKey, [FromRoute] int consultantId, [FromRoute] string date, - CancellationToken ct) + CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest(); var service = new StorageService(cache, context); var vacationDays = service.LoadConsultantVacation(consultantId); - var consultant = await consultantRepository.GetConsultantById(consultantId, ct); + var consultant = await consultantRepository.GetConsultantById(consultantId, cancellationToken); if (consultant is null || !VacationsValidator.ValidateVacation(consultant, orgUrlKey)) return NotFound(); @@ -95,15 +95,15 @@ public async Task> DeleteVacation([FromRoute] st public async Task> UpdateVacation([FromRoute] string orgUrlKey, [FromRoute] int consultantId, [FromRoute] string date, - CancellationToken ct) + CancellationToken cancellationToken) { - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, cancellationToken); if (selectedOrg is null) return BadRequest(); var service = new StorageService(cache, context); var vacationDays = service.LoadConsultantVacation(consultantId); - var consultant = await consultantRepository.GetConsultantById(consultantId, ct); + var consultant = await consultantRepository.GetConsultantById(consultantId, cancellationToken); if (consultant is null || !VacationsValidator.ValidateVacation(consultant, orgUrlKey)) return NotFound(); diff --git a/backend/Core/Consultants/IConsultantRepository.cs b/backend/Core/Consultants/IConsultantRepository.cs index b826dd0f..c11489bb 100644 --- a/backend/Core/Consultants/IConsultantRepository.cs +++ b/backend/Core/Consultants/IConsultantRepository.cs @@ -3,14 +3,14 @@ namespace Core.Consultants; public interface IConsultantRepository { /** - * Get consultant, including common Department, Organization and Competense-data + * Get consultant, including common Department, Organization and Competence-data */ - Task GetConsultantById(int id, CancellationToken ct); + Task GetConsultantById(int id, CancellationToken cancellationToken); /** - * Get consultant, including common Department, Organization and Competense-data + * Get consultant, including common Department, Organization and Competence-data */ - Task GetConsultantByEmail(string orgUrlKey, string email, CancellationToken ct); + Task GetConsultantByEmail(string orgUrlKey, string email, CancellationToken cancellationToken); - Task> GetConsultantsInOrganizationByUrlKey(string urlKey, CancellationToken ct); + Task> GetConsultantsInOrganizationByUrlKey(string urlKey, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/backend/Core/Customers/Customer.cs b/backend/Core/Customers/Customer.cs index db7581e4..b20deea6 100644 --- a/backend/Core/Customers/Customer.cs +++ b/backend/Core/Customers/Customer.cs @@ -3,16 +3,20 @@ using Core.Engagements; using Core.Organizations; +// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength +// ReSharper disable CollectionNeverUpdated.Global + namespace Core.Customers; public class Customer { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - public string? OrganizationId { get; set; } - public ICollection Agreements { get; set; } = new List(); + public int Id { get; init; } public required string Name { get; set; } - public required Organization Organization { get; set; } - public required List Projects { get; set; } + + public ICollection Agreements { get; init; } = new List(); + public required List Projects { get; init; } + + public string? OrganizationId { get; init; } + public Organization? Organization { get; init; } } \ No newline at end of file diff --git a/backend/Core/Engagements/Engagement.cs b/backend/Core/Engagements/Engagement.cs index 0de67da7..4ce9a525 100644 --- a/backend/Core/Engagements/Engagement.cs +++ b/backend/Core/Engagements/Engagement.cs @@ -46,7 +46,6 @@ public void MergeEngagement(Engagement otherEngagement) Week = s.Week, Hours = s.Hours }); - ; }); } } diff --git a/backend/Core/Organizations/Organization.cs b/backend/Core/Organizations/Organization.cs index 96a0cd5f..2e533a20 100644 --- a/backend/Core/Organizations/Organization.cs +++ b/backend/Core/Organizations/Organization.cs @@ -4,25 +4,26 @@ using Core.DomainModels; using PublicHoliday; +// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength +// ReSharper disable CollectionNeverUpdated.Global + namespace Core.Organizations; 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 Departments { get; set; } - - public required List Customers { get; set; } - - public List AbsenceTypes { get; set; } - - public int GetTotalHolidaysOfWeek(Week week) + public required string Id { get; init; } // guid ? Decide What to set here first => + public required string Name { get; init; } + public required string UrlKey { get; init; } // "variant-as", "variant-sverige" + public required string Country { get; init; } + public required int NumberOfVacationDaysInYear { get; init; } + public required bool HasVacationInChristmas { get; init; } + public required double HoursPerWorkday { get; init; } + + [JsonIgnore] public List Departments { get; init; } = []; + public required List Customers { get; init; } + public required List AbsenceTypes { get; init; } + + private int GetTotalHolidaysOfWeek(Week week) { var datesOfThisWeek = week.GetDatesInWorkWeek(); return datesOfThisWeek.Count(IsHoliday); @@ -72,16 +73,22 @@ public List GetPublicHolidays(int year) { var publicHoliday = GetPublicHoliday(); var publicHolidays = publicHoliday.PublicHolidays(year).Select(DateOnly.FromDateTime).ToList(); - if (HasVacationInChristmas) - { - var startDate = new DateTime(year, 12, 24); - var endDate = new DateTime(year, 12, 31); - var list = Enumerable.Range(0, 1 + endDate.Subtract(startDate).Days) - .Select(offset => DateOnly.FromDateTime(startDate.AddDays(offset))) - .ToList(); - publicHolidays = publicHolidays.Concat(list).Distinct().ToList(); - } + if (!HasVacationInChristmas) return publicHolidays; + + publicHolidays = publicHolidays + .Concat(GetChristmasHolidays(year)) + .Distinct() + .ToList(); return publicHolidays; } + + private static List GetChristmasHolidays(int year) + { + var startDate = new DateOnly(year, 12, 24).ToDateTime(TimeOnly.MinValue); + var endDate = new DateOnly(year, 12, 31).ToDateTime(TimeOnly.MinValue); + return Enumerable.Range(0, 1 + endDate.Subtract(startDate).Days) + .Select(offset => DateOnly.FromDateTime(startDate.AddDays(offset))) + .ToList(); + } } \ No newline at end of file diff --git a/backend/Core/PlannedAbsences/IPlannedAbsenceRepository.cs b/backend/Core/PlannedAbsences/IPlannedAbsenceRepository.cs index 6ad7fa10..9ee666ea 100644 --- a/backend/Core/PlannedAbsences/IPlannedAbsenceRepository.cs +++ b/backend/Core/PlannedAbsences/IPlannedAbsenceRepository.cs @@ -3,11 +3,13 @@ namespace Core.PlannedAbsences; public interface IPlannedAbsenceRepository { public Task>> GetPlannedAbsenceForConsultants(List consultantIds, - CancellationToken ct); + CancellationToken cancellationToken); - public Task> GetPlannedAbsenceForConsultant(int consultantId, CancellationToken ct); + public Task> GetPlannedAbsenceForConsultant(int consultantId, + CancellationToken cancellationToken); - public Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken ct); + public Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken cancellationToken); - public Task UpsertMultiplePlannedAbsences(List plannedAbsences, CancellationToken ct); + public Task UpsertMultiplePlannedAbsences(List plannedAbsences, + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/backend/Core/Staffings/IStaffingRepository.cs b/backend/Core/Staffings/IStaffingRepository.cs index 7f228ccc..e14b3963 100644 --- a/backend/Core/Staffings/IStaffingRepository.cs +++ b/backend/Core/Staffings/IStaffingRepository.cs @@ -3,11 +3,11 @@ namespace Core.Staffings; public interface IStaffingRepository { public Task>> GetStaffingForConsultants(List consultantIds, - CancellationToken ct); + CancellationToken cancellationToken); - public Task> GetStaffingForConsultant(int consultantId, CancellationToken ct); + public Task> GetStaffingForConsultant(int consultantId, CancellationToken cancellationToken); - public Task UpsertStaffing(Staffing staffing, CancellationToken ct); + public Task UpsertStaffing(Staffing staffing, CancellationToken cancellationToken); - public Task UpsertMultipleStaffings(List staffings, CancellationToken ct); + public Task UpsertMultipleStaffings(List staffings, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/backend/Infrastructure/Repositories/Consultants/ConsultantDbRepository.cs b/backend/Infrastructure/Repositories/Consultants/ConsultantDbRepository.cs index 4a2fb0ab..c92df8bc 100644 --- a/backend/Infrastructure/Repositories/Consultants/ConsultantDbRepository.cs +++ b/backend/Infrastructure/Repositories/Consultants/ConsultantDbRepository.cs @@ -7,16 +7,17 @@ namespace Infrastructure.Repositories.Consultants; public class ConsultantDbRepository(ApplicationContext context) : IConsultantRepository { - public Task GetConsultantById(int id, CancellationToken ct) + public Task GetConsultantById(int id, CancellationToken cancellationToken) { return BaseConsultantQuery() - .SingleOrDefaultAsync(c => c.Id == id, ct); + .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); } - public async Task GetConsultantByEmail(string orgUrlKey, string email, CancellationToken ct) + public async Task GetConsultantByEmail(string orgUrlKey, string email, + CancellationToken cancellationToken) { var consultant = await BaseConsultantQuery() - .SingleOrDefaultAsync(c => c.Email == email, ct); + .SingleOrDefaultAsync(c => c.Email == email, cancellationToken); if (consultant is null || consultant.Department.Organization.UrlKey != orgUrlKey) return null; @@ -24,11 +25,11 @@ public class ConsultantDbRepository(ApplicationContext context) : IConsultantRep } public Task> GetConsultantsInOrganizationByUrlKey(string urlKey, - CancellationToken ct) + CancellationToken cancellationToken) { return BaseConsultantQuery() .Where(c => c.Department.Organization.UrlKey == urlKey) - .ToListAsync(ct); + .ToListAsync(cancellationToken); } diff --git a/backend/Infrastructure/Repositories/Organization/DepartmentDbRepository.cs b/backend/Infrastructure/Repositories/Organization/DepartmentDbRepository.cs index 8a74d4c7..4c584681 100644 --- a/backend/Infrastructure/Repositories/Organization/DepartmentDbRepository.cs +++ b/backend/Infrastructure/Repositories/Organization/DepartmentDbRepository.cs @@ -9,7 +9,8 @@ public class DepartmentDbRepository(ApplicationContext context) : IDepartmentRep public async Task> GetDepartmentsInOrganizationByUrlKey(string orgUrlKey, CancellationToken cancellationToken) { - var organizationId = context.Organization.FirstOrDefault(o => o.UrlKey == orgUrlKey)?.Id; + var organizationId = + (await context.Organization.FirstOrDefaultAsync(o => o.UrlKey == orgUrlKey, cancellationToken))?.Id; if (organizationId is null) return []; return await context.Department.Where(d => d.Organization.Id == organizationId).ToListAsync(cancellationToken); diff --git a/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceCacheRepository.cs b/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceCacheRepository.cs index 69a32b27..d3924ab1 100644 --- a/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceCacheRepository.cs +++ b/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceCacheRepository.cs @@ -3,9 +3,11 @@ namespace Infrastructure.Repositories.PlannedAbsences; +// ReSharper disable once ClassNeverInstantiated.Global public class PlannedAbsenceCacheRepository(IPlannedAbsenceRepository sourceRepository, IMemoryCache cache) : IPlannedAbsenceRepository { - public async Task>> GetPlannedAbsenceForConsultants(List consultantIds, CancellationToken ct) + public async Task>> GetPlannedAbsenceForConsultants(List consultantIds, + CancellationToken cancellationToken) { var nonCachedIds = new List(); var result = new Dictionary>(); @@ -19,7 +21,8 @@ public async Task>> GetPlannedAbsenceForCon result.Add(consultantId, plannedAbsenceList); } - var queriedPlannedAbsenceLists = await sourceRepository.GetPlannedAbsenceForConsultants(nonCachedIds, ct); + var queriedPlannedAbsenceLists = + await sourceRepository.GetPlannedAbsenceForConsultants(nonCachedIds, cancellationToken); foreach (var (cId, plannedAbsences) in queriedPlannedAbsenceLists) { result.Add(cId, plannedAbsences); @@ -29,25 +32,27 @@ public async Task>> GetPlannedAbsenceForCon return result; } - public async Task> GetPlannedAbsenceForConsultant(int consultantId, CancellationToken ct) + public async Task> GetPlannedAbsenceForConsultant(int consultantId, + CancellationToken cancellationToken) { var plannedAbsenceList = GetPlannedAbsencesFromCache(consultantId); if (plannedAbsenceList is not null) return plannedAbsenceList; - plannedAbsenceList = await sourceRepository.GetPlannedAbsenceForConsultant(consultantId, ct); + plannedAbsenceList = await sourceRepository.GetPlannedAbsenceForConsultant(consultantId, cancellationToken); cache.Set(PlannedAbsenceCacheKey(consultantId), plannedAbsenceList); return plannedAbsenceList; } - public async Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken ct) + public async Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken cancellationToken) { - await sourceRepository.UpsertPlannedAbsence(plannedAbsence, ct); + await sourceRepository.UpsertPlannedAbsence(plannedAbsence, cancellationToken); ClearPlannedAbsenceCache(plannedAbsence.ConsultantId); } - public async Task UpsertMultiplePlannedAbsences(List plannedAbsences, CancellationToken ct) + public async Task UpsertMultiplePlannedAbsences(List plannedAbsences, + CancellationToken cancellationToken) { - await sourceRepository.UpsertMultiplePlannedAbsences(plannedAbsences, ct); + await sourceRepository.UpsertMultiplePlannedAbsences(plannedAbsences, cancellationToken); var consultantIds = plannedAbsences.Select(pa => pa.ConsultantId).Distinct(); foreach (var consultantId in consultantIds) ClearPlannedAbsenceCache(consultantId); @@ -56,9 +61,8 @@ public async Task UpsertMultiplePlannedAbsences(List plannedAbse private List? GetPlannedAbsencesFromCache(int consultantId) { - if (cache.TryGetValue>(PlannedAbsenceCacheKey(consultantId), out var plannedAbsenceList)) - if (plannedAbsenceList is not null) - return plannedAbsenceList; + if (cache.TryGetValue>(PlannedAbsenceCacheKey(consultantId), out var plannedAbsenceList) && + plannedAbsenceList is not null) return plannedAbsenceList; return null; } diff --git a/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceDbRepository.cs b/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceDbRepository.cs index f28f7bb7..35853f8b 100644 --- a/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceDbRepository.cs +++ b/backend/Infrastructure/Repositories/PlannedAbsences/PlannedAbsenceDbRepository.cs @@ -7,43 +7,45 @@ namespace Infrastructure.Repositories.PlannedAbsences; public class PlannedAbsenceDbRepository(ApplicationContext context) : IPlannedAbsenceRepository { public async Task>> GetPlannedAbsenceForConsultants(List consultantIds, - CancellationToken ct) + CancellationToken cancellationToken) { var ids = consultantIds.ToArray(); return await context.PlannedAbsence - .Where(pa => ids.Contains(pa.ConsultantId)) + .Where(pa => ids.Contains(pa.ConsultantId) && pa.Consultant != null) .Include(absence => absence.Absence) .Include(absence => absence.Consultant) - .GroupBy(absence => absence.Consultant.Id) - .ToDictionaryAsync(grouping => grouping.Key, grouping => grouping.ToList(), ct); + .GroupBy(absence => absence.Consultant!.Id) + .ToDictionaryAsync(grouping => grouping.Key, grouping => grouping.ToList(), cancellationToken); } - public async Task> GetPlannedAbsenceForConsultant(int consultantId, CancellationToken ct) + public async Task> GetPlannedAbsenceForConsultant(int consultantId, + CancellationToken cancellationToken) { return await context.PlannedAbsence .Where(pa => pa.ConsultantId == consultantId) .Include(absence => absence.Absence) .Include(absence => absence.Consultant) - .ToListAsync(ct); + .ToListAsync(cancellationToken); } - public async Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken ct) + public async Task UpsertPlannedAbsence(PlannedAbsence plannedAbsence, CancellationToken cancellationToken) { - var existingPlannedAbsence = context.PlannedAbsence - .FirstOrDefault(pa => pa.AbsenceId.Equals(plannedAbsence.AbsenceId) - && pa.ConsultantId.Equals(plannedAbsence.ConsultantId) - && pa.Week.Equals(plannedAbsence.Week)); + var existingPlannedAbsence = await context.PlannedAbsence + .FirstOrDefaultAsync(pa => pa.AbsenceId.Equals(plannedAbsence.AbsenceId) + && pa.ConsultantId.Equals(plannedAbsence.ConsultantId) + && pa.Week.Equals(plannedAbsence.Week), cancellationToken); if (existingPlannedAbsence is null) - await context.PlannedAbsence.AddAsync(plannedAbsence, ct); + await context.PlannedAbsence.AddAsync(plannedAbsence, cancellationToken); else existingPlannedAbsence.Hours = plannedAbsence.Hours; - await context.SaveChangesAsync(ct); + await context.SaveChangesAsync(cancellationToken); } - public async Task UpsertMultiplePlannedAbsences(List plannedAbsences, CancellationToken ct) + public async Task UpsertMultiplePlannedAbsences(List plannedAbsences, + CancellationToken cancellationToken) { - foreach (var absence in plannedAbsences) await UpsertPlannedAbsence(absence, ct); + foreach (var absence in plannedAbsences) await UpsertPlannedAbsence(absence, cancellationToken); } } \ No newline at end of file diff --git a/backend/Infrastructure/Repositories/RepositoryExtensions.cs b/backend/Infrastructure/Repositories/RepositoryExtensions.cs index c28558ee..0dacafec 100644 --- a/backend/Infrastructure/Repositories/RepositoryExtensions.cs +++ b/backend/Infrastructure/Repositories/RepositoryExtensions.cs @@ -27,7 +27,6 @@ public static void AddRepositories(this WebApplicationBuilder builder) builder.Services.Decorate(); builder.Services.AddScoped(); - //builder.Services.Decorate(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/Infrastructure/Repositories/Staffings/StaffingCacheRepository.cs b/backend/Infrastructure/Repositories/Staffings/StaffingCacheRepository.cs index 34971e9c..e7b5ae83 100644 --- a/backend/Infrastructure/Repositories/Staffings/StaffingCacheRepository.cs +++ b/backend/Infrastructure/Repositories/Staffings/StaffingCacheRepository.cs @@ -6,7 +6,7 @@ namespace Infrastructure.Repositories.Staffings; public class StaffingCacheRepository(IStaffingRepository sourceRepository, IMemoryCache cache) : IStaffingRepository { public async Task>> GetStaffingForConsultants(List consultantIds, - CancellationToken ct) + CancellationToken cancellationToken) { var nonCachedIds = new List(); var result = new Dictionary>(); @@ -20,7 +20,7 @@ public async Task>> GetStaffingForConsultants(Lis result.Add(consultantId, staffingList); } - var queriedStaffingLists = await sourceRepository.GetStaffingForConsultants(nonCachedIds, ct); + var queriedStaffingLists = await sourceRepository.GetStaffingForConsultants(nonCachedIds, cancellationToken); foreach (var (cId, staffings) in queriedStaffingLists) { result.Add(cId, staffings); @@ -30,25 +30,25 @@ public async Task>> GetStaffingForConsultants(Lis return result; } - public async Task> GetStaffingForConsultant(int consultantId, CancellationToken ct) + public async Task> GetStaffingForConsultant(int consultantId, CancellationToken cancellationToken) { var staffingList = GetStaffingsFromCache(consultantId); if (staffingList is not null) return staffingList; - staffingList = await sourceRepository.GetStaffingForConsultant(consultantId, ct); + staffingList = await sourceRepository.GetStaffingForConsultant(consultantId, cancellationToken); cache.Set(StaffingCacheKey(consultantId), staffingList); return staffingList; } - public async Task UpsertStaffing(Staffing staffing, CancellationToken ct) + public async Task UpsertStaffing(Staffing staffing, CancellationToken cancellationToken) { - await sourceRepository.UpsertStaffing(staffing, ct); + await sourceRepository.UpsertStaffing(staffing, cancellationToken); ClearStaffingCache(staffing.ConsultantId); } - public async Task UpsertMultipleStaffings(List staffings, CancellationToken ct) + public async Task UpsertMultipleStaffings(List staffings, CancellationToken cancellationToken) { - await sourceRepository.UpsertMultipleStaffings(staffings, ct); + await sourceRepository.UpsertMultipleStaffings(staffings, cancellationToken); var consultantIds = staffings.Select(staffing => staffing.ConsultantId).Distinct(); foreach (var consultantId in consultantIds) ClearStaffingCache(consultantId); diff --git a/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs b/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs index 9f7654b2..155e5db8 100644 --- a/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs +++ b/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs @@ -7,7 +7,7 @@ namespace Infrastructure.Repositories.Staffings; public class StaffingDbRepository(ApplicationContext context) : IStaffingRepository { public async Task>> GetStaffingForConsultants(List consultantIds, - CancellationToken ct) + CancellationToken cancellationToken) { var ids = consultantIds.ToArray(); @@ -19,25 +19,25 @@ public async Task>> GetStaffingForConsultants(Lis .Include(staffing => staffing.Engagement) .ThenInclude(project => project.Agreements) .GroupBy(staffing => staffing.Consultant.Id) - .ToDictionaryAsync(group => group.Key, grouping => grouping.ToList(), ct); + .ToDictionaryAsync(group => group.Key, grouping => grouping.ToList(), cancellationToken); } - public async Task> GetStaffingForConsultant(int consultantId, CancellationToken ct) + public async Task> GetStaffingForConsultant(int consultantId, CancellationToken cancellationToken) { return await context.Staffing .Where(staffing => staffing.ConsultantId == consultantId) .Include(s => s.Engagement) .ThenInclude(p => p.Customer) - .ToListAsync(ct); + .ToListAsync(cancellationToken); } - public async Task UpsertMultipleStaffings(List staffings, CancellationToken ct) + public async Task UpsertMultipleStaffings(List staffings, CancellationToken cancellationToken) { - foreach (var staffing in staffings) await UpsertStaffing(staffing, ct); + foreach (var staffing in staffings) await UpsertStaffing(staffing, cancellationToken); } - public async Task UpsertStaffing(Staffing staffing, CancellationToken ct) + public async Task UpsertStaffing(Staffing staffing, CancellationToken cancellationToken) { var existingStaffing = context.Staffing .FirstOrDefault(s => s.EngagementId.Equals(staffing.EngagementId) @@ -45,10 +45,10 @@ public async Task UpsertStaffing(Staffing staffing, CancellationToken ct) && s.Week.Equals(staffing.Week)); if (existingStaffing is null) - await context.Staffing.AddAsync(staffing, ct); + await context.Staffing.AddAsync(staffing, cancellationToken); else existingStaffing.Hours = staffing.Hours; - await context.SaveChangesAsync(ct); + await context.SaveChangesAsync(cancellationToken); } } \ No newline at end of file diff --git a/backend/Tests/AbsenceTest.cs b/backend/Tests/AbsenceTest.cs index b04d1f68..456aa231 100644 --- a/backend/Tests/AbsenceTest.cs +++ b/backend/Tests/AbsenceTest.cs @@ -42,9 +42,10 @@ public void AvailabilityCalculation( Country = "norway", NumberOfVacationDaysInYear = 25, HoursPerWorkday = 7.5, - Departments = new List(), + Departments = [], HasVacationInChristmas = true, - Customers = new List() + Customers = [], + AbsenceTypes = [] }; var department = new Department @@ -71,7 +72,7 @@ public void AvailabilityCalculation( 0 => new DateOnly(2023, 9, 4), // Week 36, 4th Sept 2023, (no public holidays) 1 => new DateOnly(2023, 4, 10), // Week 15, 10-14th May 2023 (2.påskedag) 2 => new DateOnly(2023, 4, 3), // Week 14, 3-7th April 2023 (Skjærtorsdag + Langfredag) - 5 => new DateOnly(2022, 12, 26), // Week 52, 26th-30th Descember (Variant Juleferie) + 5 => new DateOnly(2022, 12, 26), // Week 52, 26th-30th December (Variant Juleferie) _ => throw new Exception("Number of holidays can only be set to 0,1,2 or 5") }; @@ -84,7 +85,6 @@ public void AvailabilityCalculation( project.IsBillable = true; - // TODO: Change this to update consultant data for (var i = 0; i < vacationDays; i++) consultant.Vacations.Add(new Vacation { @@ -116,7 +116,7 @@ public void AvailabilityCalculation( ConsultantId = consultant.Id }); - var bookingModel = ReadModelFactory.MapToReadModelList(consultant, new List { week }).Bookings.First() + var bookingModel = ReadModelFactory.MapToReadModelList(consultant, [week]).Bookings[0] .BookingModel; Assert.Multiple(() => @@ -140,7 +140,8 @@ public void MultiplePlannedAbsences() NumberOfVacationDaysInYear = 25, HoursPerWorkday = 7.5, HasVacationInChristmas = false, - Customers = new List() + Customers = [], + AbsenceTypes = [] }; var department = new Department @@ -187,7 +188,7 @@ public void MultiplePlannedAbsences() ConsultantId = consultant.Id }); - var bookedHours = ReadModelFactory.MapToReadModelList(consultant, new List { week }).Bookings.First() + var bookedHours = ReadModelFactory.MapToReadModelList(consultant, [week]).Bookings[0] .BookingModel; Assert.That(bookedHours.TotalPlannedAbsences, Is.EqualTo(30)); diff --git a/backend/backend.sln.DotSettings b/backend/backend.sln.DotSettings index 99fda74f..3bc1e3be 100644 --- a/backend/backend.sln.DotSettings +++ b/backend/backend.sln.DotSettings @@ -1,2 +1,10 @@  + True + True + True + True + True + True + True + True True \ No newline at end of file From 301b1b79c1414e9aeee771e0eb21112f6d37edd1 Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 12:52:43 +0100 Subject: [PATCH 09/10] Fix a loooot of stuff --- backend/Api/Common/StorageService.cs | 3 +- backend/Api/Projects/ProjectController.cs | 9 +- .../StaffingController/ReadModelFactory.cs | 106 +++++++++-------- .../StaffingController/StaffingController.cs | 5 +- .../StaffingController/StaffingReadModel.cs | 73 ++++++------ .../VacationsController.cs | 2 + backend/Core/Customers/Customer.cs | 4 +- backend/Core/Engagements/Engagement.cs | 6 +- backend/Core/Organizations/Organization.cs | 2 +- .../Core/PlannedAbsences/PlannedAbsence.cs | 2 +- backend/Core/Staffings/Staffing.cs | 2 +- backend/Core/{Week => Weeks}/Week.cs | 108 ++++++++++++------ .../DatabaseContext/ApplicationContext.cs | 4 +- .../Staffings/StaffingDbRepository.cs | 10 +- .../ValueConverters/WeekConverter.cs | 4 +- backend/Tests/AbsenceTest.cs | 2 +- backend/backend.sln.DotSettings | 3 + 17 files changed, 193 insertions(+), 152 deletions(-) rename backend/Core/{Week => Weeks}/Week.cs (60%) diff --git a/backend/Api/Common/StorageService.cs b/backend/Api/Common/StorageService.cs index d98a136a..de775e13 100644 --- a/backend/Api/Common/StorageService.cs +++ b/backend/Api/Common/StorageService.cs @@ -1,10 +1,10 @@ using Api.Consultants; using Core.Consultants; using Core.Customers; -using Core.DomainModels; using Core.Engagements; using Core.Organizations; using Core.Vacations; +using Core.Weeks; using Infrastructure.DatabaseContext; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; @@ -210,6 +210,7 @@ public Customer UpdateOrCreateCustomer(Organization org, string customerName, st { Name = customerName, Organization = org, + OrganizationId = org.Id, Projects = [] }; diff --git a/backend/Api/Projects/ProjectController.cs b/backend/Api/Projects/ProjectController.cs index e47fd171..4c7d6e9f 100644 --- a/backend/Api/Projects/ProjectController.cs +++ b/backend/Api/Projects/ProjectController.cs @@ -1,10 +1,8 @@ using Api.Common; using Api.StaffingController; -using Core.Consultants; -using Core.DomainModels; using Core.Engagements; using Core.Organizations; -using Core.Staffings; +using Core.Weeks; using Infrastructure.DatabaseContext; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -228,8 +226,9 @@ public async Task> Put([FromRoute] string { Customer = customer, State = body.BookingType, - Staffings = new List(), - Consultants = new List(), + Staffings = [], + Consultants = [], + Agreements = [], Name = body.ProjectName, IsBillable = body.IsBillable }; diff --git a/backend/Api/StaffingController/ReadModelFactory.cs b/backend/Api/StaffingController/ReadModelFactory.cs index a88e7b38..3f6414e2 100644 --- a/backend/Api/StaffingController/ReadModelFactory.cs +++ b/backend/Api/StaffingController/ReadModelFactory.cs @@ -1,23 +1,23 @@ using Api.Common; using Core.Consultants; -using Core.DomainModels; using Core.Engagements; +using Core.Weeks; namespace Api.StaffingController; -public class ReadModelFactory +public class ReadModelFactory(StorageService storageService) { - private readonly StorageService _storageService; - - public ReadModelFactory(StorageService storageService) + private static bool DoubleEquals(double a, double b) { - _storageService = storageService; + const double epsilon = 0.00001; + return Math.Abs(a - b) < epsilon; } - public List GetConsultantReadModelsForWeeks(List consultants, List weeks) + public static List GetConsultantReadModelsForWeeks(List consultants, + List weeks) { - var firstDayInScope = weeks.First().FirstDayOfWorkWeek(); - var firstWorkDayOutOfScope = weeks.Last().LastWorkDayOfWeek(); + var firstDayInScope = weeks[0].FirstDayOfWorkWeek(); + var firstWorkDayOutOfScope = weeks[^1].LastWorkDayOfWeek(); return consultants .Where(c => c.EndDate == null || c.EndDate > firstDayInScope) @@ -29,16 +29,16 @@ public List GetConsultantReadModelsForWeeks(List public StaffingReadModel GetConsultantReadModelForWeek(int consultantId, Week week) { - var consultant = _storageService.LoadConsultantForSingleWeek(consultantId, week); - var readModel = MapToReadModelList(consultant, new List { week }); + var consultant = storageService.LoadConsultantForSingleWeek(consultantId, week); + var readModel = MapToReadModelList(consultant, [week]); - return new StaffingReadModel(consultant, new List { readModel.Bookings.First() }, + return new StaffingReadModel(consultant, [readModel.Bookings[0]], readModel.DetailedBooking.ToList(), readModel.IsOccupied); } public StaffingReadModel GetConsultantReadModelForWeeks(int consultantId, List weeks) { - var consultant = _storageService.LoadConsultantForWeekSet(consultantId, weeks); + var consultant = storageService.LoadConsultantForWeekSet(consultantId, weeks); var readModel = MapToReadModelList(consultant, weeks); return new StaffingReadModel(consultant, readModel.Bookings, @@ -50,7 +50,7 @@ public List GetConsultantReadModelForWeeks(List consulta var consultants = new List(); foreach (var i in consultantIds) { - var consultant = _storageService.LoadConsultantForWeekSet(i, weeks); + var consultant = storageService.LoadConsultantForWeekSet(i, weeks); var readModel = MapToReadModelList(consultant, weeks); consultants.Add(new StaffingReadModel(consultant, readModel.Bookings, @@ -73,8 +73,7 @@ public static StaffingReadModel MapToReadModelList( ).ToList(); //checks if the consultant has 0 available hours each week - var isOccupied = bookingSummary.All(b => - b.BookingModel.TotalSellableTime == 0); + var isOccupied = bookingSummary.All(b => DoubleEquals(b.BookingModel.TotalSellableTime, 0)); return new StaffingReadModel( consultant, @@ -93,7 +92,6 @@ private static List DetailedBookings(Consultant consultant, { weekSet.Sort(); - // var billableProjects = UniqueWorkTypes(projects, billableStaffing); var billableBookings = consultant.Staffings .Where(staffing => staffing.Engagement.State == EngagementState.Order) .Where(staffing => weekSet.Contains(staffing.Week)) @@ -105,7 +103,7 @@ private static List DetailedBookings(Consultant consultant, grouping.First().Engagement.Agreements.Select(a => (DateTime?)a.EndDate).DefaultIfEmpty(null).Max()), weekSet.Select(week => new WeeklyHours( - week.ToSortableInt(), grouping + week, grouping .Where(staffing => staffing.Week.Equals(week)) .Sum(staffing => staffing.Hours)) ).ToList() @@ -122,7 +120,7 @@ private static List DetailedBookings(Consultant consultant, grouping.First().Engagement.Agreements.Select(a => (DateTime?)a.EndDate).DefaultIfEmpty(null).Max()), weekSet.Select(week => new WeeklyHours( - week.ToSortableInt(), + week, grouping .Where(staffing => staffing.Week.Equals(week)) @@ -139,7 +137,7 @@ private static List DetailedBookings(Consultant consultant, grouping.First().Absence.ExcludeFromBillRate), weekSet.Select(week => new WeeklyHours( - week.ToSortableInt(), + week, grouping .Where(absence => absence.Week.Equals(week)) @@ -157,7 +155,7 @@ private static List DetailedBookings(Consultant consultant, if (vacationsInSet.Count > 0) { var vacationsPrWeek = weekSet.Select(week => new WeeklyHours( - week.ToSortableInt(), + week, vacationsInSet.Count(vacation => week.ContainsDate(vacation.Date)) * consultant.Department.Organization.HoursPerWorkday )).ToList(); @@ -171,18 +169,18 @@ private static List DetailedBookings(Consultant consultant, var startDate = consultant.StartDate; var endDate = consultant.EndDate; - var firstDayInScope = weekSet.First().FirstDayOfWorkWeek(); - var firstWorkDayOutOfScope = weekSet.Last().LastWorkDayOfWeek(); + var firstDayInScope = weekSet[0].FirstDayOfWorkWeek(); + var firstWorkDayOutOfScope = weekSet[^1].LastWorkDayOfWeek(); - if (startDate.HasValue && startDate > firstDayInScope) + if (startDate > firstDayInScope) { - var startWeeks = GetNonEmploymentHoursNotStartedOrQuit(startDate, weekSet, consultant, false); + var startWeeks = GetNonEmploymentHoursNotStartedOrQuit(startDate.Value, weekSet, consultant, false); detailedBookings = detailedBookings.Append(CreateNotStartedOrQuitDetailedBooking(startWeeks)); } - if (endDate.HasValue && endDate < firstWorkDayOutOfScope) + if (endDate < firstWorkDayOutOfScope) { - var endWeeks = GetNonEmploymentHoursNotStartedOrQuit(endDate, weekSet, consultant, true); + var endWeeks = GetNonEmploymentHoursNotStartedOrQuit(endDate.Value, weekSet, consultant, true); detailedBookings = detailedBookings.Append(CreateNotStartedOrQuitDetailedBooking(endWeeks)); } @@ -192,13 +190,13 @@ private static List DetailedBookings(Consultant consultant, return detailedBookingList; } - private static List GetNonEmploymentHoursNotStartedOrQuit(DateOnly? date, List weekSet, + private static List GetNonEmploymentHoursNotStartedOrQuit(DateOnly date, List weekSet, Consultant consultant, bool quit) { return weekSet .Select(week => { - var isTargetWeek = week.ContainsDate((DateOnly)date); + var isTargetWeek = week.ContainsDate(date); var maxWorkHoursForWeek = consultant.Department.Organization.HoursPerWorkday * 5 - consultant.Department.Organization.GetTotalHolidayHoursOfWeek(week); @@ -208,36 +206,38 @@ private static List GetNonEmploymentHoursNotStartedOrQuit(DateOnly? : GetNonEmployedHoursForWeekWhenStarting(date, week, isTargetWeek, consultant); return new WeeklyHours( - week.ToSortableInt(), Math.Min(hoursOutsideEmployment, maxWorkHoursForWeek) + week, Math.Min(hoursOutsideEmployment, maxWorkHoursForWeek) ); }) .ToList(); } - private static double GetNonEmployedHoursForWeekWhenStarting(DateOnly? startDate, Week week, bool isStartWeek, + private static double GetNonEmployedHoursForWeekWhenStarting(DateOnly startDate, Week week, bool isStartWeek, Consultant consultant) { var hasStarted = startDate < week.FirstDayOfWorkWeek(); var dayDifference = Math.Max( - (startDate.Value.ToDateTime(new TimeOnly()) - week.FirstDayOfWorkWeek().ToDateTime(new TimeOnly())) + (startDate.ToDateTime(new TimeOnly()) - week.FirstDayOfWorkWeek().ToDateTime(new TimeOnly())) .Days, 0); - return isStartWeek ? dayDifference * consultant.Department.Organization.HoursPerWorkday : - hasStarted ? 0 : consultant.Department.Organization.HoursPerWorkday * 5; + if (isStartWeek) return dayDifference * consultant.Department.Organization.HoursPerWorkday; + if (!hasStarted) return consultant.Department.Organization.HoursPerWorkday * 5; + return 0; } - private static double GetNonEmployedHoursForWeekWhenQuitting(DateOnly? endDate, Week week, bool isFinalWeek, + private static double GetNonEmployedHoursForWeekWhenQuitting(DateOnly endDate, Week week, bool isFinalWeek, Consultant consultant) { var hasQuit = endDate < week.FirstDayOfWorkWeek(); var dayDifference = Math.Max( - (week.LastWorkDayOfWeek().ToDateTime(new TimeOnly()) - endDate.Value.ToDateTime(new TimeOnly())).Days, + (week.LastWorkDayOfWeek().ToDateTime(new TimeOnly()) - endDate.ToDateTime(new TimeOnly())).Days, 0); - return isFinalWeek ? dayDifference * consultant.Department.Organization.HoursPerWorkday : - hasQuit ? consultant.Department.Organization.HoursPerWorkday * 5 : 0; + if (isFinalWeek) return dayDifference * consultant.Department.Organization.HoursPerWorkday; + if (hasQuit) return consultant.Department.Organization.HoursPerWorkday * 5; + return 0; } @@ -246,7 +246,7 @@ private static DetailedBooking CreateNotStartedOrQuitDetailedBooking(List s.BookingDetails.Type == BookingType.PlannedAbsence && s.BookingDetails.ExcludeFromBilling) + var totalExcludableAbsence = detailedBookingsArray + .Where(s => s.BookingDetails is { Type: BookingType.PlannedAbsence, ExcludeFromBilling: true }) .Select(wh => wh.TotalHoursForWeek(week)) .Sum(); @@ -297,11 +297,9 @@ private static BookedHoursPerWeek GetBookedHours(Week week, IEnumerable GetConsultantsReadModelsForProjectAndWeeks(string orgUrlKey, List weeks, int projectId) { - var firstDayInScope = weeks.First().FirstDayOfWorkWeek(); - var firstWorkDayOutOfScope = weeks.Last().LastWorkDayOfWeek(); + var firstDayInScope = weeks[0].FirstDayOfWorkWeek(); + var firstWorkDayOutOfScope = weeks[^1].LastWorkDayOfWeek(); - var activeConsultants = _storageService.LoadConsultants(orgUrlKey) + var activeConsultants = storageService.LoadConsultants(orgUrlKey) .Where(c => c.EndDate == null || c.EndDate > firstDayInScope) .Where(c => c.StartDate == null || c.StartDate < firstWorkDayOutOfScope) .Where(c => c.Staffings.Any(s => s.EngagementId == projectId && weeks.Contains(s.Week))); @@ -339,10 +337,10 @@ public List GetConsultantsReadModelsForProjectAndWeeks(string public List GetConsultantsReadModelsForAbsenceAndWeeks(string orgUrlKey, List weeks, int absenceId) { - var firstDayInScope = weeks.First().FirstDayOfWorkWeek(); - var firstWorkDayOutOfScope = weeks.Last().LastWorkDayOfWeek(); + var firstDayInScope = weeks[0].FirstDayOfWorkWeek(); + var firstWorkDayOutOfScope = weeks[^1].LastWorkDayOfWeek(); - var consultantsWithAbsenceInSelectedWeeks = _storageService.LoadConsultants(orgUrlKey) + var consultantsWithAbsenceInSelectedWeeks = storageService.LoadConsultants(orgUrlKey) .Where(c => c.EndDate == null || c.EndDate > firstDayInScope) .Where(c => c.StartDate == null || c.StartDate < firstWorkDayOutOfScope) .Where(c => @@ -361,10 +359,10 @@ public List GetConsultantsReadModelsForAbsenceAndWeeks(string public List GetConsultantsReadModelsForVacationsAndWeeks(string orgUrlKey, List weeks) { - var firstDayInScope = weeks.First().FirstDayOfWorkWeek(); - var firstWorkDayOutOfScope = weeks.Last().LastWorkDayOfWeek(); + var firstDayInScope = weeks[0].FirstDayOfWorkWeek(); + var firstWorkDayOutOfScope = weeks[^1].LastWorkDayOfWeek(); - var consultantsWithAbsenceInSelectedWeeks = _storageService.LoadConsultants(orgUrlKey) + var consultantsWithAbsenceInSelectedWeeks = storageService.LoadConsultants(orgUrlKey) .Where(c => c.EndDate == null || c.EndDate > firstDayInScope) .Where(c => c.StartDate == null || c.StartDate < firstWorkDayOutOfScope) .Where(c => diff --git a/backend/Api/StaffingController/StaffingController.cs b/backend/Api/StaffingController/StaffingController.cs index 8992ad14..dc9c6944 100644 --- a/backend/Api/StaffingController/StaffingController.cs +++ b/backend/Api/StaffingController/StaffingController.cs @@ -1,8 +1,8 @@ using Api.Common; using Core.Consultants; -using Core.DomainModels; using Core.PlannedAbsences; using Core.Staffings; +using Core.Weeks; using Infrastructure.DatabaseContext; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -39,8 +39,7 @@ public async Task Get( var consultants = service.LoadConsultants(orgUrlKey); consultants = await AddRelationalDataToConsultant(consultants, cancellationToken); - var readModels = new ReadModelFactory(service) - .GetConsultantReadModelsForWeeks(consultants, weekSet); + var readModels = ReadModelFactory.GetConsultantReadModelsForWeeks(consultants, weekSet); return Ok(readModels); } diff --git a/backend/Api/StaffingController/StaffingReadModel.cs b/backend/Api/StaffingController/StaffingReadModel.cs index b0e60006..7bbcf540 100644 --- a/backend/Api/StaffingController/StaffingReadModel.cs +++ b/backend/Api/StaffingController/StaffingReadModel.cs @@ -1,22 +1,21 @@ -using System.ComponentModel.DataAnnotations; using Core.Consultants; -using Core.DomainModels; +using Core.Weeks; // ReSharper disable NotAccessedPositionalProperty.Global namespace Api.StaffingController; public record StaffingReadModel( - [property: Required] int Id, - [property: Required] string Name, - [property: Required] string Email, - [property: Required] List Competences, - [property: Required] UpdateDepartmentReadModel Department, - [property: Required] int YearsOfExperience, - [property: Required] Degree Degree, - [property: Required] List Bookings, - [property: Required] List DetailedBooking, - [property: Required] bool IsOccupied) + int Id, + string Name, + string Email, + List Competences, + UpdateDepartmentReadModel Department, + int YearsOfExperience, + Degree Degree, + List Bookings, + List DetailedBooking, + bool IsOccupied) { public StaffingReadModel(Consultant consultant, List bookings, List detailedBookings, bool IsOccupied) @@ -42,8 +41,8 @@ public record UpdateDepartmentReadModel( string Name); public record CompetenceReadModel( - [property: Required] string Id, - [property: Required] string Name) + string Id, + string Name) { public CompetenceReadModel(Competence competence) : this( @@ -55,19 +54,17 @@ public CompetenceReadModel(Competence competence) } public record BookedHoursPerWeek( - [property: Required] int Year, - [property: Required] int WeekNumber, - [property: Required] int SortableWeek, - [property: Required] string DateString, - [property: Required] WeeklyBookingReadModel BookingModel); + Week Week, + string DateString, + WeeklyBookingReadModel BookingModel); public record DetailedBooking( - [property: Required] BookingDetails BookingDetails, - [property: Required] List Hours) + BookingDetails BookingDetails, + List Hours) { public double TotalHoursForWeek(Week week) { - return Hours.Where(weeklySum => weeklySum.Week == week.ToSortableInt()).Sum(weeklyHours => weeklyHours.Hours); + return Hours.Where(weeklySum => weeklySum.Week == week).Sum(weeklyHours => weeklyHours.Hours); } internal static double GetTotalHoursPrBookingTypeAndWeek(IEnumerable list, BookingType type, @@ -82,26 +79,26 @@ internal static double GetTotalHoursPrBookingTypeAndWeek(IEnumerable Agreements { get; init; } = new List(); public required List Projects { get; init; } - public string? OrganizationId { get; init; } - public Organization? Organization { get; init; } + public required string OrganizationId { get; init; } + public required Organization Organization { get; init; } } \ No newline at end of file diff --git a/backend/Core/Engagements/Engagement.cs b/backend/Core/Engagements/Engagement.cs index 4ce9a525..786afaf7 100644 --- a/backend/Core/Engagements/Engagement.cs +++ b/backend/Core/Engagements/Engagement.cs @@ -15,13 +15,13 @@ public class Engagement public required Customer Customer { get; set; } - public ICollection Agreements { get; set; } = new List(); + public required List Agreements { get; set; } = []; public required EngagementState State { get; set; } - public List Consultants { get; set; } = new(); + public required List Consultants { get; set; } = []; - public required List Staffings { get; set; } = new(); + public required List Staffings { get; set; } = []; public required string Name { get; set; } diff --git a/backend/Core/Organizations/Organization.cs b/backend/Core/Organizations/Organization.cs index 2e533a20..c87cb87a 100644 --- a/backend/Core/Organizations/Organization.cs +++ b/backend/Core/Organizations/Organization.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using Core.Absences; using Core.Customers; -using Core.DomainModels; +using Core.Weeks; using PublicHoliday; // ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength diff --git a/backend/Core/PlannedAbsences/PlannedAbsence.cs b/backend/Core/PlannedAbsences/PlannedAbsence.cs index 1667f825..6dca6d72 100644 --- a/backend/Core/PlannedAbsences/PlannedAbsence.cs +++ b/backend/Core/PlannedAbsences/PlannedAbsence.cs @@ -1,6 +1,6 @@ using Core.Absences; using Core.Consultants; -using Core.DomainModels; +using Core.Weeks; namespace Core.PlannedAbsences; diff --git a/backend/Core/Staffings/Staffing.cs b/backend/Core/Staffings/Staffing.cs index 78e8baf1..7d7e1561 100644 --- a/backend/Core/Staffings/Staffing.cs +++ b/backend/Core/Staffings/Staffing.cs @@ -1,6 +1,6 @@ using Core.Consultants; -using Core.DomainModels; using Core.Engagements; +using Core.Weeks; namespace Core.Staffings; diff --git a/backend/Core/Week/Week.cs b/backend/Core/Weeks/Week.cs similarity index 60% rename from backend/Core/Week/Week.cs rename to backend/Core/Weeks/Week.cs index d7441c7a..46ed521d 100644 --- a/backend/Core/Week/Week.cs +++ b/backend/Core/Weeks/Week.cs @@ -1,26 +1,24 @@ using System.Globalization; -namespace Core.DomainModels; +namespace Core.Weeks; -public class Week : IComparable, IEquatable +public sealed class Week(int year, int weekNumber) : IComparable, IEquatable { - public readonly int WeekNumber; + public readonly int Year = year; + public readonly int WeekNumber = weekNumber; - public readonly int Year; - - public Week(int year, int weekNumber) + public static Week FromInt(int weekAsInt) { - Year = year; - WeekNumber = weekNumber; + var year = weekAsInt / 100; + var weekNumber = weekAsInt % 100; + return new Week(year, weekNumber); } - public Week(int weekAsInt) + public int ToSortableInt() { - Year = weekAsInt / 100; - WeekNumber = weekAsInt % 100; + return Year * 100 + WeekNumber; } - public int CompareTo(Week? other) { // 1 if this is first @@ -39,6 +37,15 @@ public bool Equals(Week? other) return Year == other.Year && WeekNumber == other.WeekNumber; } + public override bool Equals(object? obj) + { + return typeof(object) == typeof(Week) && Equals(obj as Week); + } + + public override int GetHashCode() + { + return HashCode.Combine(Year, WeekNumber); + } public static Week FromDateTime(DateTime dateTime) { @@ -51,30 +58,29 @@ public static Week FromDateOnly(DateOnly dateOnly) } /// - /// Returns a string in the format yyyyww where y is year and w is week - /// Example: 202352 or 202401 + /// Returns a string in the format Year-WeekNumber. + /// WeekNumber will be padded with a leading zero if needed. + /// + /// For example: + /// + /// Week w = new Week(2025, 1); + /// w.ToString() + /// + /// returns "2025-01" + /// /// public override string ToString() { - return $"{ToSortableInt()}"; - } - - /// - /// Returns an int in the format yyyyww where y is year and w is week - /// Example: 202352 or 202401 - /// - public int ToSortableInt() - { - return Year * 100 + WeekNumber; + return $"{Year}-{WeekNumber:D2}"; } private static int GetWeekNumber(DateTime time) { - // If its Monday, Tuesday or Wednesday, then it'll + // If it's Monday, Tuesday or Wednesday, then it'll // be the same week# as whatever Thursday, Friday or Saturday are, // and we always get those right var day = CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(time); - if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday) time = time.AddDays(3); + if (day is >= DayOfWeek.Monday and <= DayOfWeek.Wednesday) time = time.AddDays(3); // Return the week of our adjusted day return CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(time, CalendarWeekRule.FirstFourDayWeek, @@ -83,24 +89,24 @@ private static int GetWeekNumber(DateTime time) public List GetNextWeeks(int weeksAhead) { - /*Calculate weeks and years based on thursday, as this is the day that is used to figure out weeknumbers + /*Calculate weeks and years based on thursday, as this is the day that is used to figure out week numbers at year´s end*/ var firstThursday = FirstWorkDayOfWeek().AddDays(3); return Enumerable.Range(0, weeksAhead) .Select(offset => { - var year = firstThursday.AddDays(7 * offset).Year; + var y = firstThursday.AddDays(7 * offset).Year; var week = GetWeekNumber(firstThursday.AddDays(7 * offset)); - return new Week(year, week); + return new Week(y, week); }).ToList(); } - + public List GetNextWeeks(Week otherWeek) { var numberOfWeeks = (otherWeek.FirstDayOfWorkWeek().DayNumber - FirstDayOfWorkWeek().DayNumber) / 7; - return GetNextWeeks(numberOfWeeks+1); + return GetNextWeeks(numberOfWeeks + 1); } public List GetDatesInWorkWeek() @@ -120,7 +126,7 @@ public List GetDatesInWorkWeek() private DateTime FirstWorkDayOfWeek() { // Source: https://stackoverflow.com/a/9064954 - var jan1 = new DateTime(Year, 1, 1); + var jan1 = new DateOnly(Year, 1, 1).ToDateTime(TimeOnly.MinValue); var daysOffset = DayOfWeek.Thursday - jan1.DayOfWeek; // Use first Thursday in January to get first week of the year as @@ -162,4 +168,42 @@ private bool DateIsInWeek(DateOnly day) { return FromDateOnly(day).Equals(this); } + + public static bool operator ==(Week left, Week right) + { + return left.Year == right.Year && left.WeekNumber == right.WeekNumber; + } + + public static bool operator !=(Week left, Week right) + { + return !(left == right); + } + + public static bool operator <(Week left, Week right) + { + if (left.Year < right.Year) return true; + if (left.Year > right.Year) return false; + return left.WeekNumber < right.WeekNumber; + } + + public static bool operator >(Week left, Week right) + { + if (left.Year > right.Year) return true; + if (left.Year < right.Year) return false; + return left.WeekNumber > right.WeekNumber; + } + + public static bool operator <=(Week left, Week right) + { + if (left.Year < right.Year) return true; + if (left.Year > right.Year) return false; + return left.WeekNumber <= right.WeekNumber; + } + + public static bool operator >=(Week left, Week right) + { + if (left.Year > right.Year) return true; + if (left.Year < right.Year) return false; + return left.WeekNumber >= right.WeekNumber; + } } \ No newline at end of file diff --git a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs index 25b2c1b9..c3eb259d 100644 --- a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs +++ b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs @@ -2,12 +2,12 @@ using Core.Agreements; using Core.Consultants; using Core.Customers; -using Core.DomainModels; using Core.Engagements; using Core.Organizations; using Core.PlannedAbsences; using Core.Staffings; using Core.Vacations; +using Core.Weeks; using Infrastructure.ValueConverters; using Microsoft.EntityFrameworkCore; @@ -202,8 +202,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) GraduationYear = 2019 }); - - base.OnModelCreating(modelBuilder); } } \ No newline at end of file diff --git a/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs b/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs index 155e5db8..3b733a6f 100644 --- a/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs +++ b/backend/Infrastructure/Repositories/Staffings/StaffingDbRepository.cs @@ -18,7 +18,7 @@ public async Task>> GetStaffingForConsultants(Lis .ThenInclude(project => project.Customer) .Include(staffing => staffing.Engagement) .ThenInclude(project => project.Agreements) - .GroupBy(staffing => staffing.Consultant.Id) + .GroupBy(staffing => staffing.Consultant!.Id) .ToDictionaryAsync(group => group.Key, grouping => grouping.ToList(), cancellationToken); } @@ -39,10 +39,10 @@ public async Task UpsertMultipleStaffings(List staffings, Cancellation public async Task UpsertStaffing(Staffing staffing, CancellationToken cancellationToken) { - var existingStaffing = context.Staffing - .FirstOrDefault(s => s.EngagementId.Equals(staffing.EngagementId) - && s.ConsultantId.Equals(staffing.ConsultantId) - && s.Week.Equals(staffing.Week)); + var existingStaffing = await context.Staffing + .FirstOrDefaultAsync(s => s.EngagementId.Equals(staffing.EngagementId) + && s.ConsultantId.Equals(staffing.ConsultantId) + && s.Week.Equals(staffing.Week), cancellationToken); if (existingStaffing is null) await context.Staffing.AddAsync(staffing, cancellationToken); diff --git a/backend/Infrastructure/ValueConverters/WeekConverter.cs b/backend/Infrastructure/ValueConverters/WeekConverter.cs index 744653a1..eea16ca7 100644 --- a/backend/Infrastructure/ValueConverters/WeekConverter.cs +++ b/backend/Infrastructure/ValueConverters/WeekConverter.cs @@ -1,4 +1,4 @@ -using Core.DomainModels; +using Core.Weeks; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Infrastructure.ValueConverters; @@ -7,7 +7,7 @@ public class WeekConverter : ValueConverter { public WeekConverter() : base( week => week.ToSortableInt(), - weekAsInt => new Week(weekAsInt)) + weekAsInt => Week.FromInt(weekAsInt)) { } } \ No newline at end of file diff --git a/backend/Tests/AbsenceTest.cs b/backend/Tests/AbsenceTest.cs index 456aa231..baa315cb 100644 --- a/backend/Tests/AbsenceTest.cs +++ b/backend/Tests/AbsenceTest.cs @@ -2,12 +2,12 @@ using Core.Absences; using Core.Consultants; using Core.Customers; -using Core.DomainModels; using Core.Engagements; using Core.Organizations; using Core.PlannedAbsences; using Core.Staffings; using Core.Vacations; +using Core.Weeks; using NSubstitute; namespace Tests; diff --git a/backend/backend.sln.DotSettings b/backend/backend.sln.DotSettings index 3bc1e3be..97106184 100644 --- a/backend/backend.sln.DotSettings +++ b/backend/backend.sln.DotSettings @@ -1,10 +1,13 @@  True + True True True True True True True + True True + True True \ No newline at end of file From ddb08c1e4e227ad17769476df025303099b1f8ea Mon Sep 17 00:00:00 2001 From: Truls Henrik Jakobsen Date: Mon, 6 Jan 2025 13:05:32 +0100 Subject: [PATCH 10/10] fix after merge --- backend/Api/Common/StorageService.cs | 41 +++++------ backend/Api/Projects/ProjectController.cs | 88 ++++++----------------- backend/Core/Customers/Customer.cs | 5 +- 3 files changed, 46 insertions(+), 88 deletions(-) diff --git a/backend/Api/Common/StorageService.cs b/backend/Api/Common/StorageService.cs index b689bfe2..6c8e4306 100644 --- a/backend/Api/Common/StorageService.cs +++ b/backend/Api/Common/StorageService.cs @@ -199,39 +199,40 @@ public async Task CreateConsultant(Organization org, ConsultantWrite return consultant; } - public Customer DeactivateOrActivateCustomer(int customerId, Organization org, bool active, string orgUrlKey) + public Customer? DeactivateOrActivateCustomer(int customerId, Organization org, bool active, string orgUrlKey) { var customer = GetCustomerFromId(orgUrlKey, customerId); if (customer is null) return null; customer.IsActive = active; - _dbContext.Customer.Update(customer); - _dbContext.SaveChanges(); + context.Customer.Update(customer); + context.SaveChanges(); ClearConsultantCache(orgUrlKey); return customer; } - public Customer FindOrCreateCustomer(Organization org, string customerName, string orgUrlKey) + public async Task FindOrCreateCustomer(Organization org, string customerName, string orgUrlKey, + CancellationToken cancellationToken) { - var customer = context.Customer.Where(c => c.OrganizationId == org.Id) - .SingleOrDefault(c => c.Name == customerName); + var customer = await context.Customer.Where(c => c.OrganizationId == org.Id) + .FirstOrDefaultAsync(c => c.Name == customerName, cancellationToken); - if (customer is null) + if (customer is not null) return customer; + + customer = new Customer { - customer = new Customer - { - Name = customerName, - Organization = org, - OrganizationId = org.Id, - Projects = [], - IsActive = true - }; - - _dbContext.Customer.Add(customer); - _dbContext.SaveChanges(); - ClearConsultantCache(orgUrlKey); - } + Name = customerName, + Organization = org, + OrganizationId = org.Id, + Projects = [], + Agreements = [], + IsActive = true + }; + + context.Customer.Add(customer); + await context.SaveChangesAsync(cancellationToken); + ClearConsultantCache(orgUrlKey); return customer; diff --git a/backend/Api/Projects/ProjectController.cs b/backend/Api/Projects/ProjectController.cs index 6e7a930d..ddcf2bca 100644 --- a/backend/Api/Projects/ProjectController.cs +++ b/backend/Api/Projects/ProjectController.cs @@ -1,8 +1,5 @@ using Api.Common; using Api.StaffingController; -using Core.Consultants; -using Core.Customers; -using Core.DomainModels; using Core.Engagements; using Core.Organizations; using Core.Weeks; @@ -50,12 +47,13 @@ public async Task>> Get( if (selectedOrgId is null) return BadRequest(); var absenceReadModels = new EngagementPerCustomerReadModel(-1, AbsenceCustomerName, true, - context.Absence.Where(a => a.Organization.UrlKey == orgUrlKey).Select(absence => - new EngagementReadModel(absence.Id, absence.Name, EngagementState.Absence, false)).ToList()); + await context.Absence.Where(a => a.Organization.UrlKey == orgUrlKey).Select(absence => + new EngagementReadModel(absence.Id, absence.Name, EngagementState.Absence, false)) + .ToListAsync(cancellationToken)); var projectReadModels = await context.Project.Include(project => project.Customer) .Where(project => - project.Customer.Organization != null && project.Customer.Organization.UrlKey == orgUrlKey) + project.Customer.Organization.UrlKey == orgUrlKey) .GroupBy(project => project.Customer) .Select(a => new EngagementPerCustomerReadModel( @@ -207,6 +205,20 @@ public ActionResult> Put([FromRoute] string orgUrlKey, } } + + [HttpPut] + [Route("customer/{customerId}/activate")] + public async Task>> Put([FromRoute] int customerId, [FromQuery] bool activate, + string orgUrlKey, CancellationToken ct) + { + var service = new StorageService(cache, context); + var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); + if (selectedOrg is null) return TypedResults.NotFound("Selected org not found"); + var customer = service.DeactivateOrActivateCustomer(customerId, selectedOrg, activate, orgUrlKey); + if (customer is null) return TypedResults.NotFound("Selected customer not found"); + return TypedResults.Ok(); + } + [HttpPut] public async Task> Put([FromRoute] string orgUrlKey, [FromBody] EngagementWriteModel body, CancellationToken cancellationToken) @@ -219,11 +231,13 @@ public async Task> Put([FromRoute] string if (body.CustomerName == AbsenceCustomerName) return Ok(HandleAbsenceChange(body, orgUrlKey)); - var customer = service.UpdateOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey); + var customer = await service.FindOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey, cancellationToken); var project = await context.Project .Include(p => p.Customer) - .FirstOrDefaultAsync(p => p.Customer.Id == customer.Id && p.Name == body.ProjectName, cancellationToken); + .FirstOrDefaultAsync(p => p.Customer.Id == customer.Id + && p.Name == body.ProjectName, cancellationToken + ); if (project is null) { @@ -291,64 +305,6 @@ private Engagement MergeProjects(int id) .Single(p => p.Id == engagementToKeep.Id); } - [HttpPut] - [Route("customer/{customerId}/activate")] - public async Task>> Put([FromRoute] int customerId, [FromQuery] bool activate, string orgUrlKey, CancellationToken ct) - { - - var service = new StorageService(cache, context); - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return TypedResults.NotFound("Selected org not found"); - var customer = service.DeactivateOrActivateCustomer(customerId, selectedOrg, activate, orgUrlKey); - if (customer is null) return TypedResults.NotFound("Selected customer not found"); - return TypedResults.Ok(); - } - - - [HttpPut] - public async Task> Put([FromRoute] string orgUrlKey, - [FromBody] EngagementWriteModel body, CancellationToken ct) - { - var service = new StorageService(cache, context); - - var selectedOrg = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); - if (selectedOrg is null) return BadRequest("Selected org not found"); - - if (body.CustomerName == AbsenceCustomerName) - return Ok(HandleAbsenceChange(body, orgUrlKey)); - - var customer = service.FindOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey); - - var project = context.Project - .Include(p => p.Customer) - .SingleOrDefault(p => p.Customer.Id == customer.Id - && p.Name == body.ProjectName - ); - - if (project is null) - { - project = new Engagement - { - Customer = customer, - State = body.BookingType, - Staffings = new List(), - Consultants = new List(), - Name = body.ProjectName, - IsBillable = body.IsBillable - }; - - context.Project.Add(project); - } - - await context.SaveChangesAsync(ct); - service.ClearConsultantCache(orgUrlKey); - - var responseModel = - new ProjectWithCustomerModel(project.Name, customer.Name, project.State, project.IsBillable, project.Id); - - return Ok(responseModel); - } - private ProjectWithCustomerModel HandleAbsenceChange(EngagementWriteModel body, string orgUrlKey) { diff --git a/backend/Core/Customers/Customer.cs b/backend/Core/Customers/Customer.cs index 08d5bc62..e7bdb411 100644 --- a/backend/Core/Customers/Customer.cs +++ b/backend/Core/Customers/Customer.cs @@ -1,4 +1,3 @@ -using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using Core.Agreements; using Core.Engagements; @@ -14,8 +13,10 @@ public class Customer [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; init; } public required string Name { get; set; } + public required string OrganizationId { get; set; } public required Organization Organization { get; set; } - public required List Projects { get; set; } + public required List Agreements { get; init; } = []; + public required List Projects { get; set; } = []; public bool IsActive { get; set; } = true;