diff --git a/backend/Api/Common/StorageService.cs b/backend/Api/Common/StorageService.cs index d5d9f3e3..34457903 100644 --- a/backend/Api/Common/StorageService.cs +++ b/backend/Api/Common/StorageService.cs @@ -46,6 +46,8 @@ public Consultant LoadConsultantForSingleWeek(int consultantId, Week week) var consultant = _dbContext.Consultant .Include(c => c.Department) .ThenInclude(d => d.Organization) + .Include(c => c.Projects) + .ThenInclude(p => p.Agreements) .Single(c => c.Id == consultantId); consultant.Staffings = _dbContext.Staffing.Where(staffing => @@ -66,6 +68,8 @@ public Consultant LoadConsultantForWeekSet(int consultantId, List weeks) var consultant = _dbContext.Consultant .Include(c => c.Department) .ThenInclude(d => d.Organization) + .Include(c => c.Projects) + .ThenInclude(p => p.Agreements) .Single(c => c.Id == consultantId); @@ -96,6 +100,8 @@ private List LoadConsultantsFromDb(string orgUrlKey) .ThenInclude(department => department.Organization) .Include(c => c.CompetenceConsultant) .ThenInclude(cc => cc.Competence) + .Include(c => c.Projects) + .ThenInclude(p => p.Agreements) .Where(consultant => consultant.Department.Organization.UrlKey == orgUrlKey) .OrderBy(consultant => consultant.Name) .ToList(); diff --git a/backend/Api/StaffingController/ReadModelFactory.cs b/backend/Api/StaffingController/ReadModelFactory.cs index 252f63c7..0da87f4d 100644 --- a/backend/Api/StaffingController/ReadModelFactory.cs +++ b/backend/Api/StaffingController/ReadModelFactory.cs @@ -93,6 +93,30 @@ private static List DetailedBookings(Consultant consultant, { weekSet.Sort(); + var projectsStatuses = consultant.Projects.Select(p => { + // Get the endDate furthest away for the engagement + var endDate = p.Agreements + .Where(a => a.EngagementId == p.Id) + .Select(a => a?.EndDate) + .DefaultIfEmpty((DateTime?)null) + .Max(); + + var dateToday = DateOnly.FromDateTime(DateTime.Today); + var status = endDate.HasValue + ? endDate < dateToday.ToDateTime(TimeOnly.MinValue) + ? AgreementStatus.Expired + : AgreementStatus.Active + : AgreementStatus.None; + + return new { + p.Id, + status + }; + }).ToList(); + + + + // var billableProjects = UniqueWorkTypes(projects, billableStaffing); var billableBookings = consultant.Staffings .Where(staffing => staffing.Engagement.State == EngagementState.Order) @@ -101,7 +125,7 @@ private static List DetailedBookings(Consultant consultant, .Select(grouping => new DetailedBooking( new BookingDetails(grouping.First().Engagement.Name, BookingType.Booking, grouping.First().Engagement.Customer.Name, - grouping.Key, false, grouping.First().Engagement.IsBillable), + grouping.Key, false, grouping.First().Engagement.IsBillable, projectsStatuses.First(p => p.Id == grouping.Key).status), weekSet.Select(week => new WeeklyHours( week.ToSortableInt(), grouping @@ -117,7 +141,7 @@ private static List DetailedBookings(Consultant consultant, .Select(grouping => new DetailedBooking( new BookingDetails(grouping.First().Engagement.Name, BookingType.Offer, grouping.First().Engagement.Customer.Name, - grouping.Key, false, grouping.First().Engagement.IsBillable), + grouping.Key, false, grouping.First().Engagement.IsBillable, projectsStatuses.First(p => p.Id == grouping.Key).status), weekSet.Select(week => new WeeklyHours( week.ToSortableInt(), diff --git a/backend/Api/StaffingController/StaffingReadModel.cs b/backend/Api/StaffingController/StaffingReadModel.cs index 94f5d282..45d0caa0 100644 --- a/backend/Api/StaffingController/StaffingReadModel.cs +++ b/backend/Api/StaffingController/StaffingReadModel.cs @@ -98,7 +98,15 @@ public record BookingDetails( [property: Required] string CustomerName, [property: Required] int ProjectId, [property: Required] bool ExcludeFromBilling = false, - [property: Required] bool IsBillable = false); + [property: Required] bool IsBillable = false, + [property: Required] AgreementStatus AgreementStatus = AgreementStatus.None); + +public enum AgreementStatus +{ + Active, + Expired, + None +} public record WeeklyHours([property: Required] int Week, [property: Required] double Hours); diff --git a/backend/Tests/AbsenceTest.cs b/backend/Tests/AbsenceTest.cs index b04d1f68..9cff3d87 100644 --- a/backend/Tests/AbsenceTest.cs +++ b/backend/Tests/AbsenceTest.cs @@ -1,5 +1,6 @@ using Api.StaffingController; using Core.Absences; +using Core.Agreements; using Core.Consultants; using Core.Customers; using Core.DomainModels; @@ -56,6 +57,26 @@ public void AvailabilityCalculation( Consultants = Substitute.For>() }; + var customer = new Customer + { + Id = 1, + Name = "TestCustomer", + Organization = org, + Projects = new List() + }; + + var engagement = new Engagement + { + Id = 1, + Customer = customer, + State = EngagementState.Order, + IsBillable = true, + Staffings = new List(), + Name = "TestProject", + + }; + + customer.Projects.Add(engagement); Consultant consultant = new() { @@ -63,9 +84,12 @@ public void AvailabilityCalculation( Name = "Test Variant", Email = "tv@v.no", GraduationYear = 2010, - Department = department + Department = department, + Projects = new List() }; + consultant.Projects.Add(engagement); + var mondayDateOnly = numberOfHolidays switch { 0 => new DateOnly(2023, 9, 4), // Week 36, 4th Sept 2023, (no public holidays) @@ -76,12 +100,17 @@ public void AvailabilityCalculation( }; var week = Week.FromDateOnly(mondayDateOnly); - var project = Substitute.For(); - var customer = Substitute.For(); - customer.Name = "TestCustomer"; - project.Customer = customer; - project.State = EngagementState.Order; - project.IsBillable = true; + + + + Agreement agreement = new() + { + Id = 1, + EndDate = new DateTime(2023, 12, 31), + StartDate = new DateTime(2023, 1, 1), + EngagementId = 1, + Engagement = engagement, + }; // TODO: Change this to update consultant data @@ -108,11 +137,11 @@ public void AvailabilityCalculation( if (staffedHours > 0) consultant.Staffings.Add(new Staffing { - Engagement = project, + Engagement = engagement, Consultant = consultant, Hours = staffedHours, Week = week, - EngagementId = project.Id, + EngagementId = engagement.Id, ConsultantId = consultant.Id }); @@ -162,9 +191,12 @@ public void MultiplePlannedAbsences() Name = "Test Variant", Email = "tv@v.no", GraduationYear = 2010, - Department = department + Department = department, + Projects = new List() }; + consultant.Projects.Add(Substitute.For()); + var week = new Week(2000, 1); consultant.PlannedAbsences.Add(new PlannedAbsence diff --git a/frontend/src/api-types.ts b/frontend/src/api-types.ts index 77086d9f..b468bce1 100644 --- a/frontend/src/api-types.ts +++ b/frontend/src/api-types.ts @@ -30,6 +30,13 @@ export interface BookingDetails { /** @format int32 */ projectId: number; isBillable: boolean; + agreementStatus?: string; +} + +export enum AgreementStatus { + Active, + Expired, + None, } export enum BookingType { diff --git a/frontend/src/components/Staffing/ConsultantRow.tsx b/frontend/src/components/Staffing/ConsultantRow.tsx index 54001e6e..6ad0993a 100644 --- a/frontend/src/components/Staffing/ConsultantRow.tsx +++ b/frontend/src/components/Staffing/ConsultantRow.tsx @@ -1,7 +1,11 @@ "use client"; -import { ConsultantReadModel, ProjectWithCustomerModel } from "@/api-types"; +import { + AgreementStatus, + ConsultantReadModel, + ProjectWithCustomerModel, +} from "@/api-types"; import React, { useContext, useEffect, useState } from "react"; -import { ChevronDown, Plus } from "react-feather"; +import { AlertCircle, ChevronDown, Plus } from "react-feather"; import { DetailedBookingRows } from "@/components/Staffing/DetailedBookingRows"; import { WeekCell } from "@/components/Staffing/WeekCell"; import { useModal } from "@/hooks/useModal"; @@ -78,6 +82,32 @@ export default function ConsultantRows({ const organisationUrl = usePathname().split("/")[1]; + function getStatusConsultant(consultant: ConsultantReadModel) { + const statuses = consultant.detailedBooking.map((e) => + stringToAgreementStatus(e.bookingDetails.agreementStatus), + ); + + if (statuses) { + if (statuses.includes(AgreementStatus.None)) { + return "red"; + } else if (statuses.includes(AgreementStatus.Expired)) { + return "orange"; + } else { + return null; + } + } + } + + function stringToAgreementStatus(status: string | undefined) { + if (status === "Active") { + return AgreementStatus.Active; + } else if (status === "Expired") { + return AgreementStatus.Expired; + } else { + return AgreementStatus.None; + } + } + useEffect(() => { async function fetchProject() { const url = `/${organisationUrl}/bemanning/api/projects?projectId=${selectedProjectId}`; @@ -181,7 +211,14 @@ export default function ConsultantRows({ -
+
+ {getStatusConsultant(currentConsultant)! && ( + + )} +
{consultant.imageThumbUrl ? ( void; numWorkHours: number; }) { - const colors: { color: string; text: string; icon: Icon }[] = [ - { color: "green", text: "Avtale aktiv", icon: CheckCircle }, - { color: "red", text: "Ingen avtaler funnet", icon: AlertCircle }, - { color: "orange", text: "Avtale utgått", icon: AlertCircle }, + const colors: { + color: string; + text: string; + icon: Icon; + status: AgreementStatus; + }[] = [ + { + color: "green", + text: "Avtale aktiv", + icon: CheckCircle, + status: AgreementStatus.Active, + }, + { + color: "red", + text: "Ingen avtaler funnet", + icon: AlertCircle, + status: AgreementStatus.None, + }, + { + color: "orange", + text: "Avtale utgått", + icon: AlertCircle, + status: AgreementStatus.Expired, + }, ]; const { setConsultants } = useContext(FilteredContext); @@ -112,22 +132,10 @@ export function DetailedBookingRows(props: { }, []); async function getColorIcon() { - const agreements = await getAgreementsForProject( - detailedBooking.bookingDetails.projectId, - organisationName, - ); - if (agreements) { - const endDate = Math.max( - ...agreements.map((p) => new Date(p.endDate).getTime()), - ); - const today = new Date().getTime(); - if (today > endDate) { - return setAlertColor(colors.find((c) => c.color == "orange")); - } else { - return setAlertColor(colors.find((c) => c.color == "green")); - } - } else { - return setAlertColor(colors.find((c) => c.color == "red")); + const status = detailedBooking.bookingDetails?.agreementStatus; + if (status) { + const color = colors.find((c) => AgreementStatus[c.status] === status); + setAlertColor(color); } } @@ -138,7 +146,7 @@ export function DetailedBookingRows(props: { >
- {alert ? : } + {alert && }
{alert?.text}