diff --git a/frontend/src/app/tasks/components/WorkSummaryPanel.tsx b/frontend/src/app/tasks/components/WorkSummaryPanel.tsx new file mode 100644 index 000000000..e3817dccb --- /dev/null +++ b/frontend/src/app/tasks/components/WorkSummaryPanel.tsx @@ -0,0 +1,230 @@ +'use client' +import { useState } from 'react'; +import Box from '@mui/joy/Box'; +import Stack from '@mui/joy/Stack'; +import AccordionGroup from '@mui/joy/AccordionGroup'; +import Accordion from '@mui/joy/Accordion' +import AccordionDetails, { accordionDetailsClasses } from '@mui/joy/AccordionDetails'; +import AccordionSummary, { accordionSummaryClasses } from '@mui/joy/AccordionSummary'; +import Card from '@mui/joy/Card'; +import CardContent from '@mui/joy/CardContent' +import Divider from '@mui/joy/Divider'; +import Table from '@mui/joy/Table'; +import { Tooltip } from '@mui/joy'; +import Avatar from '@mui/joy/Avatar' +import List from '@mui/joy/List'; +import ListItem from '@mui/joy/ListItem'; +import ListItemContent from '@mui/joy/ListItemContent'; +import LinearProgress from '@mui/joy/LinearProgress'; +import Typography from '@mui/joy/Typography' +import { Beach24Filled, AddSubtractCircle24Filled, CalendarDataBar24Regular, Info16Regular } from '@fluentui/react-icons'; +import { useGetWorkSummary } from '../hooks/useWorkSummary'; +import { useGetCurrentUser } from '@/hooks/useGetCurrentUser/useGetCurrentUser'; +import { getYear } from 'date-fns'; + +type WorkSummaryProps = { + contentSidebarExpanded: boolean +} + +const thisYear = getYear(new Date()) + +export const WorkSummaryPanel = ({contentSidebarExpanded} : WorkSummaryProps) => { + const [index, setIndex] = useState(0); + const summary = useGetWorkSummary() + const user = useGetCurrentUser() + const currentCapacity = user.capacities?.find(x => x.isCurrent) + + return ( + + { + setIndex(expanded ? 0 : null); + }} + > + + + + + + Worked hours + + View cumulative and segmented worked hours + + + + + + + Today: + {summary?.todayText} + + + This week: + {summary?.weekText} + + + + Hours by project + + + + + + + + + + {summary?.projectSummaries?.map((projSummary) => ( + + + + + + ))} + + + + + + + + +
ProjectTodayWeek
{projSummary.project}{projSummary.todayText}{projSummary.weekText}
Total{summary?.todayText}{summary?.weekText}
+
+
+ { + setIndex(expanded ? 1 : null); + }} + > + + + + + + Over/under hours + + Amount of hours ahead or behind expected + + + + + + Based on your capacity ({currentCapacity?.capacity } hours per day), you will have an expected number of hours to be worked for the week and year. The worked numbers below are calculated as of today. + + + Week + + + Worked: + {(summary?.week ?? 0) / 60} h + + + Expected: + {summary?.expectedHoursWeek} h + + + Ahead/behind expected: + {((summary?.week?? 0) /60) - (summary?.expectedHoursWeek ?? 0)} h + + + Year + + + Worked: + {summary?.workedHoursYear} h + + + Total expected so far: + {summary?.expectedHoursToDate} h + + + Ahead/behind pace for year: + {(summary?.workedHoursYear ?? 0) - (summary?.expectedHoursToDate ?? 0)} h + + + Total expected for year: + {summary?.expectedHoursYear} h + + + Ahead/behind total for year: + {(summary?.workedHoursYear ?? 0) - (summary?.expectedHoursYear ?? 0)} h + + + + + + { + setIndex(expanded ? 2 : null); + }} + > + + + + + + Vacation + View scheduled and available vacation + + + + + + Available for {thisYear} + {summary?.vacationAvailableText} + + + Status + + + + + + Used: + + {summary?.vacationUsedText} + + + + + + + Scheduled: + + {summary?.vacationScheduledText} + + + + + + + Pending: + + {summary?.vacationPendingText} + + + + + +
+ + ) +} diff --git a/frontend/src/app/tasks/hooks/useCreateTask.ts b/frontend/src/app/tasks/hooks/useCreateTask.ts index 136af1399..9f700a650 100644 --- a/frontend/src/app/tasks/hooks/useCreateTask.ts +++ b/frontend/src/app/tasks/hooks/useCreateTask.ts @@ -22,6 +22,7 @@ export const useCreateTask = () => { onSuccess: (_, { handleSuccess }) => { handleSuccess() queryClient.invalidateQueries(['tasks', userId]) + queryClient.invalidateQueries(['workSummary', userId]) showSuccess('Task added succesfully') revalidateTag('tags') }, diff --git a/frontend/src/app/tasks/hooks/useDeleteTask.ts b/frontend/src/app/tasks/hooks/useDeleteTask.ts index 18541a0ac..df652d48e 100644 --- a/frontend/src/app/tasks/hooks/useDeleteTask.ts +++ b/frontend/src/app/tasks/hooks/useDeleteTask.ts @@ -17,7 +17,8 @@ export const useDeleteTask = () => { queryClient.setQueryData>(['tasks', userId], (prevData) => prevData!.filter((prevData) => prevData.id !== taskId) ) - showSuccess('Task succesfully removed') + queryClient.invalidateQueries(['workSummary', userId]) + showSuccess('Task successfully removed') }, onError: () => { showError('Failed to remove task') diff --git a/frontend/src/app/tasks/hooks/useEditTask.ts b/frontend/src/app/tasks/hooks/useEditTask.ts index 33926ec47..7208fe246 100644 --- a/frontend/src/app/tasks/hooks/useEditTask.ts +++ b/frontend/src/app/tasks/hooks/useEditTask.ts @@ -28,7 +28,8 @@ export const useEditTask = ({ handleSuccess }: UseEditTaskProps) => { return task }) ) - showSuccess('Task succesfully edited') + queryClient.invalidateQueries(['workSummary', userId]) + showSuccess('Task successfully edited') handleSuccess() }, onError: (e) => { diff --git a/frontend/src/app/tasks/hooks/useWorkSummary.ts b/frontend/src/app/tasks/hooks/useWorkSummary.ts new file mode 100644 index 000000000..505427ec0 --- /dev/null +++ b/frontend/src/app/tasks/hooks/useWorkSummary.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query' +import { useClientFetch } from '@/infra/lib/useClientFetch' +import { getWorkSummary } from '@/infra/workSummary/getWorkSummary' +import { useGetCurrentUser } from '@/hooks/useGetCurrentUser/useGetCurrentUser' + +export const useGetWorkSummary = () => { + const apiClient = useClientFetch() + const { id: userId } = useGetCurrentUser() + + const { data } = useQuery({ + queryKey: ['workSummary', userId], + queryFn: () => getWorkSummary(apiClient), + initialData: [] + }) + return data +} diff --git a/frontend/src/domain/WorkSummary.ts b/frontend/src/domain/WorkSummary.ts new file mode 100644 index 000000000..e44b1b558 --- /dev/null +++ b/frontend/src/domain/WorkSummary.ts @@ -0,0 +1,30 @@ + +export type WorkSummary = { + today?: number | null + week?: number | null + todayText: string + weekText: string + projectSummaries?: Array | null + vacationAvailable: number + vacationAvailableText: string + vacationScheduled: number + vacationScheduledText: string + vacationPending: number + vacationPendingText: string + vacationUsed: number + vacationUsedText: string + expectedHoursYear: number + expectedHoursToDate: number + expectedHoursWeek: number + workedHoursYear: number +} + +export type ProjectTaskSummary = { + projectId: number + project: string + todayTotal: string + todayText: string + weekTotal: string + weekText: string + isVacation: boolean +} diff --git a/frontend/src/infra/workSummary/getWorkSummary.ts b/frontend/src/infra/workSummary/getWorkSummary.ts new file mode 100644 index 000000000..ffbeeec9a --- /dev/null +++ b/frontend/src/infra/workSummary/getWorkSummary.ts @@ -0,0 +1,14 @@ +import { ApiClient } from '@/infra/lib/apiClient' +import { WorkSummary } from '@/domain/WorkSummary'; + +export const getWorkSummary = async ( + apiClient: ApiClient +): Promise => { + const response = await apiClient(`/v1/timelog/summary`) + + if (!response.ok) { + throw new Error('Failed to fetch Work Summary') + } + + return await response.json() +} diff --git a/frontend/src/ui/BaseLayout/BaseLayout.tsx b/frontend/src/ui/BaseLayout/BaseLayout.tsx index 10e7fde14..63d3d4a51 100644 --- a/frontend/src/ui/BaseLayout/BaseLayout.tsx +++ b/frontend/src/ui/BaseLayout/BaseLayout.tsx @@ -1,15 +1,16 @@ 'use client' import { useState } from 'react' import { styled } from '@mui/joy/styles' - import { Sidebar } from '@/ui/Sidebar/Sidebar' import { ContentSidebar } from '@/ui/ContentSidebar/ContentSidebar' import { Alert } from '@/ui/Alert/Alert' +import { WorkSummaryPanel } from '@/app/tasks/components/WorkSummaryPanel'; +import { usePathname } from 'next/navigation' export const BaseLayout = ({ children }: { children: React.ReactNode }) => { const [navBarExpanded, setNavBarExpanded] = useState(false) const [contentBarExpanded, setContentBarExpanded] = useState(false) - + const pathname = usePathname() return ( <> Skip Navigation @@ -29,12 +30,15 @@ export const BaseLayout = ({ children }: { children: React.ReactNode }) => { > {children} - setContentBarExpanded((prevState) => !prevState)} - > -
Right Sidebar
-
+ { + pathname.includes('/tasks') && + setContentBarExpanded((prevState) => !prevState)} + > + + + } )