From 39c8d9c4fe902166b4e4124e2f14a591215655bc Mon Sep 17 00:00:00 2001 From: Maria Katrin Bonde Date: Fri, 20 Dec 2024 09:27:10 +0100 Subject: [PATCH 1/9] added deactivate buttons and logic --- .../app/[organisation]/kunder/api/route.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/src/app/[organisation]/kunder/api/route.ts diff --git a/frontend/src/app/[organisation]/kunder/api/route.ts b/frontend/src/app/[organisation]/kunder/api/route.ts new file mode 100644 index 00000000..7392a32a --- /dev/null +++ b/frontend/src/app/[organisation]/kunder/api/route.ts @@ -0,0 +1,21 @@ +import { putWithToken } from "@/data/apiCallsWithToken"; +import { NextRequest, NextResponse } from "next/server"; +export async function PUT( + request: NextRequest, + { + params, + }: { + params: { organisation: string }; + }, +) { + const customerId = request.nextUrl.searchParams.get("customerId"); + const activate = request.nextUrl.searchParams.get("activate"); + const endpointUrl = `${params.organisation}/projects/customer/${customerId}/activate?activate=${activate}`; + + const status = await putWithToken(endpointUrl); + if (status === undefined) { + return NextResponse.error(); + } + + return new NextResponse(); +} From 4aea490ca2eaf28cb0d31f0c9d910248a5edc497 Mon Sep 17 00:00:00 2001 From: Maria Katrin Bonde Date: Fri, 20 Dec 2024 09:28:54 +0100 Subject: [PATCH 2/9] added deactivate buttons and logic --- backend/Api/Common/StorageService.cs | 22 +- backend/Api/Projects/ProjectController.cs | 23 +- backend/Api/Projects/ProjectModels.cs | 28 +- backend/Core/Customers/Customer.cs | 4 + .../DatabaseContext/ApplicationContext.cs | 4 +- ...ctiveOnCustomerWithDefaultTrue.Designer.cs | 631 ++++++++++++++++++ ..._AddedIsActiveOnCustomerWithDefaultTrue.cs | 29 + .../ApplicationContextModelSnapshot.cs | 3 + frontend/mockdata/mockData.ts | 1 + frontend/src/api-types.ts | 2 + .../components/CostumerTable/CustomerRow.tsx | 35 +- .../CustomerSidebarWithFilters.tsx | 5 +- .../CostumerTable/CustomerTable.tsx | 6 +- .../src/components/SearchBarComponent.tsx | 4 +- frontend/src/components/Staffing/WeekCell.tsx | 2 +- frontend/src/components/StaffingSidebar.tsx | 6 +- frontend/tailwind.config.ts | 1 + 17 files changed, 780 insertions(+), 26 deletions(-) create mode 100644 backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.Designer.cs create mode 100644 backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.cs diff --git a/backend/Api/Common/StorageService.cs b/backend/Api/Common/StorageService.cs index 5d9aa87f..c5bb83b5 100644 --- a/backend/Api/Common/StorageService.cs +++ b/backend/Api/Common/StorageService.cs @@ -195,7 +195,20 @@ private List LoadConsultantsFromDb(string orgUrlKey) return consultant; } - public Customer UpdateOrCreateCustomer(Organization org, string customerName, 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(); + ClearConsultantCache(orgUrlKey); + + return customer; + } + + public Customer FindOrCreateCustomer(Organization org, string customerName, string orgUrlKey) { var customer = _dbContext.Customer.Where(c => c.OrganizationId == org.Id) .SingleOrDefault(c => c.Name == customerName); @@ -206,14 +219,15 @@ public Customer UpdateOrCreateCustomer(Organization org, string customerName, st { Name = customerName, Organization = org, - Projects = [] + Projects = [], + IsActive = true }; _dbContext.Customer.Add(customer); + _dbContext.SaveChanges(); + ClearConsultantCache(orgUrlKey); } - _dbContext.SaveChanges(); - ClearConsultantCache(orgUrlKey); return customer; } diff --git a/backend/Api/Projects/ProjectController.cs b/backend/Api/Projects/ProjectController.cs index 92e9b369..0fa8daad 100644 --- a/backend/Api/Projects/ProjectController.cs +++ b/backend/Api/Projects/ProjectController.cs @@ -1,12 +1,14 @@ using Api.Common; using Api.StaffingController; using Core.Consultants; +using Core.Customers; using Core.DomainModels; using Core.Engagements; using Core.Organizations; using Core.Staffings; using Infrastructure.DatabaseContext; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; @@ -47,7 +49,7 @@ public async Task>> Get( var selectedOrgId = await organisationRepository.GetOrganizationByUrlKey(orgUrlKey, ct); if (selectedOrgId is null) return BadRequest(); - var absenceReadModels = new EngagementPerCustomerReadModel(-1, AbsenceCustomerName, + 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()); @@ -58,6 +60,7 @@ public async Task>> Get( new EngagementPerCustomerReadModel( a.Key.Id, a.Key.Name, + a.Key.IsActive, a.Select(e => new EngagementReadModel(e.Id, e.Name, e.State, e.IsBillable)).ToList())) .ToList(); @@ -94,6 +97,7 @@ CancellationToken ct return new CustomersWithProjectsReadModel( customer.Id, customer.Name, + customer.IsActive, customer.Projects.Where(p => p.Staffings.Any(s => s.Week.CompareTo(thisWeek) >= 0) && p.State != EngagementState.Closed).Select(e => new EngagementReadModel(e.Id, e.Name, e.State, e.IsBillable)).ToList(), @@ -244,6 +248,19 @@ 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, @@ -257,7 +274,7 @@ 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 = service.FindOrCreateCustomer(selectedOrg, body.CustomerName, orgUrlKey); var project = context.Project .Include(p => p.Customer) @@ -301,7 +318,7 @@ private CustomersWithProjectsReadModel HandleGetAbsenceWithAbsences(string orgUr { var vacation = new EngagementReadModel(-1, "Ferie", EngagementState.Absence, false); - var readModel = new CustomersWithProjectsReadModel(-1, AbsenceCustomerName + " og Ferie", + var readModel = new CustomersWithProjectsReadModel(-1, AbsenceCustomerName + " og Ferie", true, context.Absence.Where(a => a.Organization.UrlKey == orgUrlKey).Select(absence => new EngagementReadModel(absence.Id, absence.Name, EngagementState.Absence, false)).ToList(), new List()); diff --git a/backend/Api/Projects/ProjectModels.cs b/backend/Api/Projects/ProjectModels.cs index b1fefb91..5a574865 100644 --- a/backend/Api/Projects/ProjectModels.cs +++ b/backend/Api/Projects/ProjectModels.cs @@ -1,18 +1,23 @@ using System.ComponentModel.DataAnnotations; +using System.Runtime.InteropServices; +using Core.Customers; using Core.Engagements; namespace Api.Projects; public record EngagementPerCustomerReadModel( - [property: Required] int CustomerId, - [property: Required] string CustomerName, - [property: Required] List Engagements); + int CustomerId, + string CustomerName, + bool IsActive, + List Engagements); + + public record EngagementReadModel( - [property: Required] int EngagementId, - [property: Required] string EngagementName, - [property: Required] EngagementState BookingType, - [property: Required] bool IsBillable); + int EngagementId, + string EngagementName, + EngagementState BookingType, + bool IsBillable); public record EngagementWriteModel( EngagementState BookingType, @@ -43,7 +48,8 @@ public record UpdateProjectWriteModel( public record UpdateEngagementNameWriteModel(int EngagementId, string EngagementName); public record CustomersWithProjectsReadModel( - [property: Required] int CustomerId, - [property: Required] string CustomerName, - [property: Required] List ActiveEngagements, - [property: Required] List InactiveEngagements); \ No newline at end of file + int CustomerId, + string CustomerName, + bool IsActive, + List ActiveEngagements, + List InactiveEngagements); \ No newline at end of file diff --git a/backend/Core/Customers/Customer.cs b/backend/Core/Customers/Customer.cs index db7581e4..8222369a 100644 --- a/backend/Core/Customers/Customer.cs +++ b/backend/Core/Customers/Customer.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using Core.Agreements; using Core.Engagements; @@ -15,4 +16,7 @@ public class Customer public required string Name { get; set; } public required Organization Organization { get; set; } public required List Projects { get; set; } + + public bool IsActive { get; set; } = true; + } \ No newline at end of file diff --git a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs index c06bbb60..704546ca 100644 --- a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs +++ b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs @@ -61,7 +61,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(customer => customer.OrganizationId); modelBuilder.Entity() - .HasIndex(customer => new { customer.OrganizationId, customer.Name }) + .HasIndex(customer => new { customer.OrganizationId, customer.Name, customer.IsActive }) .IsUnique(); modelBuilder.Entity() @@ -211,7 +211,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) GraduationYear = 2019 }); - + base.OnModelCreating(modelBuilder); } diff --git a/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.Designer.cs b/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.Designer.cs new file mode 100644 index 00000000..7b7600a7 --- /dev/null +++ b/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.Designer.cs @@ -0,0 +1,631 @@ +// +using System; +using Infrastructure.DatabaseContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20241218133743_AddedIsActiveOnCustomerWithDefaultTrue")] + partial class AddedIsActiveOnCustomerWithDefaultTrue + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Core.Absences.Absence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ExcludeFromBillRate") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Absence"); + }); + + modelBuilder.Entity("Core.Agreements.Agreement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EngagementId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NextPriceAdjustmentDate") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("Options") + .HasColumnType("nvarchar(max)"); + + b.Property("PriceAdjustmentIndex") + .HasColumnType("nvarchar(max)"); + + b.Property("PriceAdjustmentProcess") + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("EngagementId"); + + b.ToTable("Agreements"); + }); + + modelBuilder.Entity("Core.Consultants.Competence", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Competence"); + + b.HasData( + new + { + Id = "frontend", + Name = "Frontend" + }, + new + { + Id = "backend", + Name = "Backend" + }, + new + { + Id = "design", + Name = "Design" + }, + new + { + Id = "project-mgmt", + Name = "Project Management" + }, + new + { + Id = "development", + Name = "Utvikling" + }); + }); + + modelBuilder.Entity("Core.Consultants.CompetenceConsultant", b => + { + b.Property("ConsultantId") + .HasColumnType("int"); + + b.Property("CompetencesId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("ConsultantId", "CompetencesId"); + + b.HasIndex("CompetencesId"); + + b.ToTable("CompetenceConsultant"); + }); + + modelBuilder.Entity("Core.Consultants.Consultant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Degree") + .HasColumnType("nvarchar(max)"); + + b.Property("DepartmentId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("GraduationYear") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("TransferredVacationDays") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Consultant"); + + b.HasData( + new + { + Id = 1, + Degree = "Master", + DepartmentId = "trondheim", + Email = "j@variant.no", + GraduationYear = 2019, + Name = "Jonas", + StartDate = new DateTime(2020, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Core.Customers.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("Core.Engagements.Engagement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("IsBillable") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId", "Name") + .IsUnique(); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Core.Organizations.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("Hotkey") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Department"); + + b.HasData( + new + { + Id = "trondheim", + Name = "Trondheim", + OrganizationId = "variant-as" + }); + }); + + modelBuilder.Entity("Core.Organizations.Organization", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HasVacationInChristmas") + .HasColumnType("bit"); + + b.Property("HoursPerWorkday") + .HasColumnType("float"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfVacationDaysInYear") + .HasColumnType("int"); + + b.Property("UrlKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + + b.HasData( + new + { + Id = "variant-as", + Country = "norway", + HasVacationInChristmas = true, + HoursPerWorkday = 7.5, + Name = "Variant AS", + NumberOfVacationDaysInYear = 25, + UrlKey = "variant-as" + }); + }); + + modelBuilder.Entity("Core.PlannedAbsences.PlannedAbsence", b => + { + b.Property("AbsenceId") + .HasColumnType("int"); + + b.Property("ConsultantId") + .HasColumnType("int"); + + b.Property("Week") + .HasColumnType("int"); + + b.Property("Hours") + .HasColumnType("float"); + + b.HasKey("AbsenceId", "ConsultantId", "Week"); + + b.HasIndex("ConsultantId"); + + b.ToTable("PlannedAbsence"); + }); + + modelBuilder.Entity("Core.Staffings.Staffing", b => + { + b.Property("EngagementId") + .HasColumnType("int"); + + b.Property("ConsultantId") + .HasColumnType("int"); + + b.Property("Week") + .HasColumnType("int"); + + b.Property("Hours") + .HasColumnType("float"); + + b.HasKey("EngagementId", "ConsultantId", "Week"); + + b.HasIndex("ConsultantId"); + + b.ToTable("Staffing"); + }); + + modelBuilder.Entity("Core.Vacations.Vacation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConsultantId") + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ConsultantId"); + + b.ToTable("Vacation"); + }); + + modelBuilder.Entity("Core.Absences.Absence", b => + { + b.HasOne("Core.Organizations.Organization", "Organization") + .WithMany("AbsenceTypes") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Core.Agreements.Agreement", b => + { + b.HasOne("Core.Customers.Customer", "Customer") + .WithMany("Agreements") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Core.Engagements.Engagement", "Engagement") + .WithMany("Agreements") + .HasForeignKey("EngagementId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsMany("Core.Agreements.FileReference", "Files", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id")); + + b1.Property("AgreementId") + .HasColumnType("int"); + + b1.Property("BlobName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("FileName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("UploadedBy") + .HasColumnType("nvarchar(max)"); + + b1.Property("UploadedOn") + .HasColumnType("datetime2"); + + b1.HasKey("Id"); + + b1.HasIndex("AgreementId"); + + b1.ToTable("FileReference"); + + b1.WithOwner() + .HasForeignKey("AgreementId"); + }); + + b.Navigation("Customer"); + + b.Navigation("Engagement"); + + b.Navigation("Files"); + }); + + modelBuilder.Entity("Core.Consultants.CompetenceConsultant", b => + { + b.HasOne("Core.Consultants.Competence", "Competence") + .WithMany("CompetenceConsultant") + .HasForeignKey("CompetencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Consultants.Consultant", "Consultant") + .WithMany("CompetenceConsultant") + .HasForeignKey("ConsultantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Competence"); + + b.Navigation("Consultant"); + }); + + modelBuilder.Entity("Core.Consultants.Consultant", b => + { + b.HasOne("Core.Organizations.Department", "Department") + .WithMany("Consultants") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Core.Customers.Customer", b => + { + b.HasOne("Core.Organizations.Organization", "Organization") + .WithMany("Customers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Core.Engagements.Engagement", b => + { + b.HasOne("Core.Customers.Customer", "Customer") + .WithMany("Projects") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Core.Organizations.Department", b => + { + b.HasOne("Core.Organizations.Organization", "Organization") + .WithMany("Departments") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Core.PlannedAbsences.PlannedAbsence", b => + { + b.HasOne("Core.Absences.Absence", "Absence") + .WithMany() + .HasForeignKey("AbsenceId") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + + b.HasOne("Core.Consultants.Consultant", "Consultant") + .WithMany("PlannedAbsences") + .HasForeignKey("ConsultantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Absence"); + + b.Navigation("Consultant"); + }); + + modelBuilder.Entity("Core.Staffings.Staffing", b => + { + b.HasOne("Core.Consultants.Consultant", "Consultant") + .WithMany("Staffings") + .HasForeignKey("ConsultantId") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + + b.HasOne("Core.Engagements.Engagement", "Engagement") + .WithMany("Staffings") + .HasForeignKey("EngagementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Consultant"); + + b.Navigation("Engagement"); + }); + + modelBuilder.Entity("Core.Vacations.Vacation", b => + { + b.HasOne("Core.Consultants.Consultant", "Consultant") + .WithMany("Vacations") + .HasForeignKey("ConsultantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Consultant"); + }); + + modelBuilder.Entity("Core.Consultants.Competence", b => + { + b.Navigation("CompetenceConsultant"); + }); + + modelBuilder.Entity("Core.Consultants.Consultant", b => + { + b.Navigation("CompetenceConsultant"); + + b.Navigation("PlannedAbsences"); + + b.Navigation("Staffings"); + + b.Navigation("Vacations"); + }); + + modelBuilder.Entity("Core.Customers.Customer", b => + { + b.Navigation("Agreements"); + + b.Navigation("Projects"); + }); + + modelBuilder.Entity("Core.Engagements.Engagement", b => + { + b.Navigation("Agreements"); + + b.Navigation("Staffings"); + }); + + modelBuilder.Entity("Core.Organizations.Department", b => + { + b.Navigation("Consultants"); + }); + + modelBuilder.Entity("Core.Organizations.Organization", b => + { + b.Navigation("AbsenceTypes"); + + b.Navigation("Customers"); + + b.Navigation("Departments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.cs b/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.cs new file mode 100644 index 00000000..c3702566 --- /dev/null +++ b/backend/Infrastructure/Migrations/20241218133743_AddedIsActiveOnCustomerWithDefaultTrue.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class AddedIsActiveOnCustomerWithDefaultTrue : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + table: "Customer", + type: "bit", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsActive", + table: "Customer"); + } + } +} diff --git a/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs b/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs index b4fc0430..dc6e8177 100644 --- a/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs +++ b/backend/Infrastructure/Migrations/ApplicationContextModelSnapshot.cs @@ -215,6 +215,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("IsActive") + .HasColumnType("bit"); + b.Property("Name") .IsRequired() .HasColumnType("nvarchar(450)"); diff --git a/frontend/mockdata/mockData.ts b/frontend/mockdata/mockData.ts index b253eacb..51dc3aef 100644 --- a/frontend/mockdata/mockData.ts +++ b/frontend/mockdata/mockData.ts @@ -63,6 +63,7 @@ export const MockEngagements: EngagementPerCustomerReadModel[] = [ customerId: 1, customerName: "TestCustomer", engagements: [], + isActive: false, }, ]; diff --git a/frontend/src/api-types.ts b/frontend/src/api-types.ts index 0f02d598..968e9cb5 100644 --- a/frontend/src/api-types.ts +++ b/frontend/src/api-types.ts @@ -129,6 +129,7 @@ export interface EngagementPerCustomerReadModel { /** @minLength 1 */ customerName: string; engagements: EngagementReadModel[]; + isActive: boolean; } export interface EngagementReadModel { @@ -291,6 +292,7 @@ export interface WeeklyHours { export interface CustomersWithProjectsReadModel { customerId: number; customerName: string; + isActive: boolean; activeEngagements: EngagementReadModel[]; inactiveEngagements: EngagementReadModel[]; } diff --git a/frontend/src/components/CostumerTable/CustomerRow.tsx b/frontend/src/components/CostumerTable/CustomerRow.tsx index dc21a6f4..70387386 100644 --- a/frontend/src/components/CostumerTable/CustomerRow.tsx +++ b/frontend/src/components/CostumerTable/CustomerRow.tsx @@ -1,7 +1,7 @@ "use client"; import { EngagementPerCustomerReadModel } from "@/api-types"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { ArrowRight, ChevronDown } from "react-feather"; import { getColorByStaffingType, @@ -9,6 +9,8 @@ import { } from "../Staffing/helpers/utils"; import { getBookingTypeFromProjectState } from "../Staffing/EditEngagementHourModal/utils"; import Link from "next/link"; +import ActionButton from "../Buttons/ActionButton"; +import { useParams } from "next/navigation"; export default function CostumerRow({ customer, @@ -17,11 +19,34 @@ export default function CostumerRow({ }) { const [isListElementVisible, setIsListElementVisible] = useState(false); const [isRowHovered, setIsRowHovered] = useState(false); + const [isActive, setIsActive] = useState(customer.isActive); + const [isLoading, setIsLoading] = useState(false); + const { organisation } = useParams(); function toggleListElementVisibility() { setIsListElementVisible((old) => !old); } + async function onActivate(customerId: number, active: boolean) { + if (isLoading) return; + setIsLoading(true); + setIsActive(active); + try { + const response = await fetch( + `/${organisation}/kunder/api?customerId=${customerId}&activate=${active}`, + { + method: "PUT", + }, + ); + if (response.status !== 200) { + setIsActive(!active); + } + } catch { + setIsActive(!active); + } finally { + setIsLoading(false); + } + } return ( <> ))} + + onActivate(customer.customerId, !customer.isActive)} + variant="secondary" + > + {isActive ? "Deaktiver" : "Aktiver"} + + {isListElementVisible && customer.engagements && diff --git a/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx b/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx index 3c3b63d1..e241ec60 100644 --- a/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx +++ b/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx @@ -8,7 +8,10 @@ export default function CustomerSidebarWithFilters() {

Filter

- + ); diff --git a/frontend/src/components/CostumerTable/CustomerTable.tsx b/frontend/src/components/CostumerTable/CustomerTable.tsx index ac9978c3..fdfc0253 100644 --- a/frontend/src/components/CostumerTable/CustomerTable.tsx +++ b/frontend/src/components/CostumerTable/CustomerTable.tsx @@ -9,6 +9,7 @@ import WeekSelector from "../WeekSelector"; import { useWeekSelectors } from "@/hooks/useWeekSelectors"; import { WeekSpanTableHead } from "../Staffing/WeekTableHead"; import { AgreementEdit } from "../Agreement/AgreementEdit"; +import ActionButton from "../Buttons/ActionButton"; export default function CustomerTable({ customer, @@ -34,7 +35,10 @@ export default function CustomerTable({ return (
-

{customer?.customerName}

+
+

{customer?.customerName}

+ Deaktiver +
diff --git a/frontend/src/components/SearchBarComponent.tsx b/frontend/src/components/SearchBarComponent.tsx index 5a5eed4f..c37593ae 100644 --- a/frontend/src/components/SearchBarComponent.tsx +++ b/frontend/src/components/SearchBarComponent.tsx @@ -7,9 +7,11 @@ import { FilteredContext } from "@/hooks/ConsultantFilterProvider"; export default function SearchBarComponent({ hidden = false, context, + placeholder, }: { hidden?: boolean; context: Context; + placeholder: string; }) { const { setNameSearch, activeNameSearch } = useNameSearch(context); const inputRef = useRef(null); @@ -60,7 +62,7 @@ export default function SearchBarComponent({ setNameSearch(e.target.value)} diff --git a/frontend/src/components/Staffing/WeekCell.tsx b/frontend/src/components/Staffing/WeekCell.tsx index e3db5d5a..572f70e3 100644 --- a/frontend/src/components/Staffing/WeekCell.tsx +++ b/frontend/src/components/Staffing/WeekCell.tsx @@ -63,7 +63,7 @@ export function WeekCell(props: {
0 - ? `bg-black text-white` + ? `bg-overbooked text-white` : bookedHoursPerWeek.bookingModel.totalSellableTime > 0 ? `bg-available/50` : `bg-primary/[3%]` diff --git a/frontend/src/components/StaffingSidebar.tsx b/frontend/src/components/StaffingSidebar.tsx index f43c4f2f..2bd314c1 100644 --- a/frontend/src/components/StaffingSidebar.tsx +++ b/frontend/src/components/StaffingSidebar.tsx @@ -31,7 +31,10 @@ export default function StaffingSidebar({
- + {isStaffing ? : null} @@ -42,6 +45,7 @@ export default function StaffingSidebar({ {!isSidebarOpen && (