Skip to content

Commit

Permalink
Forecast table with mock data (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
MariaBonde authored Jan 7, 2025
1 parent f554418 commit bd7b2f4
Show file tree
Hide file tree
Showing 6 changed files with 695 additions and 0 deletions.
57 changes: 57 additions & 0 deletions frontend/mockdata/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Degree,
DepartmentReadModel,
EngagementPerCustomerReadModel,
Forecast,
OrganisationReadModel,
WeeklyBookingReadModel,
} from "@/api-types";
Expand All @@ -19,6 +20,54 @@ const MockWeeklyBookingReadModel: WeeklyBookingReadModel = {
totalExcludableAbsence: 0,
totalNotStartedOrQuit: 0,
};
const mockForecast1: Forecast = {
id: 1,
month: 0,
year: 2025,
forecastValue: 80,
hasBeenChanged: false,
valueAddedManually: 0,
};
const mockForecast2: Forecast = {
id: 2,
month: 1,
year: 2025,
forecastValue: 80,
hasBeenChanged: false,
valueAddedManually: 0,
};
const mockForecast3: Forecast = {
id: 3,
month: 2,
year: 2025,
forecastValue: 50,
hasBeenChanged: false,
valueAddedManually: 0,
};
const mockForecast4: Forecast = {
id: 4,
month: 3,
year: 2025,
forecastValue: 100,
hasBeenChanged: false,
valueAddedManually: 0,
};
const mockForecast5: Forecast = {
id: 5,
month: 4,
year: 2025,
forecastValue: 0,
hasBeenChanged: true,
valueAddedManually: 50,
};
const mockForecast6: Forecast = {
id: 6,
month: 5,
year: 2025,
forecastValue: 70,
hasBeenChanged: true,
valueAddedManually: 10,
};

export const MockConsultants: ConsultantReadModel[] = [
{
Expand All @@ -41,6 +90,14 @@ export const MockConsultants: ConsultantReadModel[] = [
isOccupied: true,
graduationYear: 2010,
degree: Degree.Bachelor,
forecasts: [
mockForecast1,
mockForecast2,
mockForecast3,
mockForecast4,
mockForecast5,
mockForecast6,
],
},
];

Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ export interface ConsultantReadModel {
isOccupied: boolean;
imageUrl?: string;
imageThumbUrl?: string;
forecasts?: Forecast[];
}

export interface Forecast {
id: number;
month: number;
year: number;
forecastValue: number;
hasBeenChanged: boolean;
valueAddedManually: number;
}

export interface ConsultantWriteModel {
Expand Down
291 changes: 291 additions & 0 deletions frontend/src/components/Forecast/ForecastRows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
"use client";
import { ConsultantReadModel, ProjectWithCustomerModel } from "@/api-types";
import React, { useContext, useEffect, useState } from "react";
import { AlertCircle, CheckCircle, ChevronDown, Plus } from "react-feather";
import { DetailedBookingRows } from "@/components/Staffing/DetailedBookingRows";
import { WeekCell } from "@/components/Staffing/WeekCell";
import { useModal } from "@/hooks/useModal";
import { usePathname } from "next/navigation";
import {
dayToWeek,
getBookingTypeFromProjectState,
} from "../Staffing/EditEngagementHourModal/utils";
import { useWeekSelectors } from "@/hooks/useWeekSelectors";
import { setDetailedBookingHours } from "@/components/Staffing/DetailedBookingRows";
import { FilteredContext } from "@/hooks/ConsultantFilterProvider";
import { DateTime } from "luxon";
import Image from "next/image";
import { INTERNAL_CUSTOMER_NAME } from "../Staffing/helpers/utils";
import { MonthCell } from "./MonthCell";

export default function ForecastRows({
consultant,
numWorkHours,
}: {
consultant: ConsultantReadModel;
numWorkHours: number;
}) {
const [currentConsultant, setCurrentConsultant] =
useState<ConsultantReadModel>(consultant);
const [isListElementVisible, setIsListElementVisible] = useState(false);
const [isRowHovered, setIsRowHovered] = useState(false);
const [hoveredRowWeek, setHoveredRowWeek] = useState(-1);
const [isAddStaffingHovered, setIsAddStaffingHovered] = useState(false);
const [addNewRow, setAddNewRow] = useState(false);
const { weekList, setSelectedWeekSpan } = useWeekSelectors();
const { setIsDisabledHotkeys } = useContext(FilteredContext);
const { selectedWeekFilter, weekSpan } =
useContext(FilteredContext).activeFilters;

const [newWeekList, setNewWeekList] = useState<DateTime[]>(weekList);

useEffect(() => {
setSelectedWeekSpan(consultant.bookings.length);
setCurrentConsultant(consultant);
if (selectedWeekFilter) {
setNewWeekList([]);
Array.from({ length: weekSpan }).map((_, index) => {
setNewWeekList((old) => [
...old,
DateTime.fromISO(
`${selectedWeekFilter?.year}-W${selectedWeekFilter?.weekNumber}`,
).plus({ weeks: index }),
]);
});
}
}, [consultant.bookings.length, consultant]);

const columnCount = currentConsultant.bookings.length ?? 0;

function toggleListElementVisibility() {
setIsListElementVisible((old) => !old);
}

const {
closeModal: closeChangeEngagementModal,
openModal: openChangeEngagementModal,
modalRef: changeEngagementModalRef,
} = useModal({
closeOnBackdropClick: false,
});

const [selectedProjectId, setSelectedProjectId] = useState<
number | undefined
>(undefined);
const [selectedProject, setSelectedProject] = useState<
ProjectWithCustomerModel | undefined
>(undefined);

const organisationUrl = usePathname().split("/")[1];

useEffect(() => {
async function fetchProject() {
const url = `/${organisationUrl}/bemanning/api/projects?projectId=${selectedProjectId}`;

try {
const data = await fetch(url, {
method: "get",
});
setSelectedProject((await data.json()) as ProjectWithCustomerModel);
} catch (e) {
console.error("Error updating staffing", e);
}
}
selectedProjectId && fetchProject();
}, [organisationUrl, selectedProjectId]);

function openEngagementAndSetID(id: number) {
setSelectedProjectId(id);
openChangeEngagementModal();
}

function onCloseEngagementModal() {
setSelectedProject(undefined);
setSelectedProjectId(undefined);
}

function handleNewEngagementCancelled() {
setAddNewRow(false);
setIsDisabledHotkeys(false);
}

async function handleNewEngagement(project: ProjectWithCustomerModel) {
setAddNewRow(false);
setIsDisabledHotkeys(false);
setSelectedProject(project);

if (
project !== undefined &&
!currentConsultant.detailedBooking.some(
(e) =>
e.bookingDetails.projectId === project.projectId &&
e.bookingDetails.type ===
getBookingTypeFromProjectState(project?.bookingType),
)
) {
try {
const body = {
hours: 0,
bookingType: getBookingTypeFromProjectState(project?.bookingType),
organisationUrl: organisationUrl,
consultantId: currentConsultant.id,
bookingId: `${project?.projectId}`,
startWeek: dayToWeek(newWeekList[0]),
};
const res = await setDetailedBookingHours(body);

if (res) {
const tempCurrentConsultant = { ...currentConsultant };

const newDetailedBooking = res.detailedBooking.find(
(e) => e.bookingDetails.projectId === project.projectId,
);

if (newDetailedBooking) {
newDetailedBooking.hours = newWeekList.map((e) => {
return { week: dayToWeek(e), hours: 0 };
});
tempCurrentConsultant.detailedBooking.push(newDetailedBooking);
setCurrentConsultant(tempCurrentConsultant);
}
}
} catch (e) {
console.error("Error updating staffing", e);
}
}
}

return (
<>
<tr
className="h-[52px]"
onMouseEnter={() => setIsRowHovered(true)}
onMouseLeave={() => setIsRowHovered(false)}
>
<td
className={`border-l-2 ${
isListElementVisible
? "border-l-secondary"
: isRowHovered
? "border-l-primary"
: "border-l-primary/5"
} `}
>
{/* //utkommentert foreløpig, kan slettes om vi ikke skal ha mulighet til å utvide raden <button
className={`p-2 rounded-lg ml-2 hover:bg-primary hover:bg-opacity-10 ${
isListElementVisible && "rotate-180"
}`}
onClick={toggleListElementVisibility}
>
<ChevronDown className={`text-primary w-6 h-6`} />
</button> */}
</td>
<td>
<div className="flex justify-start gap-1 items-center">
<div className="flex flex-row justify-center self-center gap-2 w-3/12">
{consultant.imageThumbUrl ? (
<Image
src={consultant.imageThumbUrl}
alt={consultant.name}
className="w-10 h-10 rounded-md self-center object-contain"
width={32}
height={32}
/>
) : (
<div className="w-10 h-10 rounded-md bg-primary"></div>
)}
</div>
<div className="flex flex-col gap-1 w-7/12 ">
<p
className={`text-black text-start ${
isListElementVisible ? "normal-medium" : "normal"
}`}
>
{currentConsultant.name}
</p>
<p className="xsmall text-black/75 text-start">
{`${currentConsultant.yearsOfExperience} års erfaring`}
</p>
</div>
</div>
</td>
{currentConsultant.forecasts?.map((b, index) => (
<MonthCell
key={index}
hasBeenEdited={b.hasBeenChanged}
forecastValue={b.forecastValue + b.valueAddedManually}
month={b.month}
consultant={currentConsultant}
setHoveredRowWeek={setHoveredRowWeek}
hoveredRowWeek={hoveredRowWeek}
columnCount={columnCount}
isLastCol={index == currentConsultant.bookings.length - 1}
isSecondLastCol={index == currentConsultant.bookings.length - 2}
numWorkHours={numWorkHours}
/>
))}
</tr>
{isListElementVisible &&
currentConsultant.detailedBooking &&
currentConsultant.detailedBooking.map((db, index) => (
<DetailedBookingRows
key={index}
consultant={currentConsultant}
detailedBooking={db}
openEngagementAndSetID={openEngagementAndSetID}
numWorkHours={numWorkHours}
/>
))}
{isListElementVisible && addNewRow && (
<tr>
<td
className={`border-l-2 ${
isListElementVisible
? "border-l-secondary"
: isRowHovered
? "border-l-primary"
: "border-l-primary/5"
} `}
></td>
</tr>
)}

{isListElementVisible && (
<tr>
<td
className={`border-l-2 ${
isListElementVisible
? "border-l-secondary"
: isRowHovered
? "border-l-primary"
: "border-l-primary/5"
} `}
></td>
<td>
{!addNewRow && (
<button
onClick={() => {
setAddNewRow(true);
setIsDisabledHotkeys(true);
}}
className="flex flex-row items-center min-w-max gap-2 h-[52px]"
onMouseEnter={() => setIsAddStaffingHovered(true)}
onMouseLeave={() => setIsAddStaffingHovered(false)}
>
<span
className={`w-8 h-8 flex justify-center items-center rounded bg-primary/0 ${
isAddStaffingHovered && "bg-primary/10"
}`}
>
<Plus size={16} className="text-primary" />
</span>

<p className="small text-primary">Legg til bemanning</p>
</button>
)}
</td>
</tr>
)}
</>
);
}
Loading

0 comments on commit bd7b2f4

Please sign in to comment.