Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sprint view bar chart #115

Merged
merged 16 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/atoms/person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ export const personsWithTotalTimeAtom = atom<PersonWithTotalTime[]>([]);
export const timebankScreenPersonTotalTimeAtom = atom<PersonTotalTime | undefined>(undefined);
export const personDailyEntryAtom = atom<DailyEntryWithIndexSignature | undefined>(undefined);
export const dailyEntriesAtom = atom<DailyEntry[]>([]);
export const avatarsAtom = atom<UsersAvatars[]>([]);
export const avatarsAtom = atom<UsersAvatars[]>([]);
6 changes: 6 additions & 0 deletions src/atoms/sprint-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atom } from "jotai";
import type { Allocations, Projects } from "src/generated/homeLambdasClient";

export const timeEntriesAtom = atom<number[]>([]);
export const allocationsAtom = atom<Allocations[]>([]);
export const projectsAtom = atom<Projects[]>([]);
83 changes: 83 additions & 0 deletions src/components/charts/sprint-view-bar-chart.tsx
Jdallos marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { BarChart, XAxis, YAxis, Tooltip, Bar, ResponsiveContainer, Cell } from "recharts";
import { getHours, getHoursAndMinutes } from "src/utils/time-utils";
import type { SprintViewChartData } from "src/types";
import strings from "src/localization/strings";

/**
* Component properties
*/
interface Props {
chartData: SprintViewChartData[];
}

/**
* Sprint overview chart component
*
* @param props component properties
*/
const SprintViewBarChart = ({chartData}: Props) =>
<ResponsiveContainer
width="100%"
height={chartData.length === 1 ? 100 : chartData.length * 60 }>
<BarChart
data={chartData}
layout="vertical"
barGap={0}
margin={{top: 0, right: 0, left: 0, bottom: 0}}>
<XAxis
type="number"
axisLine={false}
tickFormatter={(value) => getHours(value as number)}
domain={[0, (dataMax: number) => dataMax]}
style={{fontSize: "18px"}}
padding={{left:0, right:0}}
/>
<YAxis
type="category"
dataKey="projectName"
tick={false}
hide={true}
/>
<Tooltip content={<CustomTooltip/>} />
<Bar
dataKey={"timeAllocated"}
name={ strings.sprint.timeAllocated }
barSize={20}>
{chartData.map((entry) => (
<Cell key={`cell-time-allocated-${entry.id}`} fill={entry.color} />
))}
</Bar>
<Bar
dataKey={"timeEntries"}
name={ strings.sprint.timeEntries }
barSize={20}>
{chartData.map((entry) => (
<Cell style={{opacity: "0.5"}} key={`cell-time-entries-${entry.id}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>

export default SprintViewBarChart;

/**
* Tooltip for chart component
*
* @param payload project values
* @param label name of the project
*/
const CustomTooltip = ({ payload, label }: any) => {
if (!payload.length) return;

return (
<div style={{backgroundColor:"white", opacity:"0.8", borderRadius:"10px"}}>
<p style={{padding:"10px 10px 0px 10px"}}>{label}</p>
<p style={{padding:"0px 10px 0px 10px"}}>
{strings.sprint.allocation}: {getHoursAndMinutes(payload[0].value as number)}
</p>
<p style={{padding:"0px 10px 10px 10px"}}>
{strings.sprint.timeEntries}: {getHoursAndMinutes(payload[1].value as number)}
</p>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Typography } from "@mui/material"
import strings from "src/localization/strings"

/**
* Sprint card component for admin
*/
const AdminSprintViewCard = () => {
return (
<Typography variant="body1">{ strings.placeHolder.notYetImplemented }</Typography>
)
}

export default AdminSprintViewCard;
121 changes: 121 additions & 0 deletions src/components/home/sprint-view-card-content/user-sprint-view-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useEffect, useState } from "react";
import { personsAtom } from "src/atoms/person";
import { userProfileAtom } from "src/atoms/auth";
import type { Person } from "src/generated/client";
import config from "src/app/config";
import { useLambdasApi } from "src/hooks/use-api";
import type { TimeEntries } from "src/generated/homeLambdasClient";
import { CardContent, Skeleton, Typography } from "@mui/material";
import SprintViewBarChart from "src/components/charts/sprint-view-bar-chart";
import type { SprintViewChartData } from "src/types";
import strings from "src/localization/strings";
import { totalAllocations, filterAllocationsAndProjects } from "src/utils/sprint-utils";
import { errorAtom } from "src/atoms/error";
import { allocationsAtom, projectsAtom, timeEntriesAtom } from "src/atoms/sprint-data";

/**
* Sprint card component for users
*/
const UserSprintViewCard = () => {
const [loading, setLoading] = useState(false);
const [persons] = useAtom(personsAtom);
const userProfile = useAtomValue(userProfileAtom);
const loggedInPerson = persons.find(
(person: Person) =>
person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id
);
const [allocations, setAllocations] = useAtom(allocationsAtom);
const [projects, setProjects] = useAtom(projectsAtom);
const [timeEntries, setTimeEntries] = useAtom(timeEntriesAtom);
const { allocationsApi, projectsApi, timeEntriesApi } = useLambdasApi();
const setError = useSetAtom(errorAtom);

useEffect(() => {
getAllocationsAndProjects();
}, [loggedInPerson]);

/**
* Get allocations, projects names, colors and time entries
*/
const getAllocationsAndProjects = async () => {
setLoading(true);
if (loggedInPerson && !allocations.length) {
try {
const fetchedAllocations = await allocationsApi.listAllocations({
personId: loggedInPerson.id.toString(),
startDate: new Date(),
endDate: new Date()
});
const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() });
const { filteredAllocations, filteredProjects } = filterAllocationsAndProjects(
fetchedAllocations,
fetchedProjects
);
const totalTimeEntries = await Promise.all(
filteredAllocations.map(async (allocation) => {
try {
if (allocation.project) {
const fetchedTimeEntries = await timeEntriesApi.listProjectTimeEntries({
projectId: allocation.project,
startDate: allocation.startDate,
endDate: allocation.endDate
});
let totalMinutes = 0;
fetchedTimeEntries.map((timeEntry: TimeEntries) => {
if (loggedInPerson && timeEntry.person === loggedInPerson.id) {
totalMinutes += timeEntry.timeRegistered || 0;
}
});
return totalMinutes;
}
} catch (error) {
setError(`${strings.sprintRequestError.fetchTimeEntriesError}, ${error}`);
}
return 0;
})
);
setProjects(filteredProjects);
setAllocations(filteredAllocations);
setTimeEntries(totalTimeEntries);
} catch (error) {
setError(`${strings.sprintRequestError.fetchError}, ${error}`);
}
}
setLoading(false);
};

/**
* Combines allocations and projects data for chart
*/
const createChartData = (): SprintViewChartData[] => {
return allocations.map((allocation, index) => {
return {
id: index,
projectName: projects[index].name || "",
timeAllocated: totalAllocations(allocation),
timeEntries: timeEntries[index],
color: projects[index].color || ""
};
});
};

/**
* Renders sprint view bar chart
*/
const renderBarChart = () => (
<>
{allocations.length ? (
<CardContent sx={{ display: "flex", justifyContent: "left" }}>
<SprintViewBarChart chartData={createChartData()} />
</CardContent>
) : (
<Typography style={{ paddingLeft: "0" }}>{strings.sprint.noAllocation}</Typography>
)}
</>
);

return <>{!loggedInPerson || loading ? <Skeleton /> : renderBarChart()}</>;
};

export default UserSprintViewCard;
48 changes: 14 additions & 34 deletions src/components/home/sprint-view-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Card, CardContent, Typography } from "@mui/material";
import { Link } from "react-router-dom";
import strings from "src/localization/strings";
import UserRoleUtils from "src/utils/user-role-utils";
import AdminSprintViewCard from "./sprint-view-card-content/admin-sprint-view-card";
import UserSprintViewCard from "./sprint-view-card-content/user-sprint-view-card";

/**
* SprintView card component
Expand All @@ -10,40 +12,18 @@ const SprintViewCard = () => {
const adminMode = UserRoleUtils.adminMode();

return (
<Link
to={adminMode ? "/admin/sprintview" : "/sprintview"}
style={{ textDecoration: "none" }}
>
<Card
sx={{
"&:hover": {
background: "#efefef"
}
}}
>
{adminMode ? (
<CardContent>
<Typography
variant="h6"
fontWeight={"bold"}
style={{ marginTop: 6, marginBottom: 3 }}
>
{strings.sprint.sprintview}
</Typography>
<Typography variant="body1">{ strings.placeHolder.notYetImplemented }</Typography>
</CardContent>
) : (
<CardContent>
<Typography
variant="h6"
fontWeight={"bold"}
style={{ marginTop: 6, marginBottom: 3 }}
>
{strings.sprint.sprintview}
</Typography>
<Typography variant="body1">{ strings.placeHolder.notYetImplemented }</Typography>
</CardContent>
)}
<Link to={adminMode ? "/admin/sprintview" : "/sprintview"} style={{ textDecoration: "none" }}>
<Card sx={{"&:hover": {background: "#efefef"}}}>
<CardContent>
<Typography variant="h6" fontWeight={"bold"} style={{ marginTop: 6, marginBottom: "20px" }}>
{strings.sprint.sprintview}
</Typography>
{adminMode ? (
<AdminSprintViewCard/>
) : (
<UserSprintViewCard/>
)}
</CardContent>
</Card>
</Link>
);
Expand Down
18 changes: 13 additions & 5 deletions src/components/layout/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
import LocalizationButtons from "../layout-components/localization-buttons";
import strings from "src/localization/strings";
import { authAtom, userProfileAtom } from "src/atoms/auth";
import { useAtom, useAtomValue } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import NavItems from "./navitems";
import SyncButton from "./sync-button";
import { avatarsAtom, personsAtom } from "src/atoms/person";
import type { Person } from "src/generated/client";
import config from "src/app/config";
import { useLambdasApi } from "src/hooks/use-api";
import { errorAtom } from "src/atoms/error";

/**
* NavBar component
Expand All @@ -30,6 +31,7 @@ const NavBar = () => {
const [avatars, setAvatars] = useAtom(avatarsAtom);
const persons: Person[] = useAtomValue(personsAtom);
const userProfile = useAtomValue(userProfileAtom);
const setError = useSetAtom(errorAtom);
const { slackAvatarsApi } = useLambdasApi();
const loggedInPerson = persons.find(
(person: Person) =>
Expand Down Expand Up @@ -62,16 +64,22 @@ const NavBar = () => {
};

/**
* fetch avatars
* Fetch Slack avatars
*/
const getSlackAvatars = async () => {
const fetchedAvatars = await slackAvatarsApi.slackAvatar();
setAvatars(fetchedAvatars);
if (avatars) return;
try {
const fetchedAvatars = await slackAvatarsApi.slackAvatar();
setAvatars(fetchedAvatars);
}
catch (error) {
setError(`${strings.error.fetchSlackAvatarsFailed}: ${error}`);
}
};

useEffect(() => {
getSlackAvatars();
}, [loggedInPerson]);
}, []);

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/screens/sprint-view-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ const SprintViewScreen = () => {
* Fetch allocations, project names and time entries
*/
const fetchProjectDetails = async () => {
setLoading(true);
if (!loggedInPerson) return;


setLoading(true);
try {
const fetchedAllocations = await allocationsApi.listAllocations({
startDate: new Date(),
Expand Down
2 changes: 2 additions & 0 deletions src/components/sprint-view-table/menu-Item-filter-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface Props {

/**
* Filters tasks by status categories
Kseniia-Koshkina marked this conversation as resolved.
Show resolved Hide resolved
*
* @param props Component properties
*/
export const TaskStatusFilter = ({setFilter}: Props) => {
const statusFilters = [
Expand Down
Loading