Skip to content

Commit

Permalink
298 set staffing hours (#302)
Browse files Browse the repository at this point in the history
Co-authored-by: Mathilde Haukø Haugum <[email protected]>
Co-authored-by: Mathilde Haukø Haugum <[email protected]>
  • Loading branch information
3 people authored Nov 23, 2023
1 parent 15f381d commit 3e7f2d9
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 56 deletions.
105 changes: 102 additions & 3 deletions backend/Api/Common/StorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ public StorageService(IMemoryCache cache, ApplicationContext context)

public List<Consultant> LoadConsultants(string orgUrlKey)
{
if (_cache.TryGetValue<List<Consultant>>(ConsultantCacheKey, out var consultants))
if (_cache.TryGetValue<List<Consultant>>($"{ConsultantCacheKey}/{orgUrlKey}", out var consultants))
if (consultants != null)
return consultants;

var loadedConsultants = LoadConsultantsFromDb(orgUrlKey);
_cache.Set(ConsultantCacheKey, loadedConsultants);
_cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", loadedConsultants);
return loadedConsultants;
}

private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)
{
var consultantList = _dbContext.Consultant
Expand Down Expand Up @@ -74,4 +74,103 @@ private List<Consultant> LoadConsultantsFromDb(string orgUrlKey)

return hydratedConsultants;
}

public void UpdateStaffing(int id, double hours)
{
var staffing = _dbContext.Staffing
.Include(s=>s.Project)
.ThenInclude(p=> p.Customer)
.ThenInclude(c=> c.Organization)
.Include(s=>s.Consultant)
.FirstOrDefault(staffing => staffing.Id == id);
if (staffing is null) return;
var orgUrlKey = staffing.Project.Customer.Organization.UrlKey;
staffing.Hours = hours;
_dbContext.SaveChanges();
var consultantId = staffing.Consultant.Id;
var consultants = LoadConsultants(orgUrlKey);
consultants
.Single(c => c.Id == consultantId)
.Staffings
.Single(s => s.Id == id)
.Hours = hours;
_cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", consultants);
}

public void UpdateAbsence(int id, double hours)
{
var absence = _dbContext.PlannedAbsence
.Include(pa => pa.Absence)
.ThenInclude(a => a.Organization)
.Include(pa => pa.Consultant)
.FirstOrDefault(absence => absence.Id == id);
if (absence is null) return;
var orgUrlKey = absence.Absence.Organization.UrlKey;
absence.Hours = hours;
_dbContext.SaveChanges();
var consultantId = absence.Consultant.Id;
var consultants = LoadConsultants(orgUrlKey);
consultants
.Single(c => c.Id == consultantId)
.PlannedAbsences
.Single(pa => pa.Id == id)
.Hours = hours;
_cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", consultants);
}

public int CreateStaffing(int consultantId, int projectId, double hours, Week week)
{
var consultant = _dbContext.Consultant.Find(consultantId);
var project = _dbContext.Project
.Include(p=> p.Customer)
.ThenInclude(c=>c.Organization)
.FirstOrDefault(project => project.Id == projectId);
if (consultant is null || project is null) return 0; //Feilmelding?
var staffing = new Core.DomainModels.Staffing
{
Project = project,
Consultant = consultant,
Hours = hours,
Week = week
};
consultant.Staffings.Add(staffing);
_dbContext.SaveChanges();
var orgUrlKey = project.Customer.Organization.UrlKey;
var consultants = LoadConsultants(orgUrlKey);

if (!consultants.Single(c => c.Id == consultantId).Staffings.Contains(staffing))
{
consultants.Single(c=>c.Id == consultantId).Staffings.Add(staffing);
};
_cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", consultants);
return staffing.Id;
}

public int CreateAbsence(int consultantId, int absenceId, double hours, Week week)
{
var consultant = _dbContext.Consultant.Find(consultantId);
var absence = _dbContext.Absence
.Include(a=> a.Organization)
.FirstOrDefault(absence => absence.Id == absenceId);
if (consultant is null || absence is null) return 0; //Feilmelding?
var plannedAbsence = new PlannedAbsence
{
Absence = absence,
Consultant = consultant,
Hours = hours,
Week = week
};
consultant.PlannedAbsences.Add(plannedAbsence);
_dbContext.SaveChanges();
var orgUrlKey = absence.Organization.UrlKey;
var consultants = LoadConsultants(orgUrlKey);

if (!consultants.Single(c => c.Id == consultantId).PlannedAbsences.Contains(plannedAbsence))
{
consultants.Single(c=>c.Id == consultantId).PlannedAbsences.Add(plannedAbsence);
};
_cache.Set($"{ConsultantCacheKey}/{orgUrlKey}", consultants);
return plannedAbsence.Id;
}

}
65 changes: 65 additions & 0 deletions backend/Api/Staffing/ConsultantController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,69 @@ public ActionResult<List<ConsultantReadModel>> Get(
var readModels = new ReadModelFactory(service).GetConsultantReadModelsForWeeks(orgUrlKey, weekSet);
return Ok(readModels);
}

[HttpPut]
[Route("staffing/{staffingId}")]
public ActionResult<List<ConsultantReadModel>>Put(
[FromRoute] string orgUrlKey,
[FromRoute] int staffingId,
[FromQuery(Name = "Type")] BookingType bookingType,
[FromQuery(Name = "Hours")] double hours = 0
)
{
var service = new StorageService(_cache, _context);

switch (bookingType)
{
case BookingType.Booking:
case BookingType.Offer:
service.UpdateStaffing(staffingId, hours);
break;
case BookingType.PlannedAbsence:
service.UpdateAbsence(staffingId, hours);
break;
case BookingType.Vacation:
break;
default:
throw new ArgumentOutOfRangeException(nameof(bookingType), bookingType, "Invalid bookingType");
}

return Ok(hours);
}

[HttpPost]
[Route("staffing/new")]
public ActionResult<List<ConsultantReadModel>>Post(
[FromRoute] string orgUrlKey,
[FromQuery(Name = "Type")] BookingType bookingType,
[FromQuery(Name = "ConsultantID")] int consultantId,
[FromQuery(Name = "EngagementID")] int engagementId,
[FromQuery(Name = "Year")] int selectedYearParam,
[FromQuery(Name = "Week")] int selectedWeekParam,
[FromQuery(Name = "Hours")] double hours = 0
)
{
var service = new StorageService(_cache, _context);

var newId = 0;

var selectedWeek = new Week((int)selectedYearParam, (int)selectedWeekParam);

switch (bookingType)
{
case BookingType.Booking:
case BookingType.Offer:
newId = service.CreateStaffing(consultantId, engagementId, hours, selectedWeek );
break;
case BookingType.PlannedAbsence:
newId = service.CreateAbsence(consultantId, engagementId, hours, selectedWeek );
break;
case BookingType.Vacation:
break;
default:
throw new ArgumentOutOfRangeException(nameof(bookingType), bookingType, "Invalid bookingType");
}

return Ok(newId);
}
}
8 changes: 4 additions & 4 deletions backend/Api/Staffing/ConsultantReadModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public record BookedHoursPerWeek(int Year, int WeekNumber, int SortableWeek, str

public record DetailedBooking(BookingDetails BookingDetails, List<WeeklyHours> Hours)
{
public DetailedBooking(string projectName, BookingType type, string customerName, List<WeeklyHours> bookings) : this(
new BookingDetails(projectName, type, customerName), bookings)
public DetailedBooking(string projectName, BookingType type, string customerName, int projectId, List<WeeklyHours> bookings) : this(
new BookingDetails(projectName, type, customerName, projectId), bookings)
{
}

Expand All @@ -54,9 +54,9 @@ public record WeeklyBookingReadModel(double TotalBillable, double TotalOffered,

public record BookingReadModel(string Name, double Hours, BookingType Type);

public record BookingDetails(string ProjectName, BookingType Type, string CustomerName);
public record BookingDetails(string ProjectName, BookingType Type, string CustomerName, int ProjectId);

public record WeeklyHours(int Week, double Hours);
public record WeeklyHours(int Id, int Week, double Hours);

public enum BookingType
{
Expand Down
53 changes: 40 additions & 13 deletions backend/Api/Staffing/ReadModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,39 +69,65 @@ private static List<DetailedBooking> DetailedBookings(Consultant consultant,
.Where(staffing => weekSet.Contains(staffing.Week))
.GroupBy(staffing => staffing.Project.Name)
.Select(grouping => new DetailedBooking(
new BookingDetails(grouping.Key, BookingType.Booking, grouping.First().Project.Customer.Name),
weekSet.Select(week => new WeeklyHours(
week.ToSortableInt(), grouping
.Where(staffing => staffing.Week.Equals(week))
.Sum(staffing => staffing.Hours))).ToList()
new BookingDetails(grouping.Key, BookingType.Booking, grouping.First().Project.Customer.Name, grouping.First().Project.Id),
weekSet.Select(week =>
{
int? id = null;
if (grouping.Where(staffing => staffing.Week.Equals(week)).ToArray().Length == 0)
{
id = 0;};
return new WeeklyHours(
id ?? grouping
.First(staffing => staffing.Week.Equals(week)).Id,
week.ToSortableInt(), grouping
.Where(staffing => staffing.Week.Equals(week))
.Sum(staffing => staffing.Hours));
}).ToList()

));

var offeredBookings = consultant.Staffings
.Where(staffing => staffing.Project.State == ProjectState.Offer)
.Where(staffing => weekSet.Contains(staffing.Week))
.GroupBy(staffing => staffing.Project.Name)
.Select(grouping => new DetailedBooking(
new BookingDetails(grouping.Key, BookingType.Offer, grouping.First().Project.Customer.Name),
weekSet.Select(week => new WeeklyHours(
week.ToSortableInt(), grouping
new BookingDetails(grouping.Key, BookingType.Offer, grouping.First().Project.Customer.Name, grouping.First().Project.Id),
weekSet.Select(week =>
{
int? id = null;
if (grouping.Where(staffing => staffing.Week.Equals(week)).ToArray().Length == 0)
{
id = 0;};
return new WeeklyHours(
id ?? grouping.First(staffing => staffing.Week.Equals(week)).Id,
week.ToSortableInt(),
grouping
.Where(staffing =>
staffing.Week.Equals(week))
.Sum(staffing => staffing.Hours))).ToList()
.Sum(staffing => staffing.Hours));
}).ToList()
));

var plannedAbsencesPrWeek = consultant.PlannedAbsences
.Where(absence => weekSet.Contains(absence.Week))
.GroupBy(absence => absence.Absence.Name)
.Select(grouping => new DetailedBooking(
new BookingDetails("", BookingType.PlannedAbsence,
grouping.Key), //Empty projectName as PlannedAbsence does not have a project
weekSet.Select(week => new WeeklyHours(
grouping.Key, grouping.First().Absence.Id), //Empty projectName as PlannedAbsence does not have a project
weekSet.Select(week =>
{
int? id = null;
if (grouping.Where(absence => absence.Week.Equals(week)).ToArray().Length == 0) { id = 0; }
return new WeeklyHours(
id ?? grouping.First(absence =>
absence.Week.Equals(week)).Id,
week.ToSortableInt(),
grouping
.Where(absence =>
absence.Week.Equals(week))
.Sum(absence => absence.Hours)
)).ToList()
);
}).ToList()
));


Expand All @@ -114,13 +140,14 @@ private static List<DetailedBooking> DetailedBookings(Consultant consultant,
if (vacationsInSet.Count > 0)
{
var vacationsPrWeek = weekSet.Select(week => new WeeklyHours(
1,
week.ToSortableInt(),
vacationsInSet.Count(vacation => week.ContainsDate(vacation.Date)) *
consultant.Department.Organization.HoursPerWorkday
)).ToList();
detailedBookings = detailedBookings.Append(new DetailedBooking(
new BookingDetails("", BookingType.Vacation,
"Ferie"), //Empty projectName as vacation does not have a project
"Ferie", 0), //Empty projectName as vacation does not have a project, 0 as projectId as vacation is weird
vacationsPrWeek));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchWithToken } from "@/data/fetchWithToken";
import { fetchWithToken } from "@/data/apiCallsWithToken";
import { Department } from "@/types";
import { NextResponse } from "next/server";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { putWithToken } from "@/data/apiCallsWithToken";
import { NextResponse } from "next/server";

export async function PUT(
request: Request,
{ params }: { params: { organisation: string; staffingID: string } },
) {
const orgUrlKey = params.organisation;
const staffingID = params.staffingID;
const { searchParams } = new URL(request.url);
const hours = searchParams.get("hours") || "";
const bookingType = searchParams.get("bookingType") || "";

const staffing =
(await putWithToken(
`${orgUrlKey}/consultants/staffing/${staffingID}?Hours=${hours}&Type=${bookingType}`,
)) ?? [];

return NextResponse.json(staffing);
}
29 changes: 29 additions & 0 deletions frontend/src/app/[organisation]/bemanning/api/updateHours/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { postWithToken } from "@/data/apiCallsWithToken";
import { parseYearWeekFromString } from "@/data/urlUtils";
import { NextResponse } from "next/server";

export async function POST(
request: Request,
{ params }: { params: { organisation: string } },
) {
const orgUrlKey = params.organisation;
const { searchParams } = new URL(request.url);
const consultantID = searchParams.get("consultantID") || "";
const engagementID = searchParams.get("engagementID") || "";
const hours = searchParams.get("hours") || "";
const bookingType = searchParams.get("bookingType") || "";
const selectedWeek = parseYearWeekFromString(
searchParams.get("selectedWeek") || undefined,
);

const staffing =
(await postWithToken(
`${orgUrlKey}/consultants/staffing/new?Hours=${hours}&Type=${bookingType}&ConsultantID=${consultantID}&EngagementID=${engagementID}&${
selectedWeek
? `Year=${selectedWeek.year}&Week=${selectedWeek.weekNumber}`
: ""
}`,
)) ?? [];

return NextResponse.json(staffing);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchWithToken } from "@/data/fetchWithToken";
import { fetchWithToken } from "@/data/apiCallsWithToken";
import { NextResponse } from "next/server";

export async function GET(
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/app/[organisation]/bemanning/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import StaffingSidebar from "@/components/StaffingSidebar";
import FilteredConsultantsList from "@/components/FilteredConsultantsList";
import { fetchWithToken } from "@/data/fetchWithToken";
import { fetchWithToken } from "@/data/apiCallsWithToken";
import { Consultant, Department } from "@/types";
import { ConsultantFilterProvider } from "@/components/FilteredConsultantProvider";
import { stringToWeek } from "@/data/urlUtils";
import { ConsultantFilterProvider } from "@/hooks/ConsultantFilterProvider";
import { parseYearWeekFromUrlString } from "@/data/urlUtils";
import InfoPillDescriptions from "@/components/InfoPillDescriptions";

export default async function Bemanning({
Expand All @@ -13,7 +13,9 @@ export default async function Bemanning({
params: { organisation: string };
searchParams: { selectedWeek?: string; weekSpan?: string };
}) {
const selectedWeek = stringToWeek(searchParams.selectedWeek || undefined);
const selectedWeek = parseYearWeekFromUrlString(
searchParams.selectedWeek || undefined,
);
const weekSpan = searchParams.weekSpan || undefined;

const consultants =
Expand Down
Loading

0 comments on commit 3e7f2d9

Please sign in to comment.