Skip to content

Commit

Permalink
feat: project page (#490)
Browse files Browse the repository at this point in the history
Co-authored-by: Ida Marie Andreassen <[email protected]>
  • Loading branch information
TrymVei and idamand authored Jun 13, 2024
1 parent 8cfc04d commit ece9cce
Show file tree
Hide file tree
Showing 12 changed files with 592 additions and 72 deletions.
102 changes: 102 additions & 0 deletions frontend/src/app/[organisation]/prosjekt/[project]/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
import { ConsultantReadModel, ProjectWithCustomerModel } from "@/api-types";
import InfoBox from "@/components/InfoBox";
import { weekToString } from "@/data/urlUtils";
import { setWeeklyTotalBillableForProject } from "@/hooks/staffing/useConsultantsFilter";
import { useWeekSelectors } from "@/hooks/useWeekSelectors";
import { Week } from "@/types";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";

// TODO: Call funtion and set a state when adding or removing consultants and hours
function Sidebar({ project }: { project: ProjectWithCustomerModel }) {
const { selectedWeek, selectedWeekSpan } = useWeekSelectors();

const [selectedConsultants, setSelectedConsultants] = useState<
ConsultantReadModel[]
>([]);

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

useEffect(() => {
if (project != undefined) {
fetchConsultantsFromProject(
project,
organisationUrl,
selectedWeek,
selectedWeekSpan,
).then((res) => {
setSelectedConsultants([
// Use spread to make a new list, forcing a re-render
...res,
]);
});
}
}, [project, organisationUrl, selectedWeek, selectedWeekSpan]);

function calculateTotalHours() {
const weeklyTotalBillableAndOffered = setWeeklyTotalBillableForProject(
selectedConsultants,
project,
);
var sum = 0;
weeklyTotalBillableAndOffered.forEach((element) => {
sum += element;
});
return sum.toString();
}

return (
<div className="sidebar z-10">
<div className=" bg-primary/5 h-full flex flex-col gap-6 p-4 w-[300px]">
<div className="flex flex-col gap-6">
<h2 className="text-h1">Info</h2>
<div className="flex flex-col gap-2">
<h3>Om</h3>
<InfoBox
infoName="Navn på kunde"
infoValue={project.customerName}
/>
</div>
<div className="flex flex-col gap-2">
<h3>Bemanning</h3>
<InfoBox
infoName="Antall konsulenter"
infoValue={selectedConsultants.length.toString()}
/>
<InfoBox
infoName="Planlagt timeforbruk"
infoValue={calculateTotalHours()}
/>
</div>
</div>
</div>
</div>
);
}

async function fetchConsultantsFromProject(
project: ProjectWithCustomerModel,
organisationUrl: string,
selectedWeek: Week,
selectedWeekSpan: number,
) {
const url = `/${organisationUrl}/bemanning/api/projects/staffings?projectId=${
project.projectId
}&selectedWeek=${weekToString(
selectedWeek,
)}&selectedWeekSpan=${selectedWeekSpan}`;

try {
const data = await fetch(url, {
method: "get",
});
return (await data.json()) as ConsultantReadModel[];
} catch (e) {
console.error("Error updating staffing", e);
}

return [];
}

export default Sidebar;
7 changes: 7 additions & 0 deletions frontend/src/app/[organisation]/prosjekt/[project]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function BemanningLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
59 changes: 59 additions & 0 deletions frontend/src/app/[organisation]/prosjekt/[project]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { EditEngagementHour } from "@/components/Staffing/EditEngagementHourModal/EditEngagementHour";
import {
fetchEmployeesWithImageAndToken,
fetchWithToken,
} from "@/data/apiCallsWithToken";
import { ProjectWithCustomerModel } from "@/api-types";
import Sidebar from "./Sidebar";
import { ConsultantFilterProvider } from "@/hooks/ConsultantFilterProvider";
import { parseYearWeekFromUrlString } from "@/data/urlUtils";

export default async function Project({
params,
searchParams,
}: {
params: { organisation: string; project: string };
searchParams: { selectedWeek?: string; weekSpan?: string };
}) {
const project =
(await fetchWithToken<ProjectWithCustomerModel>(
`${params.organisation}/projects/get/${params.project}`,
)) ?? undefined;

const selectedWeek = parseYearWeekFromUrlString(
searchParams.selectedWeek || undefined,
);
const weekSpan = searchParams.weekSpan || undefined;

const consultants =
(await fetchEmployeesWithImageAndToken(
`${params.organisation}/staffings${
selectedWeek
? `?Year=${selectedWeek.year}&Week=${selectedWeek.weekNumber}`
: ""
}${weekSpan ? `${selectedWeek ? "&" : "?"}WeekSpan=${weekSpan}` : ""}`,
)) ?? [];

if (project) {
return (
<ConsultantFilterProvider
consultants={consultants}
departments={[]}
competences={[]}
customers={[]}
>
<Sidebar project={project} />
<div className="main p-4 pt-5 w-full flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h1>{project.projectName}</h1>
<h2>{project.customerName}</h2>
</div>

<EditEngagementHour project={project} />
</div>
</ConsultantFilterProvider>
);
} else {
return <h1>Fant ikke prosjektet</h1>;
}
}
46 changes: 22 additions & 24 deletions frontend/src/components/Buttons/FilterButton.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
export default function FilterButton({
label,
onClick,
checked,
enabled = true,
rounded,
...inputProps
}: {
label: string;
onClick: () => void;
checked: boolean;
enabled?: boolean;
}) {
function handleToggle() {
if (enabled) {
onClick();
}
}

rounded?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) {
return (
<div className="flex items-center" onClick={handleToggle}>
<div className="flex items-center" onClick={inputProps.onClick}>
<input
id="checkbox"
type="checkbox"
className={`appearance-none border flex items-center border-opacity-50 m-[1px] mr-2 h-4 w-4 rounded-sm border-primary hover:border-primary checked:bg-primary
aria-labelledby={label.replaceAll(/ /g, "-")}
disabled={inputProps.disabled}
readOnly
{...inputProps}
className={`appearance-none border flex items-center border-opacity-50 m-[1px] mr-2 h-4 w-4 border-primary hover:border-primary checked:bg-primary
${
!enabled &&
inputProps.disabled &&
"border-black/20 hover:border-black/20 checked:bg-primary/50 bg-black/5"
}
${
checked
inputProps.checked
? "hover:bg-primary hover:brightness-[1.5]"
: "hover:bg-primary hover:bg-opacity-10"
}`}
checked={checked}
disabled={!enabled}
readOnly
}
${rounded ? "rounded-full" : "rounded-sm"}
`}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -41,7 +34,7 @@ export default function FilterButton({
viewBox="0 0 12 12"
fill="none"
className={`absolute ml-[3px] pointer-events-none ${
!checked && "hidden"
!inputProps.checked && "hidden"
}`}
>
<path
Expand All @@ -51,7 +44,12 @@ export default function FilterButton({
fill="white"
/>
</svg>
<label className={`normal select-none ${!enabled && "text-black/50"}`}>
<label
id={label.replaceAll(/ /g, "-")}
className={`normal select-none ${
inputProps.disabled && "text-black/50"
}`}
>
{label}
</label>
</div>
Expand Down
88 changes: 88 additions & 0 deletions frontend/src/components/ChangeEngagementState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use client";

import {
EngagementState,
ProjectWithCustomerModel,
UpdateProjectWriteModel,
} from "@/api-types";
import { useState } from "react";
import FilterButton from "./Buttons/FilterButton";
import { usePathname, useRouter } from "next/navigation";

export default function ChangeEngagementState({
project,
}: {
project: ProjectWithCustomerModel;
}) {
const organisationName = usePathname().split("/")[1];

const router = useRouter();

const [engagementState, setEngagementState] = useState<EngagementState>(
project.bookingType,
);

async function handleChange(newState: EngagementState) {
setEngagementState(newState);

const currentDate = new Date();
const startYear = currentDate.getFullYear();
const startWeek = Math.ceil(
(currentDate.getTime() - new Date(startYear, 0, 1).getTime()) /
(7 * 24 * 60 * 60 * 1000),
);

const body: UpdateProjectWriteModel = {
engagementId: project.projectId,
projectState: newState,
startYear: startYear,
startWeek: startWeek,
weekSpan: 26,
};

await submitAddEngagementForm(body);
router.refresh();
}

async function submitAddEngagementForm(body: UpdateProjectWriteModel) {
const url = `/${organisationName}/bemanning/api/projects/updateState`;
try {
const data = await fetch(url, {
method: "PUT",
body: JSON.stringify({
...body,
}),
});
return (await data.json()) as ProjectWithCustomerModel;
} catch (e) {
console.error("Error updating engagement state", e);
}
}

return (
<form className="flex flex-row gap-4">
<FilterButton
label="Tilbud"
rounded={true}
value={EngagementState.Offer}
checked={engagementState === EngagementState.Offer}
onChange={(e) => handleChange(e.target.value as EngagementState)}
/>
<FilterButton
label="Ordre"
rounded={true}
value={EngagementState.Order}
checked={engagementState === EngagementState.Order}
onChange={(e) => handleChange(e.target.value as EngagementState)}
/>

<FilterButton
label="Avsluttet"
rounded={true}
value={EngagementState.Closed}
checked={engagementState === EngagementState.Closed}
onChange={(e) => handleChange(e.target.value as EngagementState)}
/>
</form>
);
}
9 changes: 6 additions & 3 deletions frontend/src/components/CostumerTable/CustomerRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function CostumerRow({
</tr>
{isListElementVisible &&
customer.engagements &&
customer.engagements.map((engagement, index) => (
customer.engagements.map((engagement) => (
<tr key={`${engagement.engagementId}-details`} className="h-fit">
<td className="border-l-secondary border-l-2">
<div
Expand All @@ -91,14 +91,17 @@ export default function CostumerRow({
</div>
</td>
<td className="flex flex-row gap-2 justify-start relative">
<div className="flex flex-col justify-center">
<Link
href={`prosjekt/${engagement.engagementId}`}
className="flex flex-col justify-center"
>
<p className="xsmall text-black/75 whitespace-nowrap text-ellipsis overflow-x-hidden max-w-[145px]">
{engagement.isBillable ? "Fakturerbart" : "Ikke-fakturerbart"}
</p>
<p className="text-black text-start small">
{engagement.engagementName}
</p>
</div>
</Link>
</td>
</tr>
))}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Staffing/AddEngagementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export function AddEngagementForm({

async function submitAddEngagementForm(body: EngagementWriteModel) {
const url = `/${organisationName}/bemanning/api/projects`;

setIsSubmitting(true);
try {
const data = await fetch(url, {
Expand Down Expand Up @@ -298,7 +299,7 @@ export function AddEngagementForm({
label="Fakturerbart"
onClick={handleBillableToggled}
checked={isBillable}
enabled={!isInternalProject}
disabled={isInternalProject}
/>
</>
)}
Expand Down
Loading

0 comments on commit ece9cce

Please sign in to comment.