From 7bdaa0481f4b46db17bd97fbb3bba647ee35082b Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Tue, 12 Mar 2024 14:48:51 +0200 Subject: [PATCH 01/32] a --- .gitmodules | 3 +++ home-lambdas-API-spec | 1 + 2 files changed, 4 insertions(+) create mode 160000 home-lambdas-API-spec diff --git a/.gitmodules b/.gitmodules index 0eba0f28..4d6ffb68 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "time-bank-api-spec"] path = time-bank-api-spec url = https://github.com/Metatavu/time-bank-api-spec +[submodule "home-lambdas-API-spec"] + path = home-lambdas-API-spec + url = https://github.com/Metatavu/home-lambdas-API-spec diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec new file mode 160000 index 00000000..ebb3535c --- /dev/null +++ b/home-lambdas-API-spec @@ -0,0 +1 @@ +Subproject commit ebb3535c35bcd1b3f0f9317a09ebc97a15162fe2 From abb18fe7a0eef76b85ca34b2017ad38b3d285bae Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Tue, 12 Mar 2024 14:50:41 +0200 Subject: [PATCH 02/32] a --- package.json | 3 +- src/api/home-lambdas-api.ts | 48 ++++ src/app/config.ts | 9 +- src/atoms/api.ts | 2 + .../providers/authentication-provider.tsx | 1 - src/components/screens/sprint-view-screen.tsx | 220 +++++++++++++++++- src/hooks/use-api.ts | 3 +- 7 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 src/api/home-lambdas-api.ts diff --git a/package.json b/package.json index e7c98a0a..11a95fd6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "postinstall": "npm run build-client", - "build-client": "openapi-generator-cli generate -i time-bank-api-spec/swagger.yaml -o ./src/generated/client -c generator-config.json -g typescript-fetch" + "build-client": "openapi-generator-cli generate -i time-bank-api-spec/swagger.yaml -o ./src/generated/client -c generator-config.json -g typescript-fetch", + "build-home-lambdas-client": "openapi-generator-cli generate -i home-lambdas-API-spec/swagger.yaml -o ./src/generated/homeLambdasClient -c generator-config.json -g typescript-fetch" }, "dependencies": { "@emotion/react": "^11.11.1", diff --git a/src/api/home-lambdas-api.ts b/src/api/home-lambdas-api.ts new file mode 100644 index 00000000..d1e3cfee --- /dev/null +++ b/src/api/home-lambdas-api.ts @@ -0,0 +1,48 @@ +import config from "../app/config"; +import { + AllocationsApi, + Configuration, + ConfigurationParameters, + ProjectsApi, + TasksApi, + TimeEntriesApi +} from "../generated/homeLambdasClient" + +/** + * Generic type that accepts parameters within the @ConfigurationParameters interface + */ +type ConfigConstructor = new (_params: ConfigurationParameters) => T; + +/** + * Creates a new ConfigConstructor instance with params required to access the Metatavu Home Lambda API + * + * @param ConfigConstructor ConfigConstructor class instance + * @param basePath Metatavu Home Lambda API base URL + * @param accessToken Access token for request + * @returns ConfigConstructor instance set up with params + */ +const getConfigurationFactory = + (ConfigConstructor: ConfigConstructor, basePath: string, accessToken?: string) => + () => { + return new ConfigConstructor({ + basePath: basePath, + accessToken: accessToken + }); + }; + +/** +* Metatavu Home Lambda API client with request functions to several endpoints +* +* @param accessToken Access token required for authentication +* @returns Configured API request functions +*/ +export const getLambdasClient = (accessToken?: string) => { + const getConfiguration = getConfigurationFactory(Configuration, config.lambdas.baseUrl, accessToken); + + return { + allocationsApi: new AllocationsApi(getConfiguration()), + projectsApi: new ProjectsApi(getConfiguration()), + tasksApi: new TasksApi(getConfiguration()), + timeEntriesApi: new TimeEntriesApi(getConfiguration()) + }; +}; \ No newline at end of file diff --git a/src/app/config.ts b/src/app/config.ts index 1e276cde..e61fa496 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -9,6 +9,9 @@ interface Config { api: { baseUrl: string; }; + lambdas: { + baseUrl: string; + }; person: { forecastUserIdOverride: number; }; @@ -19,7 +22,8 @@ const env = cleanEnv(import.meta.env, { VITE_KEYCLOAK_REALM: str(), VITE_KEYCLOAK_CLIENT_ID: str(), VITE_API_BASE_URL: url(), - VITE_FORECAST_USER_ID_OVERRIDE: num({ default: undefined }) + VITE_FORECAST_USER_ID_OVERRIDE: num({ default: undefined }), + VITE_PIPEDRIVE_URL: url() }); const config: Config = { @@ -31,6 +35,9 @@ const config: Config = { api: { baseUrl: env.VITE_API_BASE_URL }, + lambdas: { + baseUrl: env.VITE_PIPEDRIVE_URL + }, person: { forecastUserIdOverride: env.VITE_FORECAST_USER_ID_OVERRIDE } diff --git a/src/atoms/api.ts b/src/atoms/api.ts index d557e897..f0444a89 100644 --- a/src/atoms/api.ts +++ b/src/atoms/api.ts @@ -1,5 +1,7 @@ import { atom } from "jotai"; import { getApiClient } from "../api/api"; import { authAtom } from "./auth"; +import { getLambdasClient } from "../api/home-lambdas-api"; export const apiClientAtom = atom((get) => getApiClient(get(authAtom)?.tokenRaw)); +export const apiLambasAtom = atom((get) => getLambdasClient(get(authAtom)?.tokenRaw)); diff --git a/src/components/providers/authentication-provider.tsx b/src/components/providers/authentication-provider.tsx index 5d6f4b4f..f8bdf83c 100644 --- a/src/components/providers/authentication-provider.tsx +++ b/src/components/providers/authentication-provider.tsx @@ -80,7 +80,6 @@ const AuthenticationProvider = ({ children }: Props) => { * Sets the logged in timebank person from keycloak ID into global state */ const getPersonsList = async () => { - console.log("test2") const fetchedPersons = await personsApi.listPersons({ active: true }); setPersons(fetchedPersons); }; diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 24eb7c4a..00a0b3dc 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,11 +1,223 @@ +import React, { useState, useEffect } from 'react'; import strings from "../../localization/strings"; +import { Card, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { useLambdasApi } from "../../hooks/use-api"; +import { Person } from "../../generated/client"; +import { useAtomValue } from "jotai"; +import { personsAtom } from "../../atoms/person"; +import config from "../../app/config"; +import { userProfileAtom } from "../../atoms/auth"; +import { Allocations } from "../../generated/homeLambdasClient/models/Allocations"; +import { Projects } from "../../generated/homeLambdasClient/models/Projects"; +import { Tasks } from "../../generated/homeLambdasClient/models/Tasks"; -/** - * Sprint View screen component - */ const SprintViewScreen = () => { + const { allocationsApi, projectsApi, tasksApi } = useLambdasApi(); + const persons = useAtomValue(personsAtom); + const userProfile = useAtomValue(userProfileAtom); + const loggedInPerson = persons.find( + (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id + ); + + const [allocations, setAllocations] = useState([]); + const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [minimizedProjects, setMinimizedProjects] = useState([]); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + await Promise.all([getPersonAllocations(), getProjects(), getTasks()]); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [loggedInPerson]); + + const getProjects = async () => { + if (loggedInPerson) { + try { + const fetchedProjects = await projectsApi.listProjects({ + startDate: new Date() + }); + + const mappedProjects: Projects[] = fetchedProjects.map((project: Projects) => ({ + id: project.id, + name: project.name + })); + setProjects(mappedProjects); + } catch (error) { + console.error("Error fetching projects:", error); + } + } + }; + + const getPersonAllocations = async () => { + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + }); + + const mappedAllocations: Allocations[] = fetchedAllocations.map((allocation) => ({ + id: allocation.id, + project: allocation.project, + person: allocation.person, + monday: allocation.monday, + tuesday: allocation.tuesday, + wednesday: allocation.wednesday, + thursday: allocation.thursday, + friday: allocation.friday, + })); + + mappedAllocations.forEach((row) => { + const totalMinutes = row.monday + row.tuesday + row.wednesday + row.thursday + row.friday; + console.log("Total Minutes:", totalMinutes); + + // Perform additional calculations or set the value as needed + // For example, set the totalMinutes value to the row + row.totalMinutes = totalMinutes; + }); + + setAllocations(mappedAllocations); + } catch (error) { + console.error("Error fetching allocations:", error); + } + }; + + const getTasks = async () => { + if (loggedInPerson) { + try { + const fetchedTasks = await tasksApi.listProjectTasks({ + projectId: 0 + }); + + const mappedTasks: Tasks[] = fetchedTasks.map((task: Tasks) => ({ + id: task.id, + title: task.title, + assignees: task.assignedPersons, + projectId: task.projectId, + priority: task.highPriority, + estimate: task.estimate, + remaining: task.remaining + })); + setTasks(mappedTasks); + } catch (error) { + console.error("Error fetching tasks:", error); + } + } + }; + + const toggleMinimizeProject = (projectId: number) => { + setMinimizedProjects((prevMinimizedProjects) => { + if (prevMinimizedProjects.includes(projectId)) { + return prevMinimizedProjects.filter((id) => id !== projectId); + } else { + return [...prevMinimizedProjects, projectId]; + } + }); + }; + + const allocationColumns = ["Project Name", "Allocation", "Time Entries", "Allocations Left"]; + const projectColumns = ["Task Title", "Assigned Persons", "Status", "High Priority", "End Date"]; + + /* const calculateTotalHours = (allocation: Allocations) => { + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + const relatedTimeEntries = mockTimeEntries.filter(entry => entry.project === row.project); + + const timeRegistered = relatedTimeEntries.reduce((total, entry) => total + entry.time_registered, 0); + const allocationsLeft = totalMinutes - timeRegistered; + + const timeRegisteredHours = Math.floor(timeRegistered / 60); + const timeRegisteredMinutes = timeRegistered % 60; + + const leftHours = Math.floor(allocationsLeft / 60); + const leftMinutes = allocationsLeft % 60; + + return { + total: totalMinutes, + timeEntries: timeRegistered, + allocationsLeft: allocationsLeft, + }; + }; */ + + const filteredTasks = (projectId: number) => { + return tasks.filter((task) => task.projectId === projectId); + }; + return ( -

{ strings.sprint.sprintviewScreen }

+ <> +

{strings.sprint.sprintviewScreen}

+ {loading ? ( +

Loading...

+ ) : ( + + + + + + {allocationColumns.map((column) => ( + {column} + ))} + + + + {allocations.map((row) => ( + + {projects.find(project => project.id === row.project)?.name} + {calculateTotalHours(row).total} + {calculateTotalHours(row).timeEntries} + {calculateTotalHours(row).allocationsLeft} + + ))} + +
+
+
+ )} + {projects.map((project) => ( + + + + + + + toggleMinimizeProject(project.id)}> + {minimizedProjects.includes(project.id) ? : } + + {project.name} + + {projectColumns.map((column) => ( + {column} + ))} + + + {!minimizedProjects.includes(project.id) && ( + + {filteredTasks(project.id).map((task) => ( + + {task.title} + {task.assignees} + {project.status} + {task.priority} + {task.remaining} + + ))} + + )} +
+
+
+ ))} + ); }; diff --git a/src/hooks/use-api.ts b/src/hooks/use-api.ts index 23de973d..950c3ee8 100644 --- a/src/hooks/use-api.ts +++ b/src/hooks/use-api.ts @@ -1,4 +1,5 @@ import { useAtomValue } from "jotai"; -import { apiClientAtom } from "../atoms/api"; +import { apiClientAtom, apiLambasAtom } from "../atoms/api"; export const useApi = () => useAtomValue(apiClientAtom); +export const useLambdasApi = () => useAtomValue(apiLambasAtom); From 6cca78c4f78279005e2333d344dbb8908857c45d Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Thu, 14 Mar 2024 14:55:53 +0200 Subject: [PATCH 03/32] a --- src/components/screens/sprint-view-screen.tsx | 312 +++++++++++------- 1 file changed, 190 insertions(+), 122 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 00a0b3dc..e7bffbc4 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import strings from "../../localization/strings"; import { Card, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; @@ -12,104 +12,144 @@ import { userProfileAtom } from "../../atoms/auth"; import { Allocations } from "../../generated/homeLambdasClient/models/Allocations"; import { Projects } from "../../generated/homeLambdasClient/models/Projects"; import { Tasks } from "../../generated/homeLambdasClient/models/Tasks"; +import { TimeEntries } from "../../generated/homeLambdasClient/models/TimeEntries"; const SprintViewScreen = () => { - const { allocationsApi, projectsApi, tasksApi } = useLambdasApi(); - const persons = useAtomValue(personsAtom); + const { allocationsApi, projectsApi, tasksApi, timeEntriesApi } = useLambdasApi(); + const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); const loggedInPerson = persons.find( (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id ); - + const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); + const [timeEntries, setTimeEntries] = useState([]); const [minimizedProjects, setMinimizedProjects] = useState([]); - + useEffect(() => { const fetchData = async () => { try { - setLoading(true); - await Promise.all([getPersonAllocations(), getProjects(), getTasks()]); - } finally { - setLoading(false); + // Fetch person allocations + const allocationsData = await getPersonAllocations(); + setAllocations(allocationsData); + + // Fetch projects + const projectsData = await getProjects(); + setProjects(projectsData); + + // Fetch tasks + const tasksData = await getTasks(); + setTasks(tasksData); + + // Fetch time entries + const timeEntriesData = await getTimeEntries(); + setTimeEntries(timeEntriesData); + } catch (error) { + console.error("Error fetching data:", error); } }; - + fetchData(); }, [loggedInPerson]); const getProjects = async () => { - if (loggedInPerson) { - try { - const fetchedProjects = await projectsApi.listProjects({ - startDate: new Date() - }); + if (!loggedInPerson) return []; + + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + }); + const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); + - const mappedProjects: Projects[] = fetchedProjects.map((project: Projects) => ({ - id: project.id, - name: project.name - })); - setProjects(mappedProjects); - } catch (error) { - console.error("Error fetching projects:", error); - } + console.log("in projects, allocations", filteredAllocations); + + const fetchedProjects = await Promise.all(filteredAllocations.map(async (allocation) => { + try { + const fetchedProjects = await projectsApi.listProjects({ + startDate: new Date() + }); + const filteredProjects = fetchedProjects.filter(project => project.id === allocation.project); + console.log("filtered projects", filteredProjects) + return filteredProjects; + } catch (error) { + console.error('Error fetching time entries for project:', error); + return []; + } + })); + + const mergedProjects = fetchedProjects.flat(); + console.log("projects in projects", mergedProjects) + setProjects(prevState => [...prevState, ...mergedProjects]); + return mergedProjects; + } catch (error) { + console.error("Error fetching projects:", error); + return []; } }; const getPersonAllocations = async () => { + if (!loggedInPerson) return []; + try { const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date(), }); - - const mappedAllocations: Allocations[] = fetchedAllocations.map((allocation) => ({ - id: allocation.id, - project: allocation.project, - person: allocation.person, - monday: allocation.monday, - tuesday: allocation.tuesday, - wednesday: allocation.wednesday, - thursday: allocation.thursday, - friday: allocation.friday, - })); - - mappedAllocations.forEach((row) => { - const totalMinutes = row.monday + row.tuesday + row.wednesday + row.thursday + row.friday; - console.log("Total Minutes:", totalMinutes); - // Perform additional calculations or set the value as needed - // For example, set the totalMinutes value to the row - row.totalMinutes = totalMinutes; - }); - - setAllocations(mappedAllocations); + const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); + console.log("in allocations", filteredAllocations); + return filteredAllocations; } catch (error) { console.error("Error fetching allocations:", error); + return []; // Ensure that an empty array is returned in case of an error } }; const getTasks = async () => { - if (loggedInPerson) { - try { - const fetchedTasks = await tasksApi.listProjectTasks({ - projectId: 0 - }); + if (!loggedInPerson) return []; + + try { + const fetchedTasks = await tasksApi.listProjectTasks({ + projectId: 0 + }); + + const filteredTasks = fetchedTasks.filter(task => task.assignedPersons.includes(loggedInPerson.id)); + + + return filteredTasks; + } catch (error) { + console.error("Error fetching tasks:", error); + return []; + } + }; - const mappedTasks: Tasks[] = fetchedTasks.map((task: Tasks) => ({ - id: task.id, - title: task.title, - assignees: task.assignedPersons, - projectId: task.projectId, - priority: task.highPriority, - estimate: task.estimate, - remaining: task.remaining - })); - setTasks(mappedTasks); - } catch (error) { - console.error("Error fetching tasks:", error); - } + const getTimeEntries = async () => { + if (!loggedInPerson || !projects.length) return []; + + try { + const timeEntriesData = await Promise.all(projects.map(async (project) => { + if (!project.id) return []; + + try { + const fetchedTimeEntries = await timeEntriesApi.listProjectTimeEntries({ + projectId: project.id + }); + + return fetchedTimeEntries; + } catch (error) { + console.error('Error fetching time entries for project', error); + return []; + } + })); + + const flattenedTimeEntries = timeEntriesData.flat(); + console.log("time entries",timeEntriesData); + return flattenedTimeEntries; + } catch (error) { + console.error("Error fetching projects:", error); + return []; } }; @@ -123,66 +163,83 @@ const SprintViewScreen = () => { }); }; - const allocationColumns = ["Project Name", "Allocation", "Time Entries", "Allocations Left"]; - const projectColumns = ["Task Title", "Assigned Persons", "Status", "High Priority", "End Date"]; - - /* const calculateTotalHours = (allocation: Allocations) => { - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; + const calculateTotalHours = useMemo( + () => (allocation: Allocations | undefined) => { + if (!allocation) { + return { + total: '0h 0min', + timeEntries: '0h 0min', + allocationsLeft: '0h 0min', + }; + } - const relatedTimeEntries = mockTimeEntries.filter(entry => entry.project === row.project); - - const timeRegistered = relatedTimeEntries.reduce((total, entry) => total + entry.time_registered, 0); - const allocationsLeft = totalMinutes - timeRegistered; + const totalMinutes = + (allocation.monday || 0) + + (allocation.tuesday || 0) + + (allocation.wednesday || 0) + + (allocation.thursday || 0) + + (allocation.friday || 0); - const timeRegisteredHours = Math.floor(timeRegistered / 60); - const timeRegisteredMinutes = timeRegistered % 60; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; - const leftHours = Math.floor(allocationsLeft / 60); - const leftMinutes = allocationsLeft % 60; + const relevantTimeEntries = timeEntries.filter( + (entry) => entry.project === allocation.project + ); - return { - total: totalMinutes, - timeEntries: timeRegistered, - allocationsLeft: allocationsLeft, - }; - }; */ + const timeRegistered = relevantTimeEntries.reduce((total, entry) => total + entry.timeRegistered, 0); + + const allocationsLeft = totalMinutes - timeRegistered; + const leftHours = Math.floor(allocationsLeft / 60); + const leftMinutes = allocationsLeft % 60; + + return { + total: `${hours}h ${minutes}min`, + timeEntries: `${Math.floor(timeRegistered / 60)}h ${ + timeRegistered % 60 + }min`, + allocationsLeft: `${leftHours}h ${leftMinutes}min`, + }; + }, + [timeEntries] + ); const filteredTasks = (projectId: number) => { return tasks.filter((task) => task.projectId === projectId); }; + const filteredTimeEntries = (projectId: number) => { + return timeEntries.filter(entry => entry.project === projectId); + }; + return ( <>

{strings.sprint.sprintviewScreen}

- {loading ? ( -

Loading...

- ) : ( - - - - - - {allocationColumns.map((column) => ( - {column} - ))} + + + +
+ + + My allocations + Allocation + Time Entries + Allocations Left + + + + {allocations.map((row) => ( + + {projects.find(project => project.id === row.project)?.name} + {calculateTotalHours(row).total} + {filteredTimeEntries(row.project).reduce((acc, entry) => acc + entry.timeRegistered, 0)} + {calculateTotalHours(row).allocationsLeft} - - - {allocations.map((row) => ( - - {projects.find(project => project.id === row.project)?.name} - {calculateTotalHours(row).total} - {calculateTotalHours(row).timeEntries} - {calculateTotalHours(row).allocationsLeft} - - ))} - -
-
-
- )} + ))} + + + + {projects.map((project) => ( @@ -195,22 +252,33 @@ const SprintViewScreen = () => { {project.name} - {projectColumns.map((column) => ( - {column} - ))} + Assignees + Status + Priority + Estimate + Time Entries {!minimizedProjects.includes(project.id) && ( - {filteredTasks(project.id).map((task) => ( - - {task.title} - {task.assignees} - {project.status} - {task.priority} - {task.remaining} - - ))} + {filteredTasks(project.id).map((task) => { + const relevantTimeEntries = filteredTimeEntries(project.id); + const relevantTimeEntry = relevantTimeEntries.find(entry => entry.task === task.id); + const estimateHours = Math.floor(task.estimate / 60); + const estimateMinutes = task.estimate % 60; + const formattedEstimate = `${estimateHours}h ${estimateMinutes}min`; + const timeRegistered = relevantTimeEntry ? relevantTimeEntry.timeRegistered : '-'; + return ( + + {task.title} + {task.assignedPersons} + {project.status} + {task.highPriority} + {formattedEstimate} + {timeRegistered} + + ); + })} )} From 0442480861460b4b290948150459e6b53533313c Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Thu, 21 Mar 2024 12:48:12 +0200 Subject: [PATCH 04/32] added more changes --- src/components/screens/sprint-view-screen.tsx | 271 ++++++++---------- 1 file changed, 112 insertions(+), 159 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index e7bffbc4..2af74f92 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import strings from "../../localization/strings"; -import { Card, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from '@mui/material'; +import { Card, IconButton } from '@mui/material'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import { useLambdasApi } from "../../hooks/use-api"; @@ -13,6 +13,7 @@ import { Allocations } from "../../generated/homeLambdasClient/models/Allocation import { Projects } from "../../generated/homeLambdasClient/models/Projects"; import { Tasks } from "../../generated/homeLambdasClient/models/Tasks"; import { TimeEntries } from "../../generated/homeLambdasClient/models/TimeEntries"; +import { DataGrid } from '@mui/x-data-grid'; const SprintViewScreen = () => { const { allocationsApi, projectsApi, tasksApi, timeEntriesApi } = useLambdasApi(); @@ -46,6 +47,7 @@ const SprintViewScreen = () => { // Fetch time entries const timeEntriesData = await getTimeEntries(); setTimeEntries(timeEntriesData); + //allocations.forEach(row => calculateTotalHours(row)); } catch (error) { console.error("Error fetching data:", error); } @@ -54,39 +56,32 @@ const SprintViewScreen = () => { fetchData(); }, [loggedInPerson]); + const handleError = (error: any, message: string) => { + console.error(message, error); + return []; + }; + const getProjects = async () => { if (!loggedInPerson) return []; - + try { - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - }); + const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() }); const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - - console.log("in projects, allocations", filteredAllocations); - const fetchedProjects = await Promise.all(filteredAllocations.map(async (allocation) => { try { - const fetchedProjects = await projectsApi.listProjects({ - startDate: new Date() - }); - const filteredProjects = fetchedProjects.filter(project => project.id === allocation.project); - console.log("filtered projects", filteredProjects) - return filteredProjects; + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + return fetchedProjects.filter(project => project.id === allocation.project); } catch (error) { - console.error('Error fetching time entries for project:', error); - return []; + return handleError(error, 'Error fetching time entries for project:'); } })); - - const mergedProjects = fetchedProjects.flat(); - console.log("projects in projects", mergedProjects) + + const mergedProjects = fetchedProjects.flatMap(project => project); setProjects(prevState => [...prevState, ...mergedProjects]); return mergedProjects; } catch (error) { - console.error("Error fetching projects:", error); - return []; + return handleError(error, "Error fetching projects:"); } }; @@ -99,7 +94,6 @@ const SprintViewScreen = () => { }); const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - console.log("in allocations", filteredAllocations); return filteredAllocations; } catch (error) { console.error("Error fetching allocations:", error); @@ -115,10 +109,7 @@ const SprintViewScreen = () => { projectId: 0 }); - const filteredTasks = fetchedTasks.filter(task => task.assignedPersons.includes(loggedInPerson.id)); - - - return filteredTasks; + return fetchedTasks.filter(task => task.assignedPersons.includes(loggedInPerson.id)); } catch (error) { console.error("Error fetching tasks:", error); return []; @@ -126,165 +117,127 @@ const SprintViewScreen = () => { }; const getTimeEntries = async () => { - if (!loggedInPerson || !projects.length) return []; + if (!loggedInPerson) return []; try { - const timeEntriesData = await Promise.all(projects.map(async (project) => { - if (!project.id) return []; + const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() }); + const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); + const fetchedTimeEntries = await Promise.all(filteredAllocations.map(async (allocation) => { try { - const fetchedTimeEntries = await timeEntriesApi.listProjectTimeEntries({ - projectId: project.id - }); - + const fetchedTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project }); return fetchedTimeEntries; } catch (error) { - console.error('Error fetching time entries for project', error); - return []; + throw new Error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); } })); - - const flattenedTimeEntries = timeEntriesData.flat(); - console.log("time entries",timeEntriesData); - return flattenedTimeEntries; + + const mergedEntries = fetchedTimeEntries.flatMap(entries => entries); + return mergedEntries; } catch (error) { - console.error("Error fetching projects:", error); - return []; + throw new Error(`Error fetching time entries: ${error}`); } }; + + const toggleMinimizeProject = (projectId: number) => { - setMinimizedProjects((prevMinimizedProjects) => { - if (prevMinimizedProjects.includes(projectId)) { - return prevMinimizedProjects.filter((id) => id !== projectId); - } else { - return [...prevMinimizedProjects, projectId]; - } - }); + setMinimizedProjects(prevMinimizedProjects => + prevMinimizedProjects.includes(projectId) + ? prevMinimizedProjects.filter(id => id !== projectId) + : [...prevMinimizedProjects, projectId] + ); }; - const calculateTotalHours = useMemo( - () => (allocation: Allocations | undefined) => { - if (!allocation) { - return { - total: '0h 0min', - timeEntries: '0h 0min', - allocationsLeft: '0h 0min', - }; - } - - const totalMinutes = - (allocation.monday || 0) + - (allocation.tuesday || 0) + - (allocation.wednesday || 0) + - (allocation.thursday || 0) + - (allocation.friday || 0); - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - const relevantTimeEntries = timeEntries.filter( - (entry) => entry.project === allocation.project - ); - - const timeRegistered = relevantTimeEntries.reduce((total, entry) => total + entry.timeRegistered, 0); - - const allocationsLeft = totalMinutes - timeRegistered; - const leftHours = Math.floor(allocationsLeft / 60); - const leftMinutes = allocationsLeft % 60; - - return { - total: `${hours}h ${minutes}min`, - timeEntries: `${Math.floor(timeRegistered / 60)}h ${ - timeRegistered % 60 - }min`, - allocationsLeft: `${leftHours}h ${leftMinutes}min`, - }; - }, - [timeEntries] - ); + const calculateTotalHours = useMemo(() => (allocation: Allocations, estimate: number) => { + const totalMinutes = + (allocation.monday || 0) + + (allocation.tuesday || 0) + + (allocation.wednesday || 0) + + (allocation.thursday || 0) + + (allocation.friday || 0); + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + const relevantTimeEntries = timeEntries.filter(entry => entry.project === allocation.project); + const timeRegistered = relevantTimeEntries.reduce((total, entry) => total + entry.timeRegistered, 0); + + const allocationsLeft = totalMinutes - timeRegistered; + const leftHours = Math.floor(allocationsLeft / 60); + const leftMinutes = allocationsLeft % 60; + + const estimateHours = Math.floor(estimate / 60); + const estimateMinutes = estimate % 60; + + return { + total: `${hours}h ${minutes}min`, + timeEntries: `${Math.floor(timeRegistered / 60)}h ${timeRegistered % 60}min`, + allocationsLeft: `${leftHours}h ${leftMinutes}min`, + estimate: `${estimateHours}h ${estimateMinutes}min` + }; + }, [timeEntries]); const filteredTasks = (projectId: number) => { return tasks.filter((task) => task.projectId === projectId); }; - const filteredTimeEntries = (projectId: number) => { - return timeEntries.filter(entry => entry.project === projectId); - }; - return ( <>

{strings.sprint.sprintviewScreen}

- + - - - - - My allocations - Allocation - Time Entries - Allocations Left - - - - {allocations.map((row) => ( - - {projects.find(project => project.id === row.project)?.name} - {calculateTotalHours(row).total} - {filteredTimeEntries(row.project).reduce((acc, entry) => acc + entry.timeRegistered, 0)} - {calculateTotalHours(row).allocationsLeft} - - ))} - -
-
+ projects.find(project => project.id === params.row.project)?.name }, + { field: 'allocation', headerName: 'Allocation', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).total }, + { field: 'timeEntries', headerName: 'Time Entries', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).timeEntries }, + { field: 'allocationsLeft', headerName: 'Allocations Left', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).allocationsLeft }, + ]} + />
+ {projects.map((project) => ( - - - - - - - toggleMinimizeProject(project.id)}> - {minimizedProjects.includes(project.id) ? : } - - {project.name} - - Assignees - Status - Priority - Estimate - Time Entries - - - {!minimizedProjects.includes(project.id) && ( - - {filteredTasks(project.id).map((task) => { - const relevantTimeEntries = filteredTimeEntries(project.id); - const relevantTimeEntry = relevantTimeEntries.find(entry => entry.task === task.id); - const estimateHours = Math.floor(task.estimate / 60); - const estimateMinutes = task.estimate % 60; - const formattedEstimate = `${estimateHours}h ${estimateMinutes}min`; - const timeRegistered = relevantTimeEntry ? relevantTimeEntry.timeRegistered : '-'; - return ( - - {task.title} - {task.assignedPersons} - {project.status} - {task.highPriority} - {formattedEstimate} - {timeRegistered} - - ); - })} - - )} -
-
-
- ))} + + toggleMinimizeProject(project.id)}> + {minimizedProjects.includes(project.id) ? : } + + {project.name} + {!minimizedProjects.includes(project.id) && ( + ({ + id: task.id, + title: task.title, + assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', + status: project.status, + priority: task.highPriority ? 'High' : 'Normal', + estimate: calculateTotalHours(task, task.estimate).estimate, + timeEntries: (() => { + const totalMinutes = timeEntries + .filter(entry => entry.task === task.id) + .map(entry => entry.timeRegistered) + .reduce((total, time) => total + time, 0); + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return `${hours}h ${minutes}min`; + })(), + }))} + columns={[ + { field: 'title', headerName: 'Task Title', flex: 1 }, + { field: 'assignedPersons', headerName: 'Assignees', flex: 1 }, + { field: 'status', headerName: 'Status', flex: 1 }, + { field: 'priority', headerName: 'Priority', flex: 1 }, + { field: 'estimate', headerName: 'Estimate', flex: 1 }, + { field: 'timeEntries', headerName: 'Time Entries', flex: 1 }, + ]} + headerClassName={{ backgroundColor: '#e0e0e0', fontWeight: 'bold' }} + /> + )} + +))} ); }; From 4bd897fcae888ac5f8ab3eff85747a851965f0fb Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Fri, 22 Mar 2024 11:51:18 +0200 Subject: [PATCH 05/32] added a few corrections --- src/components/screens/sprint-view-screen.tsx | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 2af74f92..9cebea6f 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -186,11 +186,11 @@ const SprintViewScreen = () => { <>

{strings.sprint.sprintviewScreen}

- + projects.find(project => project.id === params.row.project)?.name }, + { field: 'projectName', headerName: 'My allocations', flex: 2, valueGetter: (params) => projects.find(project => project.id === params.row.project)?.name }, { field: 'allocation', headerName: 'Allocation', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).total }, { field: 'timeEntries', headerName: 'Time Entries', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).timeEntries }, { field: 'allocationsLeft', headerName: 'Allocations Left', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).allocationsLeft }, @@ -199,45 +199,44 @@ const SprintViewScreen = () => { {projects.map((project) => ( - - toggleMinimizeProject(project.id)}> - {minimizedProjects.includes(project.id) ? : } - - {project.name} - {!minimizedProjects.includes(project.id) && ( - ({ - id: task.id, - title: task.title, - assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', - status: project.status, - priority: task.highPriority ? 'High' : 'Normal', - estimate: calculateTotalHours(task, task.estimate).estimate, - timeEntries: (() => { - const totalMinutes = timeEntries - .filter(entry => entry.task === task.id) - .map(entry => entry.timeRegistered) - .reduce((total, time) => total + time, 0); - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - return `${hours}h ${minutes}min`; - })(), - }))} - columns={[ - { field: 'title', headerName: 'Task Title', flex: 1 }, - { field: 'assignedPersons', headerName: 'Assignees', flex: 1 }, - { field: 'status', headerName: 'Status', flex: 1 }, - { field: 'priority', headerName: 'Priority', flex: 1 }, - { field: 'estimate', headerName: 'Estimate', flex: 1 }, - { field: 'timeEntries', headerName: 'Time Entries', flex: 1 }, - ]} - headerClassName={{ backgroundColor: '#e0e0e0', fontWeight: 'bold' }} - /> - )} - -))} + + toggleMinimizeProject(project.id)}> + {minimizedProjects.includes(project.id) ? : } + + {project.name} + {minimizedProjects.includes(project.id) && ( + ({ + id: task.id, + title: task.title, + assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', + status: project.status, + priority: task.highPriority ? 'High' : 'Normal', + estimate: calculateTotalHours(task, task.estimate).estimate, + timeEntries: (() => { + const totalMinutes = timeEntries + .filter(entry => entry.task === task.id) + .map(entry => entry.timeRegistered) + .reduce((total, time) => total + time, 0); + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return `${hours}h ${minutes}min`; + })(), + }))} + columns={[ + { field: 'title', headerName: project.name, flex: 3 }, + { field: 'assignedPersons', headerName: 'Assignees', flex: 1 }, + { field: 'status', headerName: 'Status', flex: 1 }, + { field: 'priority', headerName: 'Priority', flex: 1 }, + { field: 'estimate', headerName: 'Estimate', flex: 1 }, + { field: 'timeEntries', headerName: 'Time Entries', flex: 1 }, + ]} + /> + )} + + ))} ); }; From 1fad29655596859dbaec4297d09af28343f82437 Mon Sep 17 00:00:00 2001 From: yuusufisse Date: Wed, 3 Apr 2024 13:15:41 +0300 Subject: [PATCH 06/32] added a few changes --- src/components/screens/sprint-view-screen.tsx | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 9cebea6f..4e2036ee 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -47,7 +47,6 @@ const SprintViewScreen = () => { // Fetch time entries const timeEntriesData = await getTimeEntries(); setTimeEntries(timeEntriesData); - //allocations.forEach(row => calculateTotalHours(row)); } catch (error) { console.error("Error fetching data:", error); } @@ -93,8 +92,7 @@ const SprintViewScreen = () => { startDate: new Date(), }); - const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - return filteredAllocations; + return fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); } catch (error) { console.error("Error fetching allocations:", error); return []; // Ensure that an empty array is returned in case of an error @@ -125,15 +123,13 @@ const SprintViewScreen = () => { const fetchedTimeEntries = await Promise.all(filteredAllocations.map(async (allocation) => { try { - const fetchedTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project }); - return fetchedTimeEntries; + return await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project }); } catch (error) { throw new Error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); } })); - const mergedEntries = fetchedTimeEntries.flatMap(entries => entries); - return mergedEntries; + return fetchedTimeEntries.flatMap(entries => entries); } catch (error) { throw new Error(`Error fetching time entries: ${error}`); } @@ -198,45 +194,49 @@ const SprintViewScreen = () => { /> - {projects.map((project) => ( - - toggleMinimizeProject(project.id)}> - {minimizedProjects.includes(project.id) ? : } - - {project.name} - {minimizedProjects.includes(project.id) && ( - ({ - id: task.id, - title: task.title, - assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', - status: project.status, - priority: task.highPriority ? 'High' : 'Normal', - estimate: calculateTotalHours(task, task.estimate).estimate, - timeEntries: (() => { - const totalMinutes = timeEntries - .filter(entry => entry.task === task.id) - .map(entry => entry.timeRegistered) - .reduce((total, time) => total + time, 0); - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - return `${hours}h ${minutes}min`; - })(), - }))} - columns={[ - { field: 'title', headerName: project.name, flex: 3 }, - { field: 'assignedPersons', headerName: 'Assignees', flex: 1 }, - { field: 'status', headerName: 'Status', flex: 1 }, - { field: 'priority', headerName: 'Priority', flex: 1 }, - { field: 'estimate', headerName: 'Estimate', flex: 1 }, - { field: 'timeEntries', headerName: 'Time Entries', flex: 1 }, - ]} - /> - )} - - ))} + {projects.map((project) => { + const timeEntriesData = filteredTasks(project.id).map(task => { + const totalMinutes = timeEntries + .filter(entry => entry.task === task.id) + .map(entry => entry.timeRegistered) + .reduce((total, time) => total + time, 0); + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + return { + id: task.id, + title: task.title, + assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', + status: project.status, + priority: task.highPriority ? 'High' : 'Normal', + estimate: calculateTotalHours(task, task.estimate).estimate, + timeEntries: `${hours}h ${minutes}min`, + }; + }); + + return ( + + toggleMinimizeProject(project.id)}> + {minimizedProjects.includes(project.id) ? : } + + {project.name} + {minimizedProjects.includes(project.id) && ( + + )} + + ); + })} ); }; From ae560aacb0bce07c7404757625e7cf51cb8790d3 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Wed, 10 Apr 2024 17:09:07 +0300 Subject: [PATCH 07/32] Added styling and logic refactoring --- src/components/screens/sprint-view-screen.tsx | 410 ++++++++++-------- .../sprint-view-table/tasks-table.tsx | 156 +++++++ src/localization/en.json | 18 +- src/localization/fi.json | 18 +- src/localization/strings.ts | 18 +- 5 files changed, 426 insertions(+), 194 deletions(-) create mode 100644 src/components/sprint-view-table/tasks-table.tsx diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 4e2036ee..c2ef8e4b 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,8 +1,5 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import strings from "../../localization/strings"; -import { Card, IconButton } from '@mui/material'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { useState, useEffect } from 'react'; +import { Card,FormControl, InputLabel, MenuItem, Select, CircularProgress, Typography, Box} from '@mui/material'; import { useLambdasApi } from "../../hooks/use-api"; import { Person } from "../../generated/client"; import { useAtomValue } from "jotai"; @@ -11,232 +8,263 @@ import config from "../../app/config"; import { userProfileAtom } from "../../atoms/auth"; import { Allocations } from "../../generated/homeLambdasClient/models/Allocations"; import { Projects } from "../../generated/homeLambdasClient/models/Projects"; -import { Tasks } from "../../generated/homeLambdasClient/models/Tasks"; import { TimeEntries } from "../../generated/homeLambdasClient/models/TimeEntries"; import { DataGrid } from '@mui/x-data-grid'; +import { getHoursAndMinutes } from '../../utils/time-utils'; +import TaskTable from '../sprint-view-table/tasks-table'; +import strings from "../../localization/strings"; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; +/** + * Sprint view screen component + */ const SprintViewScreen = () => { - const { allocationsApi, projectsApi, tasksApi, timeEntriesApi } = useLambdasApi(); + const { allocationsApi, projectsApi, timeEntriesApi } = useLambdasApi(); const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); const loggedInPerson = persons.find( (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id ); - const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); - const [tasks, setTasks] = useState([]); - const [timeEntries, setTimeEntries] = useState([]); - const [minimizedProjects, setMinimizedProjects] = useState([]); + const [timeEntries, setTimeEntries] = useState([]); + const [loading, setLoading] = useState(false); + const [myTasks, showMyTasks] = useState(true); + const [filter, setFilter] = useState(""); + /** + * Get project data if user is logged in otherwise endless loading + */ useEffect(() => { + setLoading(true); const fetchData = async () => { - try { - // Fetch person allocations - const allocationsData = await getPersonAllocations(); - setAllocations(allocationsData); - - // Fetch projects - const projectsData = await getProjects(); - setProjects(projectsData); - - // Fetch tasks - const tasksData = await getTasks(); - setTasks(tasksData); - - // Fetch time entries - const timeEntriesData = await getTimeEntries(); - setTimeEntries(timeEntriesData); - } catch (error) { - console.error("Error fetching data:", error); - } + await getPersonAllocations(); + await getProjects(); + await getTimeEntries(); + setLoading(false); }; - - fetchData(); + if (loggedInPerson) { + fetchData(); + } }, [loggedInPerson]); - const handleError = (error: any, message: string) => { - console.error(message, error); - return []; - }; - + /** + * Get project allocations for logged in user + */ const getProjects = async () => { - if (!loggedInPerson) return []; - - try { - const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() }); - const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - - const fetchedProjects = await Promise.all(filteredAllocations.map(async (allocation) => { - try { - const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - return fetchedProjects.filter(project => project.id === allocation.project); - } catch (error) { - return handleError(error, 'Error fetching time entries for project:'); - } - })); - - const mergedProjects = fetchedProjects.flatMap(project => project); - setProjects(prevState => [...prevState, ...mergedProjects]); - return mergedProjects; - } catch (error) { - return handleError(error, "Error fetching projects:"); + if (loggedInPerson) { + const projects : Projects[] = []; + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + personId: loggedInPerson.id.toString()}); + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + fetchedAllocations.forEach((allocation)=> { + const projectFound = fetchedProjects.find((project)=> project.id === allocation.project); + projectFound && projects.push(projectFound); + }) + setProjects(projects); + } catch (error) { + console.error(error, "Error fetching projects:"); + } } }; - const getPersonAllocations = async () => { - if (!loggedInPerson) return []; - - try { - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - }); - - return fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - } catch (error) { - console.error("Error fetching allocations:", error); - return []; // Ensure that an empty array is returned in case of an error - } - }; - - const getTasks = async () => { - if (!loggedInPerson) return []; - - try { - const fetchedTasks = await tasksApi.listProjectTasks({ - projectId: 0 - }); - - return fetchedTasks.filter(task => task.assignedPersons.includes(loggedInPerson.id)); - } catch (error) { - console.error("Error fetching tasks:", error); - return []; + if (loggedInPerson) { + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + personId: loggedInPerson.id.toString() + }); + setAllocations(fetchedAllocations); + } catch (error) { + console.error("Error fetching allocations:", error); + } } }; + /** + * Get project time entries for logged in user + */ const getTimeEntries = async () => { - if (!loggedInPerson) return []; - - try { - const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() }); - const filteredAllocations = fetchedAllocations.filter(allocation => allocation.person === loggedInPerson.id); - - const fetchedTimeEntries = await Promise.all(filteredAllocations.map(async (allocation) => { - try { - return await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project }); - } catch (error) { - throw new Error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); - } - })); - - return fetchedTimeEntries.flatMap(entries => entries); - } catch (error) { - throw new Error(`Error fetching time entries: ${error}`); + if (loggedInPerson) { + try { + const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() , personId: loggedInPerson.id.toString()}); + const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { + try { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0}); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries)=> { + if (loggedInPerson && timeEntry.person===loggedInPerson.id) { + totalMinutes+=(timeEntry.timeRegistered || 0) + }}) + return totalMinutes; + } catch (error) { + console.error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); + return 0; + } + })); + setTimeEntries(fetchedTimeEntries); + } catch (error) { + console.error(`Error fetching time entries: ${error}`); + } } }; - - - const toggleMinimizeProject = (projectId: number) => { - setMinimizedProjects(prevMinimizedProjects => - prevMinimizedProjects.includes(projectId) - ? prevMinimizedProjects.filter(id => id !== projectId) - : [...prevMinimizedProjects, projectId] - ); - }; - - const calculateTotalHours = useMemo(() => (allocation: Allocations, estimate: number) => { + /** + * Calculate project total time spent on the project by the user + */ + const totalAllocations = (allocation: Allocations) => { const totalMinutes = (allocation.monday || 0) + (allocation.tuesday || 0) + (allocation.wednesday || 0) + (allocation.thursday || 0) + (allocation.friday || 0); - - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - const relevantTimeEntries = timeEntries.filter(entry => entry.project === allocation.project); - const timeRegistered = relevantTimeEntries.reduce((total, entry) => total + entry.timeRegistered, 0); - - const allocationsLeft = totalMinutes - timeRegistered; - const leftHours = Math.floor(allocationsLeft / 60); - const leftMinutes = allocationsLeft % 60; - - const estimateHours = Math.floor(estimate / 60); - const estimateMinutes = estimate % 60; - - return { - total: `${hours}h ${minutes}min`, - timeEntries: `${Math.floor(timeRegistered / 60)}h ${timeRegistered % 60}min`, - allocationsLeft: `${leftHours}h ${leftMinutes}min`, - estimate: `${estimateHours}h ${estimateMinutes}min` - }; - }, [timeEntries]); + return totalMinutes * 2; + } - const filteredTasks = (projectId: number) => { - return tasks.filter((task) => task.projectId === projectId); - }; + /** + * Get total time required to complete the project + */ + const getTotalTimeEntries = (allocation: Allocations) => { + if (timeEntries.length!==0) { + return timeEntries[allocations.indexOf(allocation)]; + } + return 0; + } - return ( - <> -

{strings.sprint.sprintviewScreen}

- - - projects.find(project => project.id === params.row.project)?.name }, - { field: 'allocation', headerName: 'Allocation', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).total }, - { field: 'timeEntries', headerName: 'Time Entries', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).timeEntries }, - { field: 'allocationsLeft', headerName: 'Allocations Left', flex: 1, valueGetter: (params) => calculateTotalHours(params.row).allocationsLeft }, - ]} - /> - - - {projects.map((project) => { - const timeEntriesData = filteredTasks(project.id).map(task => { - const totalMinutes = timeEntries - .filter(entry => entry.task === task.id) - .map(entry => entry.timeRegistered) - .reduce((total, time) => total + time, 0); + /** + * Calculate the remaining time of project completion + */ + const timeLeft = (allocation: Allocations) => { + return totalAllocations(allocation) - (getTotalTimeEntries(allocation) || 0) + } - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; + /** + * Get project name + */ + const getProjectName = (allocation: Allocations) => { + if (projects.length !== 0) { + return projects[allocations.indexOf(allocation)]?.name; + } + return ""; + } - return { - id: task.id, - title: task.title, - assignedPersons: task.assignedPersons ? task.assignedPersons.join(', ') : '-', - status: project.status, - priority: task.highPriority ? 'High' : 'Normal', - estimate: calculateTotalHours(task, task.estimate).estimate, - timeEntries: `${hours}h ${minutes}min`, - }; - }); + /** + * Get project color + */ + const getProjectColor = (allocation: Allocations) => { + if (projects.length !== 0) { + return projects[allocations.indexOf(allocation)]?.color; + } + return ""; + } - return ( - - toggleMinimizeProject(project.id)}> - {minimizedProjects.includes(project.id) ? : } - - {project.name} - {minimizedProjects.includes(project.id) && ( - - )} + /** + * Calculate total unallocated time for the user + */ + const unallocatedTime = (allocation: Allocations[]) => { + const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); + const calculateWorkingLoad = (person?: Person) => { + if (!person) { + return 0; + } + const totalMinutes = + (person.monday || 0) + + (person.tuesday || 0) + + (person.wednesday || 0) + + (person.thursday || 0) + + (person.friday || 0); + return totalMinutes * 2; + } + return calculateWorkingLoad(loggedInPerson) - totalAllocatedTime; + } + + /** + * Featute for task filtering + */ + const handleOnClick = () => { + showMyTasks(!myTasks); + setFilter(""); + } + + return ( + <> + {loading || !loggedInPerson ? ( + + + + {strings.placeHolder.pleaseWait} + + + + ) : ( + <> + } label={strings.sprint.showMyTasks} onClick={() => handleOnClick()}/> + + {strings.sprint.taskStatus} + + + + getProjectName(params.row), + renderCell:(params) => <>{getProjectName(params.row)} + }, + { + field: 'allocation', + headerName: strings.sprint.allocation, + flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) + }, + { + field: 'timeEntries', + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row) || 0), + }, + { + field: 'allocationsLeft', + headerName: strings.sprint.allocationLeft, + flex: 1, cellClassName: (params) => timeLeft(params.row) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row)) + }, + ]} + /> + + {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} + - ); - })} + {projects.map((project) => { + return ( + + ) + } + )} + + )} ); }; diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx new file mode 100644 index 00000000..58805492 --- /dev/null +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -0,0 +1,156 @@ +import { Card, IconButton } from "@mui/material"; +import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; +import { DataGrid } from "@mui/x-data-grid"; +import { useEffect, useState } from "react"; +import { useLambdasApi } from "../../hooks/use-api"; +import { getHoursAndMinutes } from "../../utils/time-utils"; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import strings from "../../localization/strings"; + +/** + * Component properties + */ +interface Props { + project : Projects, + loggedInpersonId?: number, + filter?: string +} + +/** + * Task component + */ +const TaskTable = ({project, loggedInpersonId, filter}: Props) => { + const { tasksApi, timeEntriesApi } = useLambdasApi(); + const [tasks, setTasks] = useState([]); + const [timeEntries, setTimeEntries] = useState([]); + const [open, setOpen] = useState(false); + +/** + * Gather tasks and time entries when project is available and update changes appeared + */ + useEffect(() => { + const fetchData = async () => { + if (project) { + await getTasksAndTimeEntries(); + } + }; + fetchData(); + }, [project, loggedInpersonId, filter]); + +/** + * Get total time entries for the task + */ + const getTasksAndTimeEntries = async () => { + try { + let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); + if (loggedInpersonId) { + fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); + } + setTasks(fetchedTasks); + /** + * Fetch time entries for each task + */ + const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { + try { + if (project.id ){ + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id,taskId: task.id });// + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries)=> { + if (loggedInpersonId && timeEntry.person===loggedInpersonId) { + totalMinutes+=(timeEntry.timeRegistered || 0) + } + if (!loggedInpersonId) { + totalMinutes += timeEntry.timeRegistered || 0; + } + }) + return totalMinutes; + } + return 0; + } catch (error) { + console.error(`Error fetching time entries for allocation ${task.id}: ${error}`); + return 0; + } + })); + setTimeEntries(fetchedTimeEntries); + } catch (error) { + console.error(`Error fetching time entries: ${error}`); + } + }; + +/** + * Retrieve total time entries for a task + */ + const getTotalTimeEntries = (task: Tasks) => { + if (timeEntries.length!==0) { + return timeEntries[tasks.indexOf(task)]; + } + return 0; + } + + return ( + + setOpen(!open)}> + {open ? : } + + {project ? project.name : ""} + {open && tasks.length !== 0 && + getTotalTimeEntries(params.row)!==0 ? "In progress" : "On hold" + }, + { + field: 'priority', + headerName: strings.sprint.taskPriority, + cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', + flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' + }, + { field: 'estimate', + headerName: strings.sprint.estimatedTime, + flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + }, + { + field: 'timeEntries', + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) + } + ]} + /> + } + + ) +} + +export default TaskTable; \ No newline at end of file diff --git a/src/localization/en.json b/src/localization/en.json index 3f2c7870..dd1f268d 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -59,7 +59,23 @@ }, "sprint": { "sprintview": "Sprint view", - "sprintviewScreen": "Sprint view Screen" + "myAllocation": "My allocations", + "allocation": "Allocation", + "timeEntries": "Time Entries", + "allocationLeft": "Allocations Left", + "assigned": "Assignee", + "taskStatus": "Status", + "taskPriority": "Priority", + "estimatedTime": "Estimate", + "taskName": "Tasks", + "showMyTasks": "My tasks", + "onHold": "On hold", + "inProgress": "In progress", + "allTasks" : "All tasks", + "notFound" : "No result found", + "projectName": "Project", + "search": "Search for a task, project or assignee", + "unAllocated": "Unallocated Time:" }, "errors": { "fetchFailedGeneral": "There was an error fetching data", diff --git a/src/localization/fi.json b/src/localization/fi.json index a6753ece..dfe6d1d0 100644 --- a/src/localization/fi.json +++ b/src/localization/fi.json @@ -59,7 +59,23 @@ }, "sprint": { "sprintview": "Sprintinäkymä", - "sprintviewScreen": "Sprintinäkymä näyttö" + "myAllocation": "Minun allokointi", + "allocation": "Allokointi ", + "timeEntries": "Aikamerkinnät", + "allocationLeft": "Jäljellä oleva alkoatio", + "assigned": "Luovutuksensaaja", + "taskStatus": "Tila", + "taskPriority": "Prioriteetti", + "estimatedTime": "Arvio", + "taskName": "Tehtävä", + "showMyTasks": "Tehtäväni ", + "onHold": "Odotuksessa", + "inProgress": "Käynnissä", + "allTasks" : "Kaikki tehtävät", + "notFound" : "Tulosta ei löytynyt", + "projectName": "Projekti", + "search": "Hae tehtävää, projektia tai vastaanottajaa", + "unAllocated": "Kohdistamaton aika:" }, "errors": { "fetchFailedGeneral": "Tietojen hakeminen epäonnistui", diff --git a/src/localization/strings.ts b/src/localization/strings.ts index 7551ecb2..99493765 100644 --- a/src/localization/strings.ts +++ b/src/localization/strings.ts @@ -85,7 +85,23 @@ export interface Localized extends LocalizedStringsMethods { */ sprint: { sprintview: string; - sprintviewScreen: string + myAllocation: string, + allocation: string, + timeEntries: string, + allocationLeft: string, + assigned: string, + taskStatus: string, + taskPriority: string, + estimatedTime: string, + taskName: string, + showMyTasks: string, + onHold: string, + inProgress: string, + allTasks: string, + notFound: string, + projectName: string, + search: string, + unAllocated: string }; /** * General time-related expressions From 8b363f01d810f0d2a3f1fb26e405ef27b8727371 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Wed, 10 Apr 2024 20:16:27 +0300 Subject: [PATCH 08/32] added key prop and usestate value+setter pair --- src/components/screens/sprint-view-screen.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index e86cd64f..a7ed04c6 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -30,7 +30,7 @@ const SprintViewScreen = () => { const [projects, setProjects] = useState([]); const [timeEntries, setTimeEntries] = useState([]); const [loading, setLoading] = useState(false); - const [myTasks, showMyTasks] = useState(true); + const [myTasks, setMyTasks] = useState(true); const [filter, setFilter] = useState(""); /** @@ -186,7 +186,7 @@ const SprintViewScreen = () => { * Featute for task filtering */ const handleOnClick = () => { - showMyTasks(!myTasks); + setMyTasks(!myTasks); setFilter(""); } @@ -257,9 +257,9 @@ const SprintViewScreen = () => { {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} - {projects.map((project) => { + {projects.map((project, index) => { return ( - + ) } )} From 1fc07c2762c46bbd2ba941b131a507f7d04e1778 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Wed, 10 Apr 2024 20:22:59 +0300 Subject: [PATCH 09/32] added new key prop --- src/components/screens/sprint-view-screen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index a7ed04c6..06725d8a 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -257,9 +257,9 @@ const SprintViewScreen = () => { {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))}
- {projects.map((project, index) => { + {projects.map((project) => { return ( - + ) } )} From 6db188b1b17b5c2166d49a50cbce81bc2ed38139 Mon Sep 17 00:00:00 2001 From: Jdallos Date: Thu, 11 Apr 2024 11:44:55 +0300 Subject: [PATCH 10/32] Update spec --- home-lambdas-API-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index ebb3535c..51a0ccc1 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit ebb3535c35bcd1b3f0f9317a09ebc97a15162fe2 +Subproject commit 51a0ccc1d66a6935584356e69b4c67f12a2ae00c From 6f573557380df38f6b5bd976d7dd652a98c5e62d Mon Sep 17 00:00:00 2001 From: DZotoff Date: Sun, 14 Apr 2024 18:20:27 +0300 Subject: [PATCH 11/32] added new spec --- home-lambdas-API-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index 51a0ccc1..37138357 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit 51a0ccc1d66a6935584356e69b4c67f12a2ae00c +Subproject commit 3713835762bd601366dc20970bc1ee2825552356 From eb2cfdfd46b583d0e9cf18059528eba672571e3f Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 15 Apr 2024 15:07:56 +0300 Subject: [PATCH 12/32] added required changes --- src/components/screens/sprint-view-screen.tsx | 19 ++++++++------ .../sprint-view-table/tasks-table.tsx | 25 ++++++++----------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 06725d8a..40793caa 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -50,7 +50,7 @@ const SprintViewScreen = () => { }, [loggedInPerson]); /** - * Get project allocations for logged in user + * Get project names for logged in user */ const getProjects = async () => { if (loggedInPerson) { @@ -70,6 +70,10 @@ const SprintViewScreen = () => { } } }; + + /** + * Get allocations for logged in user + */ const getPersonAllocations = async () => { if (loggedInPerson) { try { @@ -196,16 +200,17 @@ const SprintViewScreen = () => { - {strings.placeHolder.pleaseWait} - + {strings.placeHolder.pleaseWait} + ) : ( <> - } label={strings.sprint.showMyTasks} onClick={() => handleOnClick()}/> - {strings.sprint.taskStatus} - @@ -254,7 +259,7 @@ const SprintViewScreen = () => { ]} /> - {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} + {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} {projects.map((project) => { diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 58805492..651720e6 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -26,9 +26,9 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const [timeEntries, setTimeEntries] = useState([]); const [open, setOpen] = useState(false); -/** - * Gather tasks and time entries when project is available and update changes appeared - */ + /** + * Gather tasks and time entries when project is available and update changes appeared + */ useEffect(() => { const fetchData = async () => { if (project) { @@ -38,9 +38,9 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { fetchData(); }, [project, loggedInpersonId, filter]); -/** - * Get total time entries for the task - */ + /** + * Get tasks and total time entries + */ const getTasksAndTimeEntries = async () => { try { let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); @@ -48,13 +48,10 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); } setTasks(fetchedTasks); - /** - * Fetch time entries for each task - */ const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { try { if (project.id ){ - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id,taskId: task.id });// + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id, taskId: task.id }); let totalMinutes = 0; totalTimeEntries.forEach((timeEntry: TimeEntries)=> { if (loggedInpersonId && timeEntry.person===loggedInpersonId) { @@ -68,7 +65,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { } return 0; } catch (error) { - console.error(`Error fetching time entries for allocation ${task.id}: ${error}`); + console.error(`Error fetching time entries for task ${task.id}: ${error}`); return 0; } })); @@ -78,9 +75,9 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { } }; -/** - * Retrieve total time entries for a task - */ + /** + * Retrieve total time entries for a task + */ const getTotalTimeEntries = (task: Tasks) => { if (timeEntries.length!==0) { return timeEntries[tasks.indexOf(task)]; From c36d765f2062fd95423f9bf1c08e91f6f7dce909 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Wed, 17 Apr 2024 14:51:47 +0300 Subject: [PATCH 13/32] added requested changes --- src/components/screens/sprint-view-screen.tsx | 93 ++++++++----------- .../sprint-view-table/tasks-table.tsx | 6 ++ 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 40793caa..92a95bd5 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -39,9 +39,7 @@ const SprintViewScreen = () => { useEffect(() => { setLoading(true); const fetchData = async () => { - await getPersonAllocations(); - await getProjects(); - await getTimeEntries(); + await fetchAllocationsData(); setLoading(false); }; if (loggedInPerson) { @@ -49,73 +47,58 @@ const SprintViewScreen = () => { } }, [loggedInPerson]); - /** - * Get project names for logged in user - */ - const getProjects = async () => { + const fetchAllocationsData = async () => { if (loggedInPerson) { - const projects : Projects[] = []; try { - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - personId: loggedInPerson.id.toString()}); - const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - fetchedAllocations.forEach((allocation)=> { - const projectFound = fetchedProjects.find((project)=> project.id === allocation.project); - projectFound && projects.push(projectFound); - }) - setProjects(projects); - } catch (error) { - console.error(error, "Error fetching projects:"); - } - } - }; - /** - * Get allocations for logged in user - */ - const getPersonAllocations = async () => { - if (loggedInPerson) { - try { + /** + * Get allocations for logged in user + */ const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date(), personId: loggedInPerson.id.toString() }); - setAllocations(fetchedAllocations); - } catch (error) { - console.error("Error fetching allocations:", error); - } - } - }; - /** - * Get project time entries for logged in user - */ - const getTimeEntries = async () => { - if (loggedInPerson) { - try { - const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date() , personId: loggedInPerson.id.toString()}); + /** + * Get project names for logged in user + */ + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + + /** + * Get project time entries for logged in user + */ const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { try { - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0}); + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0, startDate: allocation.startDate, endDate: allocation.endDate }); let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries)=> { - if (loggedInPerson && timeEntry.person===loggedInPerson.id) { - totalMinutes+=(timeEntry.timeRegistered || 0) - }}) + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPerson && timeEntry.person === loggedInPerson.id) { + totalMinutes += (timeEntry.timeRegistered || 0) + } + }); return totalMinutes; } catch (error) { console.error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); return 0; } })); + + const projects : Projects[] = []; + fetchedAllocations.forEach((allocation) => { + const projectFound = fetchedProjects.find((project) => project.id === allocation.project); + projectFound && projects.push(projectFound); + }); + + setProjects(projects); + setAllocations(fetchedAllocations); setTimeEntries(fetchedTimeEntries); + } catch (error) { - console.error(`Error fetching time entries: ${error}`); + console.error("Error fetching data:", error); } } }; - + /** * Calculate project total time spent on the project by the user */ @@ -206,8 +189,8 @@ const SprintViewScreen = () => { ) : ( <> - - } label={strings.sprint.showMyTasks} onClick={() => handleOnClick()}/> + } label={strings.sprint.showMyTasks} onClick={() => handleOnClick()}/> + {strings.sprint.taskStatus} - @@ -219,13 +218,17 @@ const SprintViewScreen = () => { borderLeft: 0, borderRight: 0, borderBottom: 0, + '& .header-color': { + backgroundColor: '#f2f2f2', + } }} disableColumnFilter hideFooter={true} rows={allocations} columns={[ { - field: 'projectName', + field: 'projectName', + headerClassName: 'header-color', filterable: false, headerName: strings.sprint.myAllocation, flex: 2, valueGetter: (params) => getProjectName(params.row), @@ -233,22 +236,25 @@ const SprintViewScreen = () => { }, { field: 'allocation', + headerClassName: 'header-color', headerName: strings.sprint.allocation, flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) }, { field: 'timeEntries', + headerClassName: 'header-color', headerName: strings.sprint.timeEntries, flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row) || 0), }, { field: 'allocationsLeft', + headerClassName: 'header-color', headerName: strings.sprint.allocationLeft, flex: 1, cellClassName: (params) => timeLeft(params.row) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row)) }, ]} /> - + {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} {strings.sprint.sprintview}: {sprintStartDate} - {sprintEndDate} diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 0146f070..08a43514 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -1,4 +1,4 @@ -import { Card, IconButton } from "@mui/material"; +import { Box, Card, CircularProgress, IconButton } from "@mui/material"; import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; import { DataGrid } from "@mui/x-data-grid"; import { useEffect, useState } from "react"; @@ -25,18 +25,21 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); /** * Gather tasks and time entries when project is available and update changes appeared */ useEffect(() => { const fetchData = async () => { - if (project) { + setLoading(true); + if (project && open) { await getTasksAndTimeEntries(); + setLoading(false); } }; fetchData(); - }, [project, loggedInpersonId, filter]); + }, [project, loggedInpersonId, filter, open]); /** * Get tasks and total time entries @@ -86,7 +89,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { } return ( - { {project ? project.name : ""} {open && tasks.length !== 0 && - getTotalTimeEntries(params.row)!==0 ? "In progress" : "On hold" - }, - { - field: 'priority', - headerName: strings.sprint.taskPriority, - cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', - flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' - }, - { field: 'estimate', - headerName: strings.sprint.estimatedTime, - flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) - }, - { - field: 'timeEntries', - headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) - } - ]} - /> + <> + {loading ? + + + + : + getTotalTimeEntries(params.row)!==0 ? "In progress" : "On hold" + }, + { + field: 'priority', + headerClassName: 'header-color', + headerName: strings.sprint.taskPriority, + cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', + flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' + }, + { field: 'estimate', + headerClassName: 'header-color', + headerName: strings.sprint.estimatedTime, + flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + }, + { + field: 'timeEntries',headerClassName: 'header-color', + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) + } + ]} + /> + } + } ) diff --git a/src/utils/time-utils.ts b/src/utils/time-utils.ts index c36812bf..e8039424 100644 --- a/src/utils/time-utils.ts +++ b/src/utils/time-utils.ts @@ -181,7 +181,7 @@ const countWorkingWeekDaysInRange = (startWeekIndex: number, endWeekIndex: numbe export const getSprintStart = (date: string) => { const weekIndex = DateTime.fromISO(date).localWeekNumber; const weekDay = DateTime.fromISO(date).weekday; - const days = (weekIndex%2 === 0 ? 0 : 7) + weekDay; + const days = (weekIndex%2 === 1 ? 0 : 7) + weekDay; return DateTime.fromISO(date).minus({days : days - 1}); } From 2b17eb7e6a01f8082ec8e69683f220f68f259b56 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Sat, 20 Apr 2024 20:31:31 +0300 Subject: [PATCH 16/32] changed status structure and added other minor changes --- home-lambdas-API-spec | 2 +- src/components/screens/sprint-view-screen.tsx | 123 +++++++------- .../sprint-view-table/tasks-table.tsx | 155 ++++++++++-------- src/localization/en.json | 7 +- src/localization/fi.json | 7 +- src/localization/strings.ts | 7 +- 6 files changed, 158 insertions(+), 143 deletions(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index 37138357..bc86c972 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit 3713835762bd601366dc20970bc1ee2825552356 +Subproject commit bc86c972cf22192e87683361bb960e17c0ff5067 diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 59d5a9b0..b6678451 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -15,6 +15,7 @@ import TaskTable from '../sprint-view-table/tasks-table'; import strings from "../../localization/strings"; import FormControlLabel from '@mui/material/FormControlLabel'; import Switch from '@mui/material/Switch'; + /** * Sprint view screen component */ @@ -28,78 +29,67 @@ const SprintViewScreen = () => { const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); const [timeEntries, setTimeEntries] = useState([]); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [myTasks, setMyTasks] = useState(true); const [filter, setFilter] = useState(""); - const sprintStartDate = getSprintStart((new Date()).toISOString()).toLocaleString(); - const sprintEndDate = getSprintEnd((new Date()).toISOString()).toLocaleString(); + const sprintStartDate = getSprintStart((new Date()).toISOString()); + const sprintEndDate = getSprintEnd((new Date()).toISOString()); /** * Get project data if user is logged in otherwise endless loading */ useEffect(() => { setLoading(true); - const fetchData = async () => { - await fetchAllocationsData(); + const fetchViewData = async () => { + await fetchData(); setLoading(false); }; if (loggedInPerson) { - fetchData(); + fetchViewData(); } }, [loggedInPerson]); - const fetchAllocationsData = async () => { - if (loggedInPerson) { - try { + /** + * Fetch allocations, project names and time entries + */ + const fetchData = async () => { + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + personId: loggedInPerson?.id.toString() + }); + + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { + try { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0, startDate: allocation.startDate, endDate: allocation.endDate }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPerson && timeEntry.person === loggedInPerson.id) { + totalMinutes += (timeEntry.timeRegistered || 0) + } + }); + return totalMinutes; + } catch (error) { + console.error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); + return 0; + } + })); - /** - * Get allocations for logged in user - */ - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - personId: loggedInPerson.id.toString() - }); + const projects : Projects[] = []; + fetchedAllocations.forEach((allocation) => { + const projectFound = fetchedProjects.find((project) => project.id === allocation.project); + projectFound && projects.push(projectFound); + }); - /** - * Get project names for logged in user - */ - const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - - /** - * Get project time entries for logged in user - */ - const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { - try { - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0, startDate: allocation.startDate, endDate: allocation.endDate }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries) => { - if (loggedInPerson && timeEntry.person === loggedInPerson.id) { - totalMinutes += (timeEntry.timeRegistered || 0) - } - }); - return totalMinutes; - } catch (error) { - console.error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); - return 0; - } - })); - - const projects : Projects[] = []; - fetchedAllocations.forEach((allocation) => { - const projectFound = fetchedProjects.find((project) => project.id === allocation.project); - projectFound && projects.push(projectFound); - }); - - setProjects(projects); - setAllocations(fetchedAllocations); - setTimeEntries(fetchedTimeEntries); - - } catch (error) { - console.error("Error fetching data:", error); - } + setProjects(projects); + setAllocations(fetchedAllocations); + setTimeEntries(fetchedTimeEntries); + } catch (error) { + console.error("Error fetching data:", error); } }; - + /** * Calculate project total time spent on the project by the user */ @@ -173,14 +163,14 @@ const SprintViewScreen = () => { /** * Featute for task filtering */ - const handleOnClick = () => { + const handleOnClickTask = () => { setMyTasks(!myTasks); setFilter(""); } return ( <> - {loading || !loggedInPerson ? ( + {loading ? ( {strings.placeHolder.pleaseWait} @@ -189,7 +179,7 @@ const SprintViewScreen = () => { ) : ( <> - } label={strings.sprint.showMyTasks} onClick={() => handleOnClick()}/> + } label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()}/> {strings.sprint.taskStatus} @@ -222,6 +215,8 @@ const SprintViewScreen = () => { backgroundColor: '#f2f2f2', } }} + autoHeight={true} + localeText={{ noResultsOverlayLabel: strings.sprint.notFound }} disableColumnFilter hideFooter={true} rows={allocations} @@ -236,7 +231,7 @@ const SprintViewScreen = () => { }, { field: 'allocation', - headerClassName: 'header-color', + headerClassName: 'header-color', headerName: strings.sprint.allocation, flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) }, @@ -257,13 +252,13 @@ const SprintViewScreen = () => { {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} - {strings.sprint.sprintview}: {sprintStartDate} - {sprintEndDate} + {strings.sprint.currentSprint}: {sprintStartDate.toLocaleString()} - {sprintEndDate.toLocaleString()} {projects.map((project) => { return ( - + ) } )} diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 08a43514..dac47f68 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -50,6 +50,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { if (loggedInpersonId) { fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); } + setTasks(fetchedTasks); const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { try { @@ -88,6 +89,16 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { return 0; } + /** + * Lowercase all letters in a string except the first letter except first one + */ + + const lowerCaseAllWordsExceptFirstLetters = (string: string) => { + return string.replace(/\S*/g, (word) => { + return word.slice(0, 1) + word.slice(1).toLowerCase(); + }); + }; + return ( { {open ? : } {project ? project.name : ""} - {open && tasks.length !== 0 && - <> - {loading ? - - - - : - getTotalTimeEntries(params.row)!==0 ? "In progress" : "On hold" - }, - { - field: 'priority', - headerClassName: 'header-color', - headerName: strings.sprint.taskPriority, - cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', - flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' - }, - { field: 'estimate', - headerClassName: 'header-color', - headerName: strings.sprint.estimatedTime, - flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) - }, - { - field: 'timeEntries',headerClassName: 'header-color', - headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) - } - ]} - /> - } - + {open && + <> + {loading ? + + + + : + lowerCaseAllWordsExceptFirstLetters(params.row.statusCategory || ''), + }, + { + field: 'priority', + headerClassName: 'header-color', + headerName: strings.sprint.taskPriority, + cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', + flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' + }, + { field: 'estimate', + headerClassName: 'header-color', + headerName: strings.sprint.estimatedTime, + flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + }, + { + field: 'timeEntries',headerClassName: 'header-color', + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) + } + ]} + /> + } + } ) diff --git a/src/localization/en.json b/src/localization/en.json index dd1f268d..4956fcc6 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -69,13 +69,16 @@ "estimatedTime": "Estimate", "taskName": "Tasks", "showMyTasks": "My tasks", - "onHold": "On hold", + "toDo": "To do", "inProgress": "In progress", "allTasks" : "All tasks", "notFound" : "No result found", "projectName": "Project", "search": "Search for a task, project or assignee", - "unAllocated": "Unallocated Time:" + "unAllocated": "Unallocated Time:", + "sprintDate": "Sprint", + "currentSprint": "Current", + "completed": "Done" }, "errors": { "fetchFailedGeneral": "There was an error fetching data", diff --git a/src/localization/fi.json b/src/localization/fi.json index dfe6d1d0..1b32028f 100644 --- a/src/localization/fi.json +++ b/src/localization/fi.json @@ -69,13 +69,16 @@ "estimatedTime": "Arvio", "taskName": "Tehtävä", "showMyTasks": "Tehtäväni ", - "onHold": "Odotuksessa", + "toDo": "Odotuksessa", "inProgress": "Käynnissä", "allTasks" : "Kaikki tehtävät", "notFound" : "Tulosta ei löytynyt", "projectName": "Projekti", "search": "Hae tehtävää, projektia tai vastaanottajaa", - "unAllocated": "Kohdistamaton aika:" + "unAllocated": "Kohdistamaton aika:", + "sprintDate": "Sprint", + "currentSprint": "Nykyinen", + "completed": "Valmis" }, "errors": { "fetchFailedGeneral": "Tietojen hakeminen epäonnistui", diff --git a/src/localization/strings.ts b/src/localization/strings.ts index 99493765..c07fa264 100644 --- a/src/localization/strings.ts +++ b/src/localization/strings.ts @@ -95,13 +95,16 @@ export interface Localized extends LocalizedStringsMethods { estimatedTime: string, taskName: string, showMyTasks: string, - onHold: string, + toDo: string, inProgress: string, allTasks: string, notFound: string, projectName: string, search: string, - unAllocated: string + unAllocated: string, + sprintDate: string, + currentSprint: string, + completed: string }; /** * General time-related expressions From 0422309f27879eb314966294a040bb9bff2e27c5 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Thu, 25 Apr 2024 12:32:43 +0300 Subject: [PATCH 17/32] Added required changes --- src/components/screens/sprint-view-screen.tsx | 148 +++++------------- .../sprint-projects-columns.tsx | 112 +++++++++++++ .../sprint-view-table/sprint-tasks-columns.ts | 71 +++++++++ .../sprint-view-table/tasks-table.tsx | 120 ++++---------- src/localization/en.json | 10 +- src/localization/fi.json | 10 +- src/localization/strings.ts | 13 +- src/utils/time-utils.ts | 2 +- 8 files changed, 287 insertions(+), 199 deletions(-) create mode 100644 src/components/sprint-view-table/sprint-projects-columns.tsx create mode 100644 src/components/sprint-view-table/sprint-tasks-columns.ts diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index b6678451..aae0bf6b 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,20 +1,18 @@ -import { useState, useEffect } from 'react'; -import { Card,FormControl, InputLabel, MenuItem, Select, CircularProgress, Typography, Box} from '@mui/material'; +import { useState, useEffect } from "react"; +import { Card,FormControl, InputLabel, MenuItem, Select, CircularProgress, Typography, Box, FormControlLabel, Switch} from "@mui/material"; import { useLambdasApi } from "../../hooks/use-api"; import { Person } from "../../generated/client"; -import { useAtomValue } from "jotai"; -import { personsAtom } from "../../atoms/person"; +import { useAtomValue, useSetAtom } from "jotai"; +import { personsAtom, } from "../../atoms/person"; import config from "../../app/config"; import { userProfileAtom } from "../../atoms/auth"; -import { Allocations } from "../../generated/homeLambdasClient/models/Allocations"; -import { Projects } from "../../generated/homeLambdasClient/models/Projects"; -import { TimeEntries } from "../../generated/homeLambdasClient/models/TimeEntries"; +import { Allocations, Projects, TimeEntries } from "../../generated/homeLambdasClient/models/"; import { DataGrid } from '@mui/x-data-grid'; import { getHoursAndMinutes, getSprintEnd, getSprintStart } from '../../utils/time-utils'; import TaskTable from '../sprint-view-table/tasks-table'; import strings from "../../localization/strings"; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Switch from '@mui/material/Switch'; +import sprintViewProjectsColumns from "../sprint-view-table/sprint-projects-columns"; +import { errorAtom } from "../../atoms/error"; /** * Sprint view screen component @@ -29,30 +27,29 @@ const SprintViewScreen = () => { const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); const [timeEntries, setTimeEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [myTasks, setMyTasks] = useState(true); - const [filter, setFilter] = useState(""); - const sprintStartDate = getSprintStart((new Date()).toISOString()); + const [loading, setLoading] = useState(false); + const [myTasks, setMyTasks] = useState(true); + const [filter, setFilter] = useState(""); + const todaysDate = (new Date()).toISOString() + const sprintStartDate = getSprintStart(todaysDate); const sprintEndDate = getSprintEnd((new Date()).toISOString()); + const columns = sprintViewProjectsColumns({allocations, timeEntries, projects}); + const setError = useSetAtom(errorAtom); /** - * Get project data if user is logged in otherwise endless loading + * Get project data if user is logged in */ useEffect(() => { setLoading(true); - const fetchViewData = async () => { - await fetchData(); - setLoading(false); - }; if (loggedInPerson) { - fetchViewData(); + fetchPersonEngagement(); } }, [loggedInPerson]); /** * Fetch allocations, project names and time entries */ - const fetchData = async () => { + const fetchPersonEngagement = async () => { try { const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date(), @@ -71,7 +68,8 @@ const SprintViewScreen = () => { }); return totalMinutes; } catch (error) { - console.error(`Error fetching time entries for allocation ${allocation.id}: ${error}`); + const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id||0).toString(), error as string).toString(); + setError(message); return 0; } })); @@ -86,12 +84,15 @@ const SprintViewScreen = () => { setAllocations(fetchedAllocations); setTimeEntries(fetchedTimeEntries); } catch (error) { - console.error("Error fetching data:", error); + setError(`${strings.sprintRequestError.fetchError}, ${error}`); } + setLoading(false); }; /** - * Calculate project total time spent on the project by the user + * Calculate total time allocated to the project for 2 week period + * + * @param allocation expected work load of user in minutes */ const totalAllocations = (allocation: Allocations) => { const totalMinutes = @@ -104,44 +105,9 @@ const SprintViewScreen = () => { } /** - * Get total time required to complete the project - */ - const getTotalTimeEntries = (allocation: Allocations) => { - if (timeEntries.length!==0) { - return timeEntries[allocations.indexOf(allocation)]; - } - return 0; - } - - /** - * Calculate the remaining time of project completion - */ - const timeLeft = (allocation: Allocations) => { - return totalAllocations(allocation) - (getTotalTimeEntries(allocation) || 0) - } - - /** - * Get project name - */ - const getProjectName = (allocation: Allocations) => { - if (projects.length !== 0) { - return projects[allocations.indexOf(allocation)]?.name; - } - return ""; - } - - /** - * Get project color - */ - const getProjectColor = (allocation: Allocations) => { - if (projects.length !== 0) { - return projects[allocations.indexOf(allocation)]?.color; - } - return ""; - } - - /** - * Calculate total unallocated time for the user + * Calculate total unallocated time for the user in the current 2 week period + * @param total + * @param person user time spent on the project in minutes */ const unallocatedTime = (allocation: Allocations[]) => { const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); @@ -172,19 +138,19 @@ const SprintViewScreen = () => { <> {loading ? ( - + {strings.placeHolder.pleaseWait} - + ) : ( <> } label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()}/> - + {strings.sprint.taskStatus} - { borderLeft: 0, borderRight: 0, borderBottom: 0, - '& .header-color': { - backgroundColor: '#f2f2f2', + "& .header-color": { + backgroundColor: "#f2f2f2", } }} autoHeight={true} @@ -220,47 +186,17 @@ const SprintViewScreen = () => { disableColumnFilter hideFooter={true} rows={allocations} - columns={[ - { - field: 'projectName', - headerClassName: 'header-color', - filterable: false, - headerName: strings.sprint.myAllocation, - flex: 2, valueGetter: (params) => getProjectName(params.row), - renderCell:(params) => <>{getProjectName(params.row)} - }, - { - field: 'allocation', - headerClassName: 'header-color', - headerName: strings.sprint.allocation, - flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) - }, - { - field: 'timeEntries', - headerClassName: 'header-color', - headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row) || 0), - }, - { - field: 'allocationsLeft', - headerClassName: 'header-color', - headerName: strings.sprint.allocationLeft, - flex: 1, cellClassName: (params) => timeLeft(params.row) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row)) - }, - ]} + columns={columns} /> - - {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} + + {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} - {strings.sprint.currentSprint}: {sprintStartDate.toLocaleString()} - {sprintEndDate.toLocaleString()} + {strings.formatString(strings.sprint.current, sprintStartDate.toLocaleString(), sprintEndDate.toLocaleString() )} - {projects.map((project) => { - return ( - - ) - } + {projects.map((project) => + )} )} diff --git a/src/components/sprint-view-table/sprint-projects-columns.tsx b/src/components/sprint-view-table/sprint-projects-columns.tsx new file mode 100644 index 00000000..1a30328e --- /dev/null +++ b/src/components/sprint-view-table/sprint-projects-columns.tsx @@ -0,0 +1,112 @@ +import { GridColDef } from "@mui/x-data-grid"; +import { Box} from "@mui/material"; +import strings from "../../localization/strings"; +import { getHoursAndMinutes } from "../../utils/time-utils"; +import { Allocations, Projects } from "../../generated/homeLambdasClient"; + +/** + * Component properties + */ +interface Props { + + allocations: Allocations[], + timeEntries: number[], + projects: Projects[] +} + +const sprintViewProjectsColumns = ({allocations, timeEntries, projects}: Props) => { + /** + * Define columns for data grid + */ + const columns: GridColDef[] = [ + { + field: "projectName", + headerClassName: "header-color", + filterable: false, + headerName: strings.sprint.myAllocation, + flex: 2, valueGetter: (params) => getProjectName(params.row, allocations, projects), + renderCell: (params) => <>{getProjectName(params.row, allocations, projects)} + }, + { + field: "allocation", + headerClassName: "header-color", + headerName: strings.sprint.allocation, + flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) + }, + { + field: "timeEntries", + headerClassName: "header-color", + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row, allocations, timeEntries) || 0), + }, + { + field: "allocationsLeft", + headerClassName: "header-color", + headerName: strings.sprint.allocationLeft, + flex: 1, cellClassName: (params) => timeLeft(params.row, allocations, timeEntries) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row, allocations, timeEntries)) + }, + ]; + return columns; +}; + +/** + * Retrieve total time entries for a task + * + * @param task task of allocated project + */ +const getTotalTimeEntries = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { + if (timeEntries.length) { + return timeEntries[allocations.indexOf(allocation)]; + } + return 0; +} + +/** + * Get project name + * + * @param allocation expected work load of user in minutes + */ +const getProjectName = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { + if (projects.length) { + return projects[allocations.indexOf(allocation)]?.name; + } + return ""; +} + +/** + * Get project color + * + * @param allocation expected work load of user in minutes + */ +const getProjectColor = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { + if (projects.length) { + return projects[allocations.indexOf(allocation)]?.color; + } + return ""; +} + +/** + * Calculate total time allocated to the project for 2 week period + * + * @param allocation expected work load of user in minutes + */ +const totalAllocations = (allocation: Allocations) => { + const totalMinutes = + (allocation.monday || 0) + + (allocation.tuesday || 0) + + (allocation.wednesday || 0) + + (allocation.thursday || 0) + + (allocation.friday || 0); + return totalMinutes * 2; +} + +/** + * Calculate the remaining time of project completion + * + * @param allocation expected work load of user in minutes + */ + const timeLeft = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { + return totalAllocations(allocation) - getTotalTimeEntries(allocation, allocations, timeEntries) + } + +export default sprintViewProjectsColumns; diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts new file mode 100644 index 00000000..6c907bc9 --- /dev/null +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -0,0 +1,71 @@ +import { GridColDef } from "@mui/x-data-grid"; +import strings from "../../localization/strings"; +import { getHoursAndMinutes } from "../../utils/time-utils"; +import { Tasks } from "../../generated/homeLambdasClient"; + +/** + * Component properties + */ +interface Props { + tasks : Tasks[], + timeEntries: number[] +} + +const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { + /** + * Define columns for data grid + */ + const columns: GridColDef[] = [ + { + field: "title", + headerClassName: "header-color", + headerName: strings.sprint.taskName, + minWidth: 0, + flex: 3 + }, + { + field: "assignedPersons", + headerClassName: "header-color", + headerName: strings.sprint.assigned, + flex: 1 + }, + { + field: "status", + headerClassName: "header-color", + headerName: strings.sprint.taskStatus, + flex: 1, valueGetter: (params) => params.row.status || "", + }, + { + field: "priority", + headerClassName: "header-color", + headerName: strings.sprint.taskPriority, + cellClassName: (params) => params.row.highPriority ? "high_priority": "low_priority", + flex: 1, valueGetter: (params) => params.row.highPriority ? "High" : "Normal" + }, + { field: "estimate", + headerClassName: "header-color", + headerName: strings.sprint.estimatedTime, + flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + }, + { + field: "timeEntries", headerClassName: "header-color", + headerName: strings.sprint.timeEntries, + flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row, tasks, timeEntries)) + }, + ]; + return columns; +}; + +/** + * Retrieve total time entries for a task + * + * @param task task of allocated project + */ +const getTotalTimeEntries = (task: Tasks, tasks: Tasks[], timeEntries: number[]) => { + if (timeEntries.length) { + return timeEntries[tasks.indexOf(task)]; + } + return 0; +} + +export default sprintViewTasksColumns; diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index dac47f68..bceb8434 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -1,12 +1,14 @@ -import { Box, Card, CircularProgress, IconButton } from "@mui/material"; +import { Box, Card, CircularProgress, IconButton, Typography } from "@mui/material"; import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; import { DataGrid } from "@mui/x-data-grid"; import { useEffect, useState } from "react"; import { useLambdasApi } from "../../hooks/use-api"; -import { getHoursAndMinutes } from "../../utils/time-utils"; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import strings from "../../localization/strings"; +import sprintViewTasksColumns from "./sprint-tasks-columns"; +import { errorAtom } from "../../atoms/error"; +import { useSetAtom } from "jotai"; /** * Component properties @@ -19,32 +21,32 @@ interface Props { /** * Task component + * + * @param props component properties */ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); - const [open, setOpen] = useState(false); - const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const columns = sprintViewTasksColumns({tasks, timeEntries}); + const setError = useSetAtom(errorAtom); /** * Gather tasks and time entries when project is available and update changes appeared */ useEffect(() => { - const fetchData = async () => { - setLoading(true); - if (project && open) { - await getTasksAndTimeEntries(); - setLoading(false); - } - }; - fetchData(); - }, [project, loggedInpersonId, filter, open]); + if (project && open) { + getTasksAndTimeEntries(); + } + }, [project, loggedInpersonId, filter, open]); /** * Get tasks and total time entries */ const getTasksAndTimeEntries = async () => { + setLoading(true); try { let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); if (loggedInpersonId) { @@ -69,51 +71,34 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { } return 0; } catch (error) { - console.error(`Error fetching time entries for task ${task.id}: ${error}`); + const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id||0, error as string).toString(); + setError(message); return 0; } })); setTimeEntries(fetchedTimeEntries); + setLoading(false); } catch (error) { - console.error(`Error fetching time entries: ${error}`); - } - }; - - /** - * Retrieve total time entries for a task - */ - const getTotalTimeEntries = (task: Tasks) => { - if (timeEntries.length!==0) { - return timeEntries[tasks.indexOf(task)]; + setError(`${strings.sprintRequestError.fetchTimeEntriesError} ${error}`); + setLoading(false); } - return 0; - } - - /** - * Lowercase all letters in a string except the first letter except first one - */ - - const lowerCaseAllWordsExceptFirstLetters = (string: string) => { - return string.replace(/\S*/g, (word) => { - return word.slice(0, 1) + word.slice(1).toLowerCase(); - }); }; return ( - setOpen(!open)}> {open ? : } - {project ? project.name : ""} + {project ? project.name : ""} {open && <> {loading ? - + : @@ -124,8 +109,8 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { borderLeft: 0, borderRight: 0, borderBottom: 0, - '& .header-color': { - backgroundColor: '#f2f2f2', + "& .header-color": { + backgroundColor: "#f2f2f2", } }} autoHeight={true} @@ -135,50 +120,13 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { filterModel={ { items: [{ - field: 'status', - operator: 'contains', + field: "status", + operator: "contains", value: filter }] }} rows={tasks} - columns={[ - { - field: 'title', - headerClassName: 'header-color', - headerName: strings.sprint.taskName, - minWidth: 0, - flex: 3 - }, - { - field: 'assignedPersons', - headerClassName: 'header-color', - headerName: strings.sprint.assigned, - flex: 1 - }, - { - field: 'status', - headerClassName: 'header-color', - headerName: strings.sprint.taskStatus, - flex: 1, valueGetter: (params) => lowerCaseAllWordsExceptFirstLetters(params.row.statusCategory || ''), - }, - { - field: 'priority', - headerClassName: 'header-color', - headerName: strings.sprint.taskPriority, - cellClassName: (params) => params.row.highPriority ? 'high_priority' : 'low_priority', - flex: 1, valueGetter: (params) => params.row.highPriority ? 'High' : 'Normal' - }, - { field: 'estimate', - headerClassName: 'header-color', - headerName: strings.sprint.estimatedTime, - flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) - }, - { - field: 'timeEntries',headerClassName: 'header-color', - headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row)) - } - ]} + columns={columns} /> } diff --git a/src/localization/en.json b/src/localization/en.json index 4956fcc6..57a8c176 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -77,8 +77,8 @@ "search": "Search for a task, project or assignee", "unAllocated": "Unallocated Time:", "sprintDate": "Sprint", - "currentSprint": "Current", - "completed": "Done" + "completed": "Done", + "current": "Current: {0}-{1}" }, "errors": { "fetchFailedGeneral": "There was an error fetching data", @@ -156,6 +156,12 @@ "noVacationRequestsFound": "No vacation requests found", "nameNotFound": "Name not found" }, + "sprintRequestError": { + "fetchError": "Fetching sprint data requests has failed", + "fetchTimeEntriesError": "Error fetching time entries", + "fetchAllocationError": "Error fetching time entries for allocation {0}: {1}", + "fetchTasksError": "Error fetching time entries for task {0}: {1}" + }, "form": { "submit": "Submit", "update": "Update" diff --git a/src/localization/fi.json b/src/localization/fi.json index 1b32028f..afae9984 100644 --- a/src/localization/fi.json +++ b/src/localization/fi.json @@ -77,8 +77,8 @@ "search": "Hae tehtävää, projektia tai vastaanottajaa", "unAllocated": "Kohdistamaton aika:", "sprintDate": "Sprint", - "currentSprint": "Nykyinen", - "completed": "Valmis" + "completed": "Valmis", + "current": "Nykyinen: {0}-{1}" }, "errors": { "fetchFailedGeneral": "Tietojen hakeminen epäonnistui", @@ -156,6 +156,12 @@ "noVacationRequestsFound": "Lomahakemuksia ei löydetty", "nameNotFound": "Nimeä ei löydetty" }, + "sprintRequestError": { + "fetchError": "Sprintin tietopyyntöjen hakeminen epäonnistui", + "fetchTimeEntriesError": "Virhe aikamerkintöjen hakemisessa", + "fetchAllocationError": "Virhe aikamerkintöjen hakemisessa jakamista varten {0}: {1}", + "fetchTasksError": "Tehtävän {0} aikamerkintöjen noutovirhe: {1}" + }, "form": { "submit": "Lähetä", "update": "Päivitä" diff --git a/src/localization/strings.ts b/src/localization/strings.ts index c07fa264..3f139672 100644 --- a/src/localization/strings.ts +++ b/src/localization/strings.ts @@ -103,8 +103,8 @@ export interface Localized extends LocalizedStringsMethods { search: string, unAllocated: string, sprintDate: string, - currentSprint: string, - completed: string + completed: string, + current: string, }; /** * General time-related expressions @@ -181,6 +181,15 @@ export interface Localized extends LocalizedStringsMethods { noVacationRequestsFound: string; nameNotFound: string; }; + /** + * Translations related to sprint requests errors + */ + sprintRequestError: { + fetchError: string; + fetchTimeEntriesError: string; + fetchAllocationError: string; + fetchTasksError: string; + }; /** * Translations related to form */ diff --git a/src/utils/time-utils.ts b/src/utils/time-utils.ts index e8039424..8e8cf314 100644 --- a/src/utils/time-utils.ts +++ b/src/utils/time-utils.ts @@ -191,5 +191,5 @@ export const getSprintStart = (date: string) => { * @param date string date */ export const getSprintEnd = (date: string) => { - return getSprintStart(date).plus({days : 14}); + return getSprintStart(date).plus({days : 11}); } \ No newline at end of file From 1a404377405c78960caa076a8bfe38628d157ef3 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Sun, 28 Apr 2024 15:36:50 +0300 Subject: [PATCH 18/32] added new changes --- src/components/screens/sprint-view-screen.tsx | 239 ++++++++++-------- .../sprint-projects-columns.tsx | 99 +++----- .../sprint-view-table/sprint-tasks-columns.ts | 30 +-- .../sprint-view-table/tasks-table.tsx | 98 ++++--- src/utils/sprint-utils.ts | 103 ++++++++ 5 files changed, 345 insertions(+), 224 deletions(-) create mode 100644 src/utils/sprint-utils.ts diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index a86d4076..aaf55184 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -7,12 +7,13 @@ import { personsAtom, } from "src/atoms/person"; import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; import { Allocations, Projects, TimeEntries } from "src/generated/homeLambdasClient/models/"; -import { DataGrid } from '@mui/x-data-grid'; -import { getHoursAndMinutes, getSprintEnd, getSprintStart } from 'src/utils/time-utils'; -import TaskTable from '../sprint-view-table/tasks-table'; +import { DataGrid } from "@mui/x-data-grid"; +import { getHoursAndMinutes, getSprintEnd, getSprintStart } from "src/utils/time-utils"; +import TaskTable from "src/components/sprint-view-table/tasks-table"; import strings from "src/localization/strings"; -import sprintViewProjectsColumns from "../sprint-view-table/sprint-projects-columns"; +import sprintViewProjectsColumns from "src/components/sprint-view-table/sprint-projects-columns"; import { errorAtom } from "src/atoms/error"; +import { calculateWorkingLoad, totalAllocations } from "src/utils/sprint-utils"; /** * Sprint view screen component @@ -32,7 +33,7 @@ const SprintViewScreen = () => { const [filter, setFilter] = useState(""); const todaysDate = (new Date()).toISOString() const sprintStartDate = getSprintStart(todaysDate); - const sprintEndDate = getSprintEnd((new Date()).toISOString()); + const sprintEndDate = getSprintEnd(todaysDate); const columns = sprintViewProjectsColumns({allocations, timeEntries, projects}); const setError = useSetAtom(errorAtom); @@ -40,89 +41,67 @@ const SprintViewScreen = () => { * Get project data if user is logged in */ useEffect(() => { - setLoading(true); - if (loggedInPerson) { - fetchPersonEngagement(); - } - }, [loggedInPerson]); + fetchProjectDetails(); + },[loggedInPerson]); /** * Fetch allocations, project names and time entries */ - const fetchPersonEngagement = async () => { - try { - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - personId: loggedInPerson?.id.toString() - }); - - const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { - try { - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project || 0, startDate: allocation.startDate, endDate: allocation.endDate }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries) => { - if (loggedInPerson && timeEntry.person === loggedInPerson.id) { - totalMinutes += (timeEntry.timeRegistered || 0) + const fetchProjectDetails = async () => { + setLoading(true); + if (loggedInPerson){ + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + personId: loggedInPerson?.id.toString() + }); + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { + try { + if (allocation.project) { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project, startDate: allocation.startDate, endDate: allocation.endDate }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPerson && timeEntry.person === loggedInPerson.id) { + totalMinutes += (timeEntry.timeRegistered || 0) + } + }); + return totalMinutes; } - }); - return totalMinutes; - } catch (error) { - const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id||0).toString(), error as string).toString(); - setError(message); + } catch (error) { + if (allocation.id) { + const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id).toString(), error as string).toString(); + setError(message); + } + } return 0; - } - })); - - const projects : Projects[] = []; - fetchedAllocations.forEach((allocation) => { - const projectFound = fetchedProjects.find((project) => project.id === allocation.project); - projectFound && projects.push(projectFound); - }); + })); + + const projects : Projects[] = []; + fetchedAllocations.forEach((allocation) => { + const projectFound = fetchedProjects.find((project) => project.id === allocation.project); + projectFound && projects.push(projectFound); + }); - setProjects(projects); - setAllocations(fetchedAllocations); - setTimeEntries(fetchedTimeEntries); - } catch (error) { - setError(`${strings.sprintRequestError.fetchError}, ${error}`); + setProjects(projects); + setAllocations(fetchedAllocations); + setTimeEntries(fetchedTimeEntries); + setLoading(false); + } catch (error) { + setError(`${strings.sprintRequestError.fetchError}, ${error}`); + setLoading(false); + } } setLoading(false); }; - /** - * Calculate total time allocated to the project for 2 week period - * - * @param allocation expected work load of user in minutes - */ - const totalAllocations = (allocation: Allocations) => { - const totalMinutes = - (allocation.monday || 0) + - (allocation.tuesday || 0) + - (allocation.wednesday || 0) + - (allocation.thursday || 0) + - (allocation.friday || 0); - return totalMinutes * 2; - } - /** * Calculate total unallocated time for the user in the current 2 week period - * @param total - * @param person user time spent on the project in minutes + * + * @param alllocation task allocated within a project */ const unallocatedTime = (allocation: Allocations[]) => { const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); - const calculateWorkingLoad = (person?: Person) => { - if (!person) { - return 0; - } - const totalMinutes = - (person.monday || 0) + - (person.tuesday || 0) + - (person.wednesday || 0) + - (person.thursday || 0) + - (person.friday || 0); - return totalMinutes * 2; - } return calculateWorkingLoad(loggedInPerson) - totalAllocatedTime; } @@ -136,51 +115,92 @@ const SprintViewScreen = () => { return ( <> - {loading ? ( - - + {!allocations.length && !projects.length && !timeEntries.length ? ( + + {loading && {strings.placeHolder.pleaseWait} - - - + + } + ) : ( <> - } label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()}/> + } + label={strings.sprint.showMyTasks} + onClick={() => handleOnClickTask()} + /> - {strings.sprint.taskStatus} + + {strings.sprint.taskStatus} + - - + { rows={allocations} columns={columns} /> - - {strings.sprint.unAllocated} {getHoursAndMinutes(unallocatedTime(allocations))} + + + {strings.sprint.unAllocated}, {getHoursAndMinutes(unallocatedTime(allocations))} + {strings.formatString(strings.sprint.current, sprintStartDate.toLocaleString(), sprintEndDate.toLocaleString() )} {projects.map((project) => - + )} )} diff --git a/src/components/sprint-view-table/sprint-projects-columns.tsx b/src/components/sprint-view-table/sprint-projects-columns.tsx index 1a30328e..29f20cc6 100644 --- a/src/components/sprint-view-table/sprint-projects-columns.tsx +++ b/src/components/sprint-view-table/sprint-projects-columns.tsx @@ -1,19 +1,24 @@ import { GridColDef } from "@mui/x-data-grid"; import { Box} from "@mui/material"; import strings from "../../localization/strings"; -import { getHoursAndMinutes } from "../../utils/time-utils"; -import { Allocations, Projects } from "../../generated/homeLambdasClient"; +import { getHoursAndMinutes } from "src/utils/time-utils"; +import { getProjectColor, getProjectName, getTotalTimeEntriesAllocations, timeLeft, totalAllocations } from "src/utils/sprint-utils"; +import { Allocations, Projects } from "src/generated/homeLambdasClient"; /** * Component properties */ interface Props { - allocations: Allocations[], timeEntries: number[], projects: Projects[] } +/** + * Requests data for Project's table columns + * + * @param props component properties + */ const sprintViewProjectsColumns = ({allocations, timeEntries, projects}: Props) => { /** * Define columns for data grid @@ -24,89 +29,47 @@ const sprintViewProjectsColumns = ({allocations, timeEntries, projects}: Props) headerClassName: "header-color", filterable: false, headerName: strings.sprint.myAllocation, - flex: 2, valueGetter: (params) => getProjectName(params.row, allocations, projects), - renderCell: (params) => <>{getProjectName(params.row, allocations, projects)} + flex: 2, + valueGetter: (params) => getProjectName(params.row, allocations, projects), + renderCell: (params) => + <> + + {getProjectName(params.row, allocations, projects)} + }, { field: "allocation", headerClassName: "header-color", headerName: strings.sprint.allocation, - flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) + flex: 1, + valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) }, { field: "timeEntries", headerClassName: "header-color", headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row, allocations, timeEntries) || 0), + flex: 1, + valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntriesAllocations(params.row, allocations, timeEntries)) }, { field: "allocationsLeft", headerClassName: "header-color", headerName: strings.sprint.allocationLeft, - flex: 1, cellClassName: (params) => timeLeft(params.row, allocations, timeEntries) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row, allocations, timeEntries)) + flex: 1, + cellClassName: (params) => timeLeft(params.row, allocations, timeEntries) < 0 ? "negative-value" : "", + valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row, allocations, timeEntries)) }, ]; return columns; }; -/** - * Retrieve total time entries for a task - * - * @param task task of allocated project - */ -const getTotalTimeEntries = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { - if (timeEntries.length) { - return timeEntries[allocations.indexOf(allocation)]; - } - return 0; -} - -/** - * Get project name - * - * @param allocation expected work load of user in minutes - */ -const getProjectName = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { - if (projects.length) { - return projects[allocations.indexOf(allocation)]?.name; - } - return ""; -} - -/** - * Get project color - * - * @param allocation expected work load of user in minutes - */ -const getProjectColor = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { - if (projects.length) { - return projects[allocations.indexOf(allocation)]?.color; - } - return ""; -} - -/** - * Calculate total time allocated to the project for 2 week period - * - * @param allocation expected work load of user in minutes - */ -const totalAllocations = (allocation: Allocations) => { - const totalMinutes = - (allocation.monday || 0) + - (allocation.tuesday || 0) + - (allocation.wednesday || 0) + - (allocation.thursday || 0) + - (allocation.friday || 0); - return totalMinutes * 2; -} - -/** - * Calculate the remaining time of project completion - * - * @param allocation expected work load of user in minutes - */ - const timeLeft = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { - return totalAllocations(allocation) - getTotalTimeEntries(allocation, allocations, timeEntries) - } - export default sprintViewProjectsColumns; diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts index 6c907bc9..589ecfcd 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -2,6 +2,7 @@ import { GridColDef } from "@mui/x-data-grid"; import strings from "../../localization/strings"; import { getHoursAndMinutes } from "../../utils/time-utils"; import { Tasks } from "../../generated/homeLambdasClient"; +import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; /** * Component properties @@ -11,6 +12,11 @@ interface Props { timeEntries: number[] } +/** + * Requests data for Task's table columns + * + * @param props component properties + */ const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { /** * Define columns for data grid @@ -33,39 +39,31 @@ const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { field: "status", headerClassName: "header-color", headerName: strings.sprint.taskStatus, - flex: 1, valueGetter: (params) => params.row.status || "", + flex: 1, + valueGetter: (params) => params.row.status || "", }, { field: "priority", headerClassName: "header-color", headerName: strings.sprint.taskPriority, cellClassName: (params) => params.row.highPriority ? "high_priority": "low_priority", - flex: 1, valueGetter: (params) => params.row.highPriority ? "High" : "Normal" + flex: 1, + valueGetter: (params) => params.row.highPriority ? "High" : "Normal" }, { field: "estimate", headerClassName: "header-color", headerName: strings.sprint.estimatedTime, - flex: 1, valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + flex: 1, + valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) }, { field: "timeEntries", headerClassName: "header-color", headerName: strings.sprint.timeEntries, - flex: 1, valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntries(params.row, tasks, timeEntries)) + flex: 1, + valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntriesTasks(params.row, tasks, timeEntries)) }, ]; return columns; }; -/** - * Retrieve total time entries for a task - * - * @param task task of allocated project - */ -const getTotalTimeEntries = (task: Tasks, tasks: Tasks[], timeEntries: number[]) => { - if (timeEntries.length) { - return timeEntries[tasks.indexOf(task)]; - } - return 0; -} - export default sprintViewTasksColumns; diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index bceb8434..108b1f2c 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -32,60 +32,77 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const [loading, setLoading] = useState(false); const columns = sprintViewTasksColumns({tasks, timeEntries}); const setError = useSetAtom(errorAtom); + const [reload, setReload] = useState(false); /** * Gather tasks and time entries when project is available and update changes appeared */ useEffect(() => { if (project && open) { - getTasksAndTimeEntries(); + getTasksAndTimeEntries(); } - }, [project, loggedInpersonId, filter, open]); + }, [project, open, filter, reload]); + /** + * Handle loggenInPersonId change + */ + useEffect(()=>{ + setReload(true); + },[loggedInpersonId]); + /** * Get tasks and total time entries */ const getTasksAndTimeEntries = async () => { setLoading(true); - try { - let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); - if (loggedInpersonId) { - fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); - } - - setTasks(fetchedTasks); - const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { - try { - if (project.id ){ - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id, taskId: task.id }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries)=> { - if (loggedInpersonId && timeEntry.person===loggedInpersonId) { - totalMinutes+=(timeEntry.timeRegistered || 0) - } - if (!loggedInpersonId) { - totalMinutes += timeEntry.timeRegistered || 0; - } - }) - return totalMinutes; + if (reload || !tasks.length || !timeEntries.length){ + try { + let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); + if (loggedInpersonId) { + fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); + } + setTasks(fetchedTasks); + const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { + try { + if (project.id ){ + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id, taskId: task.id }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries)=> { + if (loggedInpersonId && timeEntry.person===loggedInpersonId) { + totalMinutes+=(timeEntry.timeRegistered || 0) + } + if (!loggedInpersonId) { + totalMinutes += timeEntry.timeRegistered || 0; + } + }) + return totalMinutes; + } + } catch (error) { + if (task.id) { + const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id||0, error as string).toString(); + setError(message); + } } return 0; - } catch (error) { - const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id||0, error as string).toString(); - setError(message); - return 0; - } - })); - setTimeEntries(fetchedTimeEntries); - setLoading(false); - } catch (error) { - setError(`${strings.sprintRequestError.fetchTimeEntriesError} ${error}`); - setLoading(false); + })); + setTimeEntries(fetchedTimeEntries); + } catch (error) { + setError(`${strings.sprintRequestError.fetchTimeEntriesError} ${error}`); + } } + setReload(false); + setLoading(false); }; return ( - { setOpen(!open)}> {open ? : } - {project ? project.name : ""} + {project?.name} {open && <> {loading ? - + : { borderRight: 0, borderBottom: 0, "& .header-color": { - backgroundColor: "#f2f2f2", + backgroundColor: "#f2f2f2" } }} autoHeight={true} diff --git a/src/utils/sprint-utils.ts b/src/utils/sprint-utils.ts new file mode 100644 index 00000000..5e29658c --- /dev/null +++ b/src/utils/sprint-utils.ts @@ -0,0 +1,103 @@ +import { Person } from "src/generated/client"; +import { Allocations, Projects, Tasks } from "src/generated/homeLambdasClient"; + +/** + * Retrieve total time entries for an Allocation + * + * @param allocation task allocated within a project + * @param allocations tasks related to the project + * @param timeEntries total time entries requested for each allocation + */ +export const getTotalTimeEntriesAllocations = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { + if (timeEntries.length) { + return timeEntries[allocations.indexOf(allocation)] || 0; + } + return 0; +} + +/** + * Retrieve total time entries for a task + * + * @param task task of allocated project + * @param tasks total tasks related to the project + * @param timeEntries total time entries requested for each task + */ +export const getTotalTimeEntriesTasks = (task: Tasks, tasks: Tasks[], timeEntries: number[]) => { + if (timeEntries.length) { + return timeEntries[tasks.indexOf(task)] || 0; + } + return 0; +} + +/** + * Get project name + * + * @param allocation task allocated within a project + * @param allocations tasks related to the project + * @param projects project associated with the given allocation + */ +export const getProjectName = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { + if (projects.length) { + return projects[allocations.indexOf(allocation)]?.name || ""; + } + return ""; +} + +/** + * Get project color + * + * @param allocation task allocated within a project + * @param allocations tasks related to the project + * @param projects project associated with the given allocation + */ +export const getProjectColor = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { + if (projects.length) { + return projects[allocations.indexOf(allocation)]?.color || ""; + } + return ""; +} + +/** + * Calculate total time allocated to the project for 2 week period + * + * @param allocation expected work load of user in minutes + */ +export const totalAllocations = (allocation: Allocations) => { + const totalMinutes = + (allocation.monday || 0) + + (allocation.tuesday || 0) + + (allocation.wednesday || 0) + + (allocation.thursday || 0) + + (allocation.friday || 0); + return totalMinutes * 2; +} + +/** + * Calculate the remaining time of project completion + * + * @param allocation task allocated within a project + * @param allocations tasks related to the project + * @param projects project associated with the given allocation + */ +export const timeLeft = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { + return totalAllocations(allocation) - getTotalTimeEntriesAllocations(allocation, allocations, timeEntries) || 0; +} + +/** + * Calculate registered time for the user in the current 2 week period + * + * @param person user time spent on the project in minutes + */ + +export const calculateWorkingLoad = (person?: Person) => { + if (!person) { + return 0; + } + const totalMinutes = + (person.monday || 0) + + (person.tuesday || 0) + + (person.wednesday || 0) + + (person.thursday || 0) + + (person.friday || 0); + return totalMinutes * 2; +} \ No newline at end of file From 9894fa9c0d2ed40f0ec60f841d7dbdb45170ee6c Mon Sep 17 00:00:00 2001 From: DZotoff Date: Thu, 2 May 2024 00:43:31 +0300 Subject: [PATCH 19/32] added some changes --- src/components/screens/sprint-view-screen.tsx | 95 ++++++++++--------- .../sprint-projects-columns.tsx | 28 +++--- .../sprint-view-table/sprint-tasks-columns.ts | 4 +- .../sprint-view-table/tasks-table.tsx | 65 +++++++------ src/utils/sprint-utils.ts | 38 ++++---- 5 files changed, 120 insertions(+), 110 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index aaf55184..b9443962 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -86,10 +86,8 @@ const SprintViewScreen = () => { setProjects(projects); setAllocations(fetchedAllocations); setTimeEntries(fetchedTimeEntries); - setLoading(false); } catch (error) { setError(`${strings.sprintRequestError.fetchError}, ${error}`); - setLoading(false); } } setLoading(false); @@ -98,12 +96,12 @@ const SprintViewScreen = () => { /** * Calculate total unallocated time for the user in the current 2 week period * - * @param alllocation task allocated within a project + * @param allocation task allocated within a project */ const unallocatedTime = (allocation: Allocations[]) => { - const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); - return calculateWorkingLoad(loggedInPerson) - totalAllocatedTime; - } + const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); + return calculateWorkingLoad(loggedInPerson) - totalAllocatedTime; + } /** * Featute for task filtering @@ -120,14 +118,17 @@ const SprintViewScreen = () => { p: "25%", display: "flex", justifyContent: "center" - }}> + }} + > {loading && {strings.placeHolder.pleaseWait} - + } ) : ( @@ -137,7 +138,7 @@ const SprintViewScreen = () => { label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()} /> - + {strings.sprint.taskStatus} @@ -180,27 +181,29 @@ const SprintViewScreen = () => { - - + { rows={allocations} columns={columns} /> - - + {strings.sprint.unAllocated}, {getHoursAndMinutes(unallocatedTime(allocations))} @@ -229,7 +238,7 @@ const SprintViewScreen = () => { )} diff --git a/src/components/sprint-view-table/sprint-projects-columns.tsx b/src/components/sprint-view-table/sprint-projects-columns.tsx index 29f20cc6..aeaf773b 100644 --- a/src/components/sprint-view-table/sprint-projects-columns.tsx +++ b/src/components/sprint-view-table/sprint-projects-columns.tsx @@ -15,7 +15,7 @@ interface Props { } /** - * Requests data for Project's table columns + * Sprint view projects table columns component * * @param props component properties */ @@ -32,19 +32,19 @@ const sprintViewProjectsColumns = ({allocations, timeEntries, projects}: Props) flex: 2, valueGetter: (params) => getProjectName(params.row, allocations, projects), renderCell: (params) => - <> - - {getProjectName(params.row, allocations, projects)} - + <> + + {getProjectName(params.row, allocations, projects)} + }, { field: "allocation", diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts index 589ecfcd..bc4593b3 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -13,7 +13,7 @@ interface Props { } /** - * Requests data for Task's table columns + * Sprint view tasks table columns component * * @param props component properties */ @@ -40,7 +40,7 @@ const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { headerClassName: "header-color", headerName: strings.sprint.taskStatus, flex: 1, - valueGetter: (params) => params.row.status || "", + valueGetter: (params) => params.row.status || "" }, { field: "priority", diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 108b1f2c..3dc96743 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -15,16 +15,16 @@ import { useSetAtom } from "jotai"; */ interface Props { project : Projects, - loggedInpersonId?: number, + loggedInPersonId?: number, filter?: string } /** - * Task component + * Task table component * * @param props component properties */ -const TaskTable = ({project, loggedInpersonId, filter}: Props) => { +const TaskTable = ({project, loggedInPersonId, filter}: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); @@ -35,7 +35,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const [reload, setReload] = useState(false); /** - * Gather tasks and time entries when project is available and update changes appeared + * Gather tasks and time entries when project is available and update reload state */ useEffect(() => { if (project && open) { @@ -48,7 +48,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { */ useEffect(()=>{ setReload(true); - },[loggedInpersonId]); + }, [loggedInPersonId]); /** * Get tasks and total time entries @@ -58,8 +58,8 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { if (reload || !tasks.length || !timeEntries.length){ try { let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); - if (loggedInpersonId) { - fetchedTasks = fetchedTasks.filter((task)=> task.assignedPersons?.includes(loggedInpersonId )); + if (loggedInPersonId) { + fetchedTasks = fetchedTasks.filter((task) => task.assignedPersons?.includes(loggedInPersonId)); } setTasks(fetchedTasks); const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { @@ -68,10 +68,10 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id, taskId: task.id }); let totalMinutes = 0; totalTimeEntries.forEach((timeEntry: TimeEntries)=> { - if (loggedInpersonId && timeEntry.person===loggedInpersonId) { + if (loggedInPersonId && timeEntry.person === loggedInPersonId) { totalMinutes+=(timeEntry.timeRegistered || 0) } - if (!loggedInpersonId) { + if (!loggedInPersonId) { totalMinutes += timeEntry.timeRegistered || 0; } }) @@ -79,7 +79,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { } } catch (error) { if (task.id) { - const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id||0, error as string).toString(); + const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id || 0, error as string).toString(); setError(message); } } @@ -95,19 +95,21 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { }; return ( - + setOpen(!open)}> {open ? : } @@ -115,12 +117,14 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { {open && <> {loading ? - - + : @@ -139,8 +143,7 @@ const TaskTable = ({project, loggedInpersonId, filter}: Props) => { localeText={{ noResultsOverlayLabel: strings.sprint.notFound }} disableColumnFilter hideFooter={true} - filterModel={ - { + filterModel={{ items: [{ field: "status", operator: "contains", diff --git a/src/utils/sprint-utils.ts b/src/utils/sprint-utils.ts index 5e29658c..9132b475 100644 --- a/src/utils/sprint-utils.ts +++ b/src/utils/sprint-utils.ts @@ -2,11 +2,11 @@ import { Person } from "src/generated/client"; import { Allocations, Projects, Tasks } from "src/generated/homeLambdasClient"; /** - * Retrieve total time entries for an Allocation + * Retrieve total time entries for an allocation * - * @param allocation task allocated within a project - * @param allocations tasks related to the project - * @param timeEntries total time entries requested for each allocation + * @param allocation allocation + * @param allocations list of allocations + * @param timeEntries list of total time entries associated with allocations */ export const getTotalTimeEntriesAllocations = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { if (timeEntries.length) { @@ -19,8 +19,8 @@ export const getTotalTimeEntriesAllocations = (allocation: Allocations, allocati * Retrieve total time entries for a task * * @param task task of allocated project - * @param tasks total tasks related to the project - * @param timeEntries total time entries requested for each task + * @param tasks list of tasks related to the project + * @param timeEntries list of total time associated with tasks */ export const getTotalTimeEntriesTasks = (task: Tasks, tasks: Tasks[], timeEntries: number[]) => { if (timeEntries.length) { @@ -32,9 +32,9 @@ export const getTotalTimeEntriesTasks = (task: Tasks, tasks: Tasks[], timeEntrie /** * Get project name * - * @param allocation task allocated within a project - * @param allocations tasks related to the project - * @param projects project associated with the given allocation + * @param allocation allocation + * @param allocations list of allocations + * @param projects list of project associated with the allocations */ export const getProjectName = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { if (projects.length) { @@ -46,9 +46,9 @@ export const getProjectName = (allocation: Allocations, allocations: Allocations /** * Get project color * - * @param allocation task allocated within a project - * @param allocations tasks related to the project - * @param projects project associated with the given allocation + * @param allocation allocation + * @param allocations list of allocations + * @param projects list of projects associated with allocations */ export const getProjectColor = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { if (projects.length) { @@ -75,9 +75,9 @@ export const totalAllocations = (allocation: Allocations) => { /** * Calculate the remaining time of project completion * - * @param allocation task allocated within a project - * @param allocations tasks related to the project - * @param projects project associated with the given allocation + * @param allocation allocation + * @param allocations list of allocations + * @param projects list of projects associated with allocations */ export const timeLeft = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { return totalAllocations(allocation) - getTotalTimeEntriesAllocations(allocation, allocations, timeEntries) || 0; @@ -88,11 +88,9 @@ export const timeLeft = (allocation: Allocations, allocations: Allocations[], ti * * @param person user time spent on the project in minutes */ - export const calculateWorkingLoad = (person?: Person) => { - if (!person) { - return 0; - } + if (!person) return 0; + const totalMinutes = (person.monday || 0) + (person.tuesday || 0) + @@ -100,4 +98,4 @@ export const calculateWorkingLoad = (person?: Person) => { (person.thursday || 0) + (person.friday || 0); return totalMinutes * 2; -} \ No newline at end of file +}; From c50f0ff3bdeb2dc0f244ada332ccda22d2d4a1da Mon Sep 17 00:00:00 2001 From: Daniil Zotov Date: Thu, 2 May 2024 14:41:40 +0300 Subject: [PATCH 20/32] fixed filtering & styling --- src/components/screens/sprint-view-screen.tsx | 20 ++++++++++--------- .../sprint-view-table/sprint-tasks-columns.ts | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index b9443962..1f55602e 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -221,17 +221,19 @@ const SprintViewScreen = () => { paddingBottom:" 10px" }} > - + {strings.sprint.unAllocated} + - {strings.sprint.unAllocated}, {getHoursAndMinutes(unallocatedTime(allocations))} - - + color: unallocatedTime(allocations) < 0 ? "red" : "" + }} + > + {getHoursAndMinutes(unallocatedTime(allocations))} + + + {strings.formatString(strings.sprint.current, sprintStartDate.toLocaleString(), sprintEndDate.toLocaleString() )} - + {projects.map((project) => diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts index bc4593b3..e21202bd 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -40,7 +40,8 @@ const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { headerClassName: "header-color", headerName: strings.sprint.taskStatus, flex: 1, - valueGetter: (params) => params.row.status || "" + valueGetter: (params) => params.row.statusCategory || "", + renderCell: (params) => params.row.status }, { field: "priority", From b30742fb6924a1bbaf2d39ea79372f0691219315 Mon Sep 17 00:00:00 2001 From: Daniil Zotov Date: Thu, 2 May 2024 15:22:33 +0300 Subject: [PATCH 21/32] minor changes --- src/components/screens/sprint-view-screen.tsx | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 1f55602e..edb7cec9 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -49,46 +49,45 @@ const SprintViewScreen = () => { */ const fetchProjectDetails = async () => { setLoading(true); - if (loggedInPerson){ - try { - const fetchedAllocations = await allocationsApi.listAllocations({ - startDate: new Date(), - personId: loggedInPerson?.id.toString() - }); - const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { - try { - if (allocation.project) { - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project, startDate: allocation.startDate, endDate: allocation.endDate }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries) => { - if (loggedInPerson && timeEntry.person === loggedInPerson.id) { - totalMinutes += (timeEntry.timeRegistered || 0) - } - }); - return totalMinutes; - } - } catch (error) { - if (allocation.id) { - const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id).toString(), error as string).toString(); - setError(message); - } + if (!loggedInPerson) return; + try { + const fetchedAllocations = await allocationsApi.listAllocations({ + startDate: new Date(), + personId: loggedInPerson?.id.toString() + }); + const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { + try { + if (allocation.project) { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project, startDate: allocation.startDate, endDate: allocation.endDate }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPerson && timeEntry.person === loggedInPerson.id) { + totalMinutes += (timeEntry.timeRegistered || 0) + } + }); + return totalMinutes; } - return 0; - })); - - const projects : Projects[] = []; - fetchedAllocations.forEach((allocation) => { - const projectFound = fetchedProjects.find((project) => project.id === allocation.project); - projectFound && projects.push(projectFound); - }); + } catch (error) { + if (allocation.id) { + const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id).toString(), error as string).toString(); + setError(message); + } + } + return 0; + })); + + const projects : Projects[] = []; + fetchedAllocations.forEach((allocation) => { + const projectFound = fetchedProjects.find((project) => project.id === allocation.project); + projectFound && projects.push(projectFound); + }); - setProjects(projects); - setAllocations(fetchedAllocations); - setTimeEntries(fetchedTimeEntries); - } catch (error) { - setError(`${strings.sprintRequestError.fetchError}, ${error}`); - } + setProjects(projects); + setAllocations(fetchedAllocations); + setTimeEntries(fetchedTimeEntries); + } catch (error) { + setError(`${strings.sprintRequestError.fetchError}, ${error}`); } setLoading(false); }; @@ -113,14 +112,14 @@ const SprintViewScreen = () => { return ( <> - {!allocations.length && !projects.length && !timeEntries.length ? ( + {loading ? ( - {loading && + { {strings.placeHolder.pleaseWait} Date: Tue, 7 May 2024 09:46:35 +0300 Subject: [PATCH 22/32] fixed a few indentation problems --- home-lambdas-API-spec | 2 +- src/components/screens/sprint-view-screen.tsx | 249 ++++++++++-------- src/components/screens/timebank-screen.tsx | 11 +- .../sprint-projects-columns.tsx | 85 +++--- .../sprint-view-table/sprint-tasks-columns.ts | 61 +++-- .../sprint-view-table/tasks-table.tsx | 159 ++++++----- src/utils/sprint-utils.ts | 71 +++-- 7 files changed, 356 insertions(+), 282 deletions(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index bc86c972..ebb3535c 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit bc86c972cf22192e87683361bb960e17c0ff5067 +Subproject commit ebb3535c35bcd1b3f0f9317a09ebc97a15162fe2 diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index edb7cec9..76c69b93 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,9 +1,20 @@ import { useState, useEffect } from "react"; -import { Card,FormControl, InputLabel, MenuItem, Select, CircularProgress, Typography, Box, FormControlLabel, Switch} from "@mui/material"; +import { + Card, + FormControl, + InputLabel, + MenuItem, + Select, + CircularProgress, + Typography, + Box, + FormControlLabel, + Switch +} from "@mui/material"; import { useLambdasApi } from "src/hooks/use-api"; import { Person } from "src/generated/client"; import { useAtomValue, useSetAtom } from "jotai"; -import { personsAtom, } from "src/atoms/person"; +import { personsAtom } from "src/atoms/person"; import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; import { Allocations, Projects, TimeEntries } from "src/generated/homeLambdasClient/models/"; @@ -23,7 +34,8 @@ const SprintViewScreen = () => { const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); const loggedInPerson = persons.find( - (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id + (person: Person) => + person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id ); const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); @@ -31,10 +43,10 @@ const SprintViewScreen = () => { const [loading, setLoading] = useState(false); const [myTasks, setMyTasks] = useState(true); const [filter, setFilter] = useState(""); - const todaysDate = (new Date()).toISOString() + const todaysDate = new Date().toISOString(); const sprintStartDate = getSprintStart(todaysDate); const sprintEndDate = getSprintEnd(todaysDate); - const columns = sprintViewProjectsColumns({allocations, timeEntries, projects}); + const columns = sprintViewProjectsColumns({ allocations, timeEntries, projects }); const setError = useSetAtom(errorAtom); /** @@ -42,7 +54,7 @@ const SprintViewScreen = () => { */ useEffect(() => { fetchProjectDetails(); - },[loggedInPerson]); + }, [loggedInPerson]); /** * Fetch allocations, project names and time entries @@ -56,28 +68,40 @@ const SprintViewScreen = () => { personId: loggedInPerson?.id.toString() }); const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - const fetchedTimeEntries = await Promise.all(fetchedAllocations.map(async (allocation) => { - try { - if (allocation.project) { - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: allocation.project, startDate: allocation.startDate, endDate: allocation.endDate }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries) => { - if (loggedInPerson && timeEntry.person === loggedInPerson.id) { - totalMinutes += (timeEntry.timeRegistered || 0) - } - }); - return totalMinutes; - } - } catch (error) { - if (allocation.id) { - const message: string = strings.formatString(strings.sprintRequestError.fetchAllocationError, (allocation.id).toString(), error as string).toString(); - setError(message); + const fetchedTimeEntries = await Promise.all( + fetchedAllocations.map(async (allocation) => { + try { + if (allocation.project) { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ + projectId: allocation.project, + startDate: allocation.startDate, + endDate: allocation.endDate + }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPerson && timeEntry.person === loggedInPerson.id) { + totalMinutes += timeEntry.timeRegistered || 0; + } + }); + return totalMinutes; + } + } catch (error) { + if (allocation.id) { + const message: string = strings + .formatString( + strings.sprintRequestError.fetchAllocationError, + allocation.id.toString(), + error as string + ) + .toString(); + setError(message); + } } - } - return 0; - })); + return 0; + }) + ); - const projects : Projects[] = []; + const projects: Projects[] = []; fetchedAllocations.forEach((allocation) => { const projectFound = fetchedProjects.find((project) => project.id === allocation.project); projectFound && projects.push(projectFound); @@ -94,106 +118,98 @@ const SprintViewScreen = () => { /** * Calculate total unallocated time for the user in the current 2 week period - * + * * @param allocation task allocated within a project */ const unallocatedTime = (allocation: Allocations[]) => { - const totalAllocatedTime = allocation.reduce((total, allocation) => total + totalAllocations(allocation), 0); + const totalAllocatedTime = allocation.reduce( + (total, allocation) => total + totalAllocations(allocation), + 0 + ); return calculateWorkingLoad(loggedInPerson) - totalAllocatedTime; - } + }; /** - * Featute for task filtering + * Featute for task filtering */ const handleOnClickTask = () => { setMyTasks(!myTasks); setFilter(""); - } - + }; + return ( <> {loading ? ( - - { - {strings.placeHolder.pleaseWait} - - } - + { + + {strings.placeHolder.pleaseWait} + + + } + ) : ( <> - } - label={strings.sprint.showMyTasks} - onClick={() => handleOnClickTask()} - /> - - - {strings.sprint.taskStatus} - + } + label={strings.sprint.showMyTasks} + onClick={() => handleOnClickTask()} + /> + + {strings.sprint.taskStatus} - - { "& .header-color": { backgroundColor: "#f2f2f2" } - }} + }} autoHeight={true} localeText={{ noResultsOverlayLabel: strings.sprint.notFound }} disableColumnFilter - hideFooter={true} + hideFooter={true} rows={allocations} columns={columns} /> - - - {strings.sprint.unAllocated} - + {strings.sprint.unAllocated} + {getHoursAndMinutes(unallocatedTime(allocations))} - + - - {strings.formatString(strings.sprint.current, sprintStartDate.toLocaleString(), sprintEndDate.toLocaleString() )} + + {strings.formatString( + strings.sprint.current, + sprintStartDate.toLocaleString(), + sprintEndDate.toLocaleString() + )} - {projects.map((project) => - ( + - )} + ))} )} ); }; -export default SprintViewScreen; \ No newline at end of file +export default SprintViewScreen; diff --git a/src/components/screens/timebank-screen.tsx b/src/components/screens/timebank-screen.tsx index ca3880c1..45484d15 100644 --- a/src/components/screens/timebank-screen.tsx +++ b/src/components/screens/timebank-screen.tsx @@ -31,7 +31,8 @@ const TimebankScreen = () => { const [personDailyEntry, setPersonDailyEntry] = useAtom(personDailyEntryAtom); const [dailyEntries, setDailyEntries] = useAtom(dailyEntriesAtom); const loggedInPerson = persons.find( - (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id + (person: Person) => + person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id ); const [selectedEmployeeId, setSelectedEmployeeId] = useState(loggedInPerson?.id); const selectedPerson = persons.find((person) => person.id === selectedEmployeeId); @@ -136,7 +137,11 @@ const TimebankScreen = () => { }); setDailyEntries(fetchedDailyEntries); setPersonDailyEntry( - fetchedDailyEntries.find((item) => item.person === selectedEmployeeId && DateTime.fromJSDate(item.date).toISODate() === selectedDate.toISODate()) + fetchedDailyEntries.find( + (item) => + item.person === selectedEmployeeId && + DateTime.fromJSDate(item.date).toISODate() === selectedDate.toISODate() + ) ); } catch (error) { setError(`${strings.error.dailyEntriesFetch}, ${error}`); @@ -148,7 +153,7 @@ const TimebankScreen = () => {
{!personDailyEntry || !selectedEmployeeId || !dailyEntries.length || !personTotalTime ? ( - { loading && } + {loading && } ) : ( { +const sprintViewProjectsColumns = ({ allocations, timeEntries, projects }: Props) => { /** * Define columns for data grid */ const columns: GridColDef[] = [ - { - field: "projectName", - headerClassName: "header-color", + { + field: "projectName", + headerClassName: "header-color", filterable: false, - headerName: strings.sprint.myAllocation, - flex: 2, + headerName: strings.sprint.myAllocation, + flex: 2, valueGetter: (params) => getProjectName(params.row, allocations, projects), - renderCell: (params) => + renderCell: (params) => ( <> - {getProjectName(params.row, allocations, projects)} + ) }, - { - field: "allocation", + { + field: "allocation", headerClassName: "header-color", - headerName: strings.sprint.allocation, - flex: 1, + headerName: strings.sprint.allocation, + flex: 1, valueGetter: (params) => getHoursAndMinutes(totalAllocations(params.row)) }, - { - field: "timeEntries", - headerClassName: "header-color", - headerName: strings.sprint.timeEntries, - flex: 1, - valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntriesAllocations(params.row, allocations, timeEntries)) + { + field: "timeEntries", + headerClassName: "header-color", + headerName: strings.sprint.timeEntries, + flex: 1, + valueGetter: (params) => + getHoursAndMinutes(getTotalTimeEntriesAllocations(params.row, allocations, timeEntries)) }, - { - field: "allocationsLeft", - headerClassName: "header-color", - headerName: strings.sprint.allocationLeft, - flex: 1, - cellClassName: (params) => timeLeft(params.row, allocations, timeEntries) < 0 ? "negative-value" : "", + { + field: "allocationsLeft", + headerClassName: "header-color", + headerName: strings.sprint.allocationLeft, + flex: 1, + cellClassName: (params) => + timeLeft(params.row, allocations, timeEntries) < 0 ? "negative-value" : "", valueGetter: (params) => getHoursAndMinutes(timeLeft(params.row, allocations, timeEntries)) - }, + } ]; return columns; }; diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts index e21202bd..c81f6d0b 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -8,61 +8,64 @@ import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; * Component properties */ interface Props { - tasks : Tasks[], - timeEntries: number[] + tasks: Tasks[]; + timeEntries: number[]; } /** * Sprint view tasks table columns component - * + * * @param props component properties */ -const sprintViewTasksColumns = ({tasks, timeEntries}: Props) => { +const sprintViewTasksColumns = ({ tasks, timeEntries }: Props) => { /** * Define columns for data grid */ const columns: GridColDef[] = [ - { + { field: "title", - headerClassName: "header-color", + headerClassName: "header-color", headerName: strings.sprint.taskName, minWidth: 0, flex: 3 }, - { + { field: "assignedPersons", - headerClassName: "header-color", - headerName: strings.sprint.assigned, + headerClassName: "header-color", + headerName: strings.sprint.assigned, flex: 1 }, - { + { field: "status", - headerClassName: "header-color", - headerName: strings.sprint.taskStatus, - flex: 1, + headerClassName: "header-color", + headerName: strings.sprint.taskStatus, + flex: 1, valueGetter: (params) => params.row.statusCategory || "", renderCell: (params) => params.row.status }, - { + { field: "priority", - headerClassName: "header-color", + headerClassName: "header-color", headerName: strings.sprint.taskPriority, - cellClassName: (params) => params.row.highPriority ? "high_priority": "low_priority", - flex: 1, - valueGetter: (params) => params.row.highPriority ? "High" : "Normal" + cellClassName: (params) => (params.row.highPriority ? "high_priority" : "low_priority"), + flex: 1, + valueGetter: (params) => (params.row.highPriority ? "High" : "Normal") }, - { field: "estimate", - headerClassName: "header-color", - headerName: strings.sprint.estimatedTime, - flex: 1, - valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) - }, - { - field: "timeEntries", headerClassName: "header-color", - headerName: strings.sprint.timeEntries, - flex: 1, - valueGetter: (params) => getHoursAndMinutes(getTotalTimeEntriesTasks(params.row, tasks, timeEntries)) + { + field: "estimate", + headerClassName: "header-color", + headerName: strings.sprint.estimatedTime, + flex: 1, + valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) }, + { + field: "timeEntries", + headerClassName: "header-color", + headerName: strings.sprint.timeEntries, + flex: 1, + valueGetter: (params) => + getHoursAndMinutes(getTotalTimeEntriesTasks(params.row, tasks, timeEntries)) + } ]; return columns; }; diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 3dc96743..9b4e139b 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -14,23 +14,23 @@ import { useSetAtom } from "jotai"; * Component properties */ interface Props { - project : Projects, - loggedInPersonId?: number, - filter?: string + project: Projects; + loggedInPersonId?: number; + filter?: string; } /** * Task table component - * + * * @param props component properties */ -const TaskTable = ({project, loggedInPersonId, filter}: Props) => { +const TaskTable = ({ project, loggedInPersonId, filter }: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const columns = sprintViewTasksColumns({tasks, timeEntries}); + const columns = sprintViewTasksColumns({ tasks, timeEntries }); const setError = useSetAtom(errorAtom); const [reload, setReload] = useState(false); @@ -46,45 +46,58 @@ const TaskTable = ({project, loggedInPersonId, filter}: Props) => { /** * Handle loggenInPersonId change */ - useEffect(()=>{ + useEffect(() => { setReload(true); }, [loggedInPersonId]); - + /** - * Get tasks and total time entries + * Get tasks and total time entries */ const getTasksAndTimeEntries = async () => { setLoading(true); - if (reload || !tasks.length || !timeEntries.length){ + if (reload || !tasks.length || !timeEntries.length) { try { - let fetchedTasks = await tasksApi.listProjectTasks({projectId: project.id}); + let fetchedTasks = await tasksApi.listProjectTasks({ projectId: project.id }); if (loggedInPersonId) { - fetchedTasks = fetchedTasks.filter((task) => task.assignedPersons?.includes(loggedInPersonId)); + fetchedTasks = fetchedTasks.filter((task) => + task.assignedPersons?.includes(loggedInPersonId) + ); } setTasks(fetchedTasks); - const fetchedTimeEntries = await Promise.all(fetchedTasks.map(async (task) => { - try { - if (project.id ){ - const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ projectId: project.id, taskId: task.id }); - let totalMinutes = 0; - totalTimeEntries.forEach((timeEntry: TimeEntries)=> { - if (loggedInPersonId && timeEntry.person === loggedInPersonId) { - totalMinutes+=(timeEntry.timeRegistered || 0) - } - if (!loggedInPersonId) { - totalMinutes += timeEntry.timeRegistered || 0; - } - }) - return totalMinutes; + const fetchedTimeEntries = await Promise.all( + fetchedTasks.map(async (task) => { + try { + if (project.id) { + const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ + projectId: project.id, + taskId: task.id + }); + let totalMinutes = 0; + totalTimeEntries.forEach((timeEntry: TimeEntries) => { + if (loggedInPersonId && timeEntry.person === loggedInPersonId) { + totalMinutes += timeEntry.timeRegistered || 0; + } + if (!loggedInPersonId) { + totalMinutes += timeEntry.timeRegistered || 0; + } + }); + return totalMinutes; + } + } catch (error) { + if (task.id) { + const message: string = strings + .formatString( + strings.sprintRequestError.fetchTasksError, + task.id || 0, + error as string + ) + .toString(); + setError(message); + } } - } catch (error) { - if (task.id) { - const message: string = strings.formatString(strings.sprintRequestError.fetchTasksError, task.id || 0, error as string).toString(); - setError(message); - } - } - return 0; - })); + return 0; + }) + ); setTimeEntries(fetchedTimeEntries); } catch (error) { setError(`${strings.sprintRequestError.fetchTimeEntriesError} ${error}`); @@ -94,40 +107,42 @@ const TaskTable = ({project, loggedInPersonId, filter}: Props) => { setLoading(false); }; - return ( - setOpen(!open)}> {open ? : } - {project?.name} - {open && + {project?.name} + {open && ( <> - {loading ? - - + - : + ) : ( { disableColumnFilter hideFooter={true} filterModel={{ - items: [{ - field: "status", - operator: "contains", - value: filter - }] + items: [ + { + field: "status", + operator: "contains", + value: filter + } + ] }} rows={tasks} columns={columns} - /> - } + /> + )} - } + )} - ) -} + ); +}; -export default TaskTable; \ No newline at end of file +export default TaskTable; diff --git a/src/utils/sprint-utils.ts b/src/utils/sprint-utils.ts index 9132b475..bf1e58bb 100644 --- a/src/utils/sprint-utils.ts +++ b/src/utils/sprint-utils.ts @@ -3,21 +3,25 @@ import { Allocations, Projects, Tasks } from "src/generated/homeLambdasClient"; /** * Retrieve total time entries for an allocation - * + * * @param allocation allocation * @param allocations list of allocations * @param timeEntries list of total time entries associated with allocations */ -export const getTotalTimeEntriesAllocations = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { +export const getTotalTimeEntriesAllocations = ( + allocation: Allocations, + allocations: Allocations[], + timeEntries: number[] +) => { if (timeEntries.length) { return timeEntries[allocations.indexOf(allocation)] || 0; } return 0; -} +}; /** * Retrieve total time entries for a task - * + * * @param task task of allocated project * @param tasks list of tasks related to the project * @param timeEntries list of total time associated with tasks @@ -27,75 +31,90 @@ export const getTotalTimeEntriesTasks = (task: Tasks, tasks: Tasks[], timeEntrie return timeEntries[tasks.indexOf(task)] || 0; } return 0; -} +}; /** * Get project name - * + * * @param allocation allocation * @param allocations list of allocations * @param projects list of project associated with the allocations */ -export const getProjectName = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { +export const getProjectName = ( + allocation: Allocations, + allocations: Allocations[], + projects: Projects[] +) => { if (projects.length) { return projects[allocations.indexOf(allocation)]?.name || ""; } return ""; -} +}; /** * Get project color - * + * * @param allocation allocation * @param allocations list of allocations * @param projects list of projects associated with allocations */ -export const getProjectColor = (allocation: Allocations, allocations: Allocations[], projects: Projects[]) => { +export const getProjectColor = ( + allocation: Allocations, + allocations: Allocations[], + projects: Projects[] +) => { if (projects.length) { return projects[allocations.indexOf(allocation)]?.color || ""; } return ""; -} +}; /** * Calculate total time allocated to the project for 2 week period - * + * * @param allocation expected work load of user in minutes */ export const totalAllocations = (allocation: Allocations) => { const totalMinutes = - (allocation.monday || 0) + - (allocation.tuesday || 0) + - (allocation.wednesday || 0) + - (allocation.thursday || 0) + + (allocation.monday || 0) + + (allocation.tuesday || 0) + + (allocation.wednesday || 0) + + (allocation.thursday || 0) + (allocation.friday || 0); return totalMinutes * 2; -} +}; /** * Calculate the remaining time of project completion - * + * * @param allocation allocation * @param allocations list of allocations * @param projects list of projects associated with allocations */ -export const timeLeft = (allocation: Allocations, allocations: Allocations[], timeEntries: number[]) => { - return totalAllocations(allocation) - getTotalTimeEntriesAllocations(allocation, allocations, timeEntries) || 0; -} +export const timeLeft = ( + allocation: Allocations, + allocations: Allocations[], + timeEntries: number[] +) => { + return ( + totalAllocations(allocation) - + getTotalTimeEntriesAllocations(allocation, allocations, timeEntries) || 0 + ); +}; /** * Calculate registered time for the user in the current 2 week period - * + * * @param person user time spent on the project in minutes */ export const calculateWorkingLoad = (person?: Person) => { if (!person) return 0; const totalMinutes = - (person.monday || 0) + - (person.tuesday || 0) + - (person.wednesday || 0) + - (person.thursday || 0) + + (person.monday || 0) + + (person.tuesday || 0) + + (person.wednesday || 0) + + (person.thursday || 0) + (person.friday || 0); return totalMinutes * 2; }; From 8741e1aa21f9333b328582666305da7e0315e96b Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 20 May 2024 13:54:24 +0300 Subject: [PATCH 23/32] fix spec issue --- home-lambdas-API-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index ebb3535c..56434fed 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit ebb3535c35bcd1b3f0f9317a09ebc97a15162fe2 +Subproject commit 56434fedbcdd97e5eff82630162bae07403bad5b From 4b241dfd25c48a202adf634435b616d5846cc5bf Mon Sep 17 00:00:00 2001 From: DZotoff Date: Fri, 24 May 2024 10:54:41 +0300 Subject: [PATCH 24/32] added required changes --- biome.json | 6 +- src/components/constants/index.ts | 12 ++- src/components/screens/sprint-view-screen.tsx | 74 ++++--------------- .../menu-Item-filter-table.tsx | 44 +++++++++++ .../sprint-projects-columns.tsx | 4 +- .../sprint-view-table/sprint-tasks-columns.ts | 4 +- .../sprint-view-table/tasks-table.tsx | 13 ++-- src/utils/sprint-utils.ts | 16 ++++ 8 files changed, 101 insertions(+), 72 deletions(-) create mode 100644 src/components/sprint-view-table/menu-Item-filter-table.tsx diff --git a/biome.json b/biome.json index 2fc85351..93a80105 100644 --- a/biome.json +++ b/biome.json @@ -8,10 +8,12 @@ "rules": { "recommended": true, "complexity": { - "noUselessFragments": "error" + "noUselessFragments": "error", + "noForEach": "off" }, "suspicious": { - "noExplicitAny": "off" + "noExplicitAny": "off", + "useExhaustiveDependencies": "off" }, "correctness": { "noUnusedVariables": "error" diff --git a/src/components/constants/index.ts b/src/components/constants/index.ts index e1d2bed5..17d9505a 100644 --- a/src/components/constants/index.ts +++ b/src/components/constants/index.ts @@ -12,4 +12,14 @@ export const COLORS = [ /** * Days of week array. * */ -export const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; \ No newline at end of file +export const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + +/** + * Task statuses. + * */ +export const Status = { + TODO: "TODO", + INPROGRESS: "INPROGRESS", + DONE: "DONE", + ALL: "" +}; \ No newline at end of file diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 76c69b93..af2397b8 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -1,30 +1,20 @@ import { useState, useEffect } from "react"; -import { - Card, - FormControl, - InputLabel, - MenuItem, - Select, - CircularProgress, - Typography, - Box, - FormControlLabel, - Switch -} from "@mui/material"; +import { Card, CircularProgress, Typography, Box, FormControlLabel, Switch } from "@mui/material"; import { useLambdasApi } from "src/hooks/use-api"; -import { Person } from "src/generated/client"; +import type { Person } from "src/generated/client"; import { useAtomValue, useSetAtom } from "jotai"; import { personsAtom } from "src/atoms/person"; import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; -import { Allocations, Projects, TimeEntries } from "src/generated/homeLambdasClient/models/"; +import type { Allocations, Projects, TimeEntries} from "src/generated/homeLambdasClient/models/"; import { DataGrid } from "@mui/x-data-grid"; import { getHoursAndMinutes, getSprintEnd, getSprintStart } from "src/utils/time-utils"; import TaskTable from "src/components/sprint-view-table/tasks-table"; import strings from "src/localization/strings"; import sprintViewProjectsColumns from "src/components/sprint-view-table/sprint-projects-columns"; import { errorAtom } from "src/atoms/error"; -import { calculateWorkingLoad, totalAllocations } from "src/utils/sprint-utils"; +import { calculateWorkingLoad, totalAllocations, filterAllocationsAndProjects } from "src/utils/sprint-utils"; +import { TaskStatusFilter } from "src/components/sprint-view-table/menu-Item-filter-table"; /** * Sprint view screen component @@ -65,11 +55,13 @@ const SprintViewScreen = () => { try { const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date(), + endDate: new Date(), personId: loggedInPerson?.id.toString() }); const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); + const {filteredAllocations, filteredProjects} = filterAllocationsAndProjects(fetchedAllocations, fetchedProjects); const fetchedTimeEntries = await Promise.all( - fetchedAllocations.map(async (allocation) => { + filteredAllocations.map(async (allocation) => { try { if (allocation.project) { const totalTimeEntries = await timeEntriesApi.listProjectTimeEntries({ @@ -101,14 +93,8 @@ const SprintViewScreen = () => { }) ); - const projects: Projects[] = []; - fetchedAllocations.forEach((allocation) => { - const projectFound = fetchedProjects.find((project) => project.id === allocation.project); - projectFound && projects.push(projectFound); - }); - - setProjects(projects); - setAllocations(fetchedAllocations); + setProjects(filteredProjects); + setAllocations(filteredAllocations); setTimeEntries(fetchedTimeEntries); } catch (error) { setError(`${strings.sprintRequestError.fetchError}, ${error}`); @@ -167,35 +153,7 @@ const SprintViewScreen = () => { label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()} /> - - {strings.sprint.taskStatus} - - + {TaskStatusFilter(setFilter)} { display: "flex", justifyContent: "space-between", padding: "5px", - paddingTop: " 10px", - paddingBottom: " 10px" + paddingTop: "10px", + paddingBottom: "10px" }} > @@ -256,18 +214,18 @@ const SprintViewScreen = () => { - {projects.map((project) => ( + {projects.map((project) => - ))} + )} )} ); }; -export default SprintViewScreen; +export default SprintViewScreen; \ No newline at end of file diff --git a/src/components/sprint-view-table/menu-Item-filter-table.tsx b/src/components/sprint-view-table/menu-Item-filter-table.tsx new file mode 100644 index 00000000..0be098b2 --- /dev/null +++ b/src/components/sprint-view-table/menu-Item-filter-table.tsx @@ -0,0 +1,44 @@ +import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; +import strings from 'src/localization/strings'; +import { Status } from '../constants'; + + interface Filter { + key: number; + value: string; + label: string; + } + + export const TaskStatusFilter = (setFilter: (string: string) => void) => { + + const statusFilters : Filter[] = [ + { key: 1, value: Status.TODO, label: strings.sprint.toDo }, + { key: 2, value: Status.INPROGRESS, label: strings.sprint.inProgress }, + { key: 3, value: Status.DONE, label: strings.sprint.completed }, + { key: 4, value: Status.ALL, label: strings.sprint.allTasks } + ]; + + return ( + + {strings.sprint.taskStatus} + + + ) + } \ No newline at end of file diff --git a/src/components/sprint-view-table/sprint-projects-columns.tsx b/src/components/sprint-view-table/sprint-projects-columns.tsx index 0432a4e5..e3f829b5 100644 --- a/src/components/sprint-view-table/sprint-projects-columns.tsx +++ b/src/components/sprint-view-table/sprint-projects-columns.tsx @@ -1,4 +1,4 @@ -import { GridColDef } from "@mui/x-data-grid"; +import type { GridColDef } from "@mui/x-data-grid"; import { Box } from "@mui/material"; import strings from "../../localization/strings"; import { getHoursAndMinutes } from "src/utils/time-utils"; @@ -9,7 +9,7 @@ import { timeLeft, totalAllocations } from "src/utils/sprint-utils"; -import { Allocations, Projects } from "src/generated/homeLambdasClient"; +import type { Allocations, Projects } from "src/generated/homeLambdasClient"; /** * Component properties diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts index c81f6d0b..732cc398 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ b/src/components/sprint-view-table/sprint-tasks-columns.ts @@ -1,7 +1,7 @@ -import { GridColDef } from "@mui/x-data-grid"; +import type { GridColDef } from "@mui/x-data-grid"; import strings from "../../localization/strings"; import { getHoursAndMinutes } from "../../utils/time-utils"; -import { Tasks } from "../../generated/homeLambdasClient"; +import type { Tasks } from "../../generated/homeLambdasClient"; import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; /** diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 9b4e139b..6c8da86b 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -57,12 +57,11 @@ const TaskTable = ({ project, loggedInPersonId, filter }: Props) => { setLoading(true); if (reload || !tasks.length || !timeEntries.length) { try { - let fetchedTasks = await tasksApi.listProjectTasks({ projectId: project.id }); - if (loggedInPersonId) { - fetchedTasks = fetchedTasks.filter((task) => - task.assignedPersons?.includes(loggedInPersonId) - ); - } + const fetchedTasks = !loggedInPersonId ? await tasksApi.listProjectTasks({ projectId: project.id }) + : await tasksApi.listProjectTasks({ projectId: project.id }).then(result => result.filter((task) => + task.assignedPersons?.includes(loggedInPersonId || 0) + )); + setTasks(fetchedTasks); const fetchedTimeEntries = await Promise.all( fetchedTasks.map(async (task) => { @@ -177,4 +176,4 @@ const TaskTable = ({ project, loggedInPersonId, filter }: Props) => { ); }; -export default TaskTable; +export default TaskTable; \ No newline at end of file diff --git a/src/utils/sprint-utils.ts b/src/utils/sprint-utils.ts index bf1e58bb..d982becd 100644 --- a/src/utils/sprint-utils.ts +++ b/src/utils/sprint-utils.ts @@ -118,3 +118,19 @@ export const calculateWorkingLoad = (person?: Person) => { (person.friday || 0); return totalMinutes * 2; }; + +/** + * Filter allocations and projects if project is not running + * + * @param allocations allocations + * @param projects list of running projects + */ +export const filterAllocationsAndProjects = (allocations: Allocations[], projects: Projects[]) => { + const filteredProjects: Projects[] = []; + const filteredAllocations = allocations.filter(allocation => projects.find(project => allocation.project === project.id)); + filteredAllocations.map(allocation => { + const allocationProject = projects.find(project => allocation.project === project.id); + if (allocationProject) filteredProjects.push(allocationProject); + }) + return {filteredAllocations, filteredProjects}; +} From 17b2150fa6e15e6b2a4d30f17446e764ab771feb Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 27 May 2024 10:27:20 +0300 Subject: [PATCH 25/32] added avatars and some minor changes --- src/api/api.ts | 4 +- src/components/layout/navbar.tsx | 10 +- src/components/screens/sprint-view-screen.tsx | 16 ++- .../sprint-view-table/sprint-tasks-columns.ts | 73 ----------- .../sprint-tasks-columns.tsx | 120 ++++++++++++++++++ .../sprint-view-table/tasks-table.tsx | 7 +- 6 files changed, 141 insertions(+), 89 deletions(-) delete mode 100644 src/components/sprint-view-table/sprint-tasks-columns.ts create mode 100644 src/components/sprint-view-table/sprint-tasks-columns.tsx diff --git a/src/api/api.ts b/src/api/api.ts index 5570942f..eb4f134d 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -12,6 +12,7 @@ import { AllocationsApi, Configuration as LambdaConfiguration, ProjectsApi, + SlackAvatarsApi, TasksApi, TimeEntriesApi } from "../generated/homeLambdasClient" @@ -69,6 +70,7 @@ export const getLambdasApiClient = (accessToken?: string) => { allocationsApi: new AllocationsApi(getConfiguration()), projectsApi: new ProjectsApi(getConfiguration()), tasksApi: new TasksApi(getConfiguration()), - timeEntriesApi: new TimeEntriesApi(getConfiguration()) + timeEntriesApi: new TimeEntriesApi(getConfiguration()), + slackAvatarsApi: new SlackAvatarsApi(getConfiguration()) }; }; \ No newline at end of file diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index fae88a35..41bdcf27 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -1,13 +1,5 @@ import { MouseEvent, useState } from "react"; -import AppBar from "@mui/material/AppBar"; -import Box from "@mui/material/Box"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import Menu from "@mui/material/Menu"; -import Container from "@mui/material/Container"; -import Avatar from "@mui/material/Avatar"; -import Tooltip from "@mui/material/Tooltip"; -import MenuItem from "@mui/material/MenuItem"; +import {MenuItem, AppBar, Box, Toolbar, IconButton, Menu, Container, Tooltip, Avatar } from "@mui/material"; import LocalizationButtons from "../layout-components/localization-buttons"; import strings from "src/localization/strings"; import { authAtom } from "src/atoms/auth"; diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index af2397b8..174c8277 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -6,7 +6,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { personsAtom } from "src/atoms/person"; import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; -import type { Allocations, Projects, TimeEntries} from "src/generated/homeLambdasClient/models/"; +import type { Allocations, Projects, TimeEntries, UsersAvatars} from "src/generated/homeLambdasClient/models/"; import { DataGrid } from "@mui/x-data-grid"; import { getHoursAndMinutes, getSprintEnd, getSprintStart } from "src/utils/time-utils"; import TaskTable from "src/components/sprint-view-table/tasks-table"; @@ -20,7 +20,7 @@ import { TaskStatusFilter } from "src/components/sprint-view-table/menu-Item-fil * Sprint view screen component */ const SprintViewScreen = () => { - const { allocationsApi, projectsApi, timeEntriesApi } = useLambdasApi(); + const { allocationsApi, projectsApi, timeEntriesApi, slackAvatarsApi } = useLambdasApi(); const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); const loggedInPerson = persons.find( @@ -30,6 +30,7 @@ const SprintViewScreen = () => { const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); const [timeEntries, setTimeEntries] = useState([]); + const [avatars, setAvatars] = useState([]); const [loading, setLoading] = useState(false); const [myTasks, setMyTasks] = useState(true); const [filter, setFilter] = useState(""); @@ -46,6 +47,14 @@ const SprintViewScreen = () => { fetchProjectDetails(); }, [loggedInPerson]); + /** + * fetch avatars + */ + const fetchSlackAvatars = async () => { + const fetchedAvatars = await slackAvatarsApi.slackAvatar(); + setAvatars(fetchedAvatars); + } + /** * Fetch allocations, project names and time entries */ @@ -92,7 +101,7 @@ const SprintViewScreen = () => { return 0; }) ); - + await fetchSlackAvatars(); setProjects(filteredProjects); setAllocations(filteredAllocations); setTimeEntries(fetchedTimeEntries); @@ -220,6 +229,7 @@ const SprintViewScreen = () => { project={project} loggedInPersonId={myTasks ? loggedInPerson?.id : undefined} filter={filter} + avatars={avatars} /> )} diff --git a/src/components/sprint-view-table/sprint-tasks-columns.ts b/src/components/sprint-view-table/sprint-tasks-columns.ts deleted file mode 100644 index 732cc398..00000000 --- a/src/components/sprint-view-table/sprint-tasks-columns.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { GridColDef } from "@mui/x-data-grid"; -import strings from "../../localization/strings"; -import { getHoursAndMinutes } from "../../utils/time-utils"; -import type { Tasks } from "../../generated/homeLambdasClient"; -import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; - -/** - * Component properties - */ -interface Props { - tasks: Tasks[]; - timeEntries: number[]; -} - -/** - * Sprint view tasks table columns component - * - * @param props component properties - */ -const sprintViewTasksColumns = ({ tasks, timeEntries }: Props) => { - /** - * Define columns for data grid - */ - const columns: GridColDef[] = [ - { - field: "title", - headerClassName: "header-color", - headerName: strings.sprint.taskName, - minWidth: 0, - flex: 3 - }, - { - field: "assignedPersons", - headerClassName: "header-color", - headerName: strings.sprint.assigned, - flex: 1 - }, - { - field: "status", - headerClassName: "header-color", - headerName: strings.sprint.taskStatus, - flex: 1, - valueGetter: (params) => params.row.statusCategory || "", - renderCell: (params) => params.row.status - }, - { - field: "priority", - headerClassName: "header-color", - headerName: strings.sprint.taskPriority, - cellClassName: (params) => (params.row.highPriority ? "high_priority" : "low_priority"), - flex: 1, - valueGetter: (params) => (params.row.highPriority ? "High" : "Normal") - }, - { - field: "estimate", - headerClassName: "header-color", - headerName: strings.sprint.estimatedTime, - flex: 1, - valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) - }, - { - field: "timeEntries", - headerClassName: "header-color", - headerName: strings.sprint.timeEntries, - flex: 1, - valueGetter: (params) => - getHoursAndMinutes(getTotalTimeEntriesTasks(params.row, tasks, timeEntries)) - } - ]; - return columns; -}; - -export default sprintViewTasksColumns; diff --git a/src/components/sprint-view-table/sprint-tasks-columns.tsx b/src/components/sprint-view-table/sprint-tasks-columns.tsx new file mode 100644 index 00000000..d5de22bc --- /dev/null +++ b/src/components/sprint-view-table/sprint-tasks-columns.tsx @@ -0,0 +1,120 @@ +import type { GridColDef } from "@mui/x-data-grid"; +import strings from "../../localization/strings"; +import { getHoursAndMinutes } from "../../utils/time-utils"; +import type { Tasks, UsersAvatars } from "../../generated/homeLambdasClient"; +import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; +import { Avatar, AvatarGroup } from "@mui/material"; +import Tooltip from '@mui/material/Tooltip'; +import { personsAtom } from "src/atoms/person"; +import { Person } from "src/generated/client"; +import { useAtomValue } from "jotai"; +/** + * Component properties + */ +interface Props { + tasks: Tasks[]; + timeEntries: number[]; + avatars?: UsersAvatars[]; +} + +/** + * Sprint view tasks table columns component + * + * @param props component properties + */ +const sprintViewTasksColumns = ({ tasks, timeEntries, avatars }: Props) => { + /** + * Define columns for data grid + */ + const columns: GridColDef[] = [ + { + field: "title", + headerClassName: "header-color", + headerName: strings.sprint.taskName, + minWidth: 0, + flex: 3 + }, + { + field: "assignedPersons", + headerClassName: "header-color", + headerName: strings.sprint.assigned, + flex: 1, + renderCell: (params) => { + const persons: Person[] = useAtomValue(personsAtom); + return ( + + {params.row.assignedPersons.map((personId: number, index: number) => { + const avatar = avatars && avatars.find(avatar => avatar.personId === personId); + const person = persons && persons.find(person => person.id === personId); + const maxAvatarsInLine = 3; + if (index + + + ); + } + if (index === maxAvatarsInLine && params.row.assignedPersons.length-maxAvatarsInLine > 0) { + let tooltipTitile = ""; + params.row.assignedPersons.slice(maxAvatarsInLine).map((personId: number) => { + const personFound = persons.find(person => person.id === personId); + tooltipTitile += `${personFound?.firstName} ${personFound?.lastName}, `; + }) + tooltipTitile = tooltipTitile.slice(0, tooltipTitile.length - 2); + if (params.row.assignedPersons.length-maxAvatarsInLine === 1) { + return ( + + + + ) + } + return ( + + ) + } + })} + + ) + } + }, + { + field: "status", + headerClassName: "header-color", + headerName: strings.sprint.taskStatus, + flex: 1, + valueGetter: (params) => params.row.statusCategory || "", + renderCell: (params) => params.row.status + }, + { + field: "priority", + headerClassName: "header-color", + headerName: strings.sprint.taskPriority, + cellClassName: (params) => (params.row.highPriority ? "high_priority" : "low_priority"), + flex: 1, + valueGetter: (params) => (params.row.highPriority ? "High" : "Normal") + }, + { + field: "estimate", + headerClassName: "header-color", + headerName: strings.sprint.estimatedTime, + flex: 1, + valueGetter: (params) => getHoursAndMinutes(params.row.estimate || 0) + }, + { + field: "timeEntries", + headerClassName: "header-color", + headerName: strings.sprint.timeEntries, + flex: 1, + valueGetter: (params) => + getHoursAndMinutes(getTotalTimeEntriesTasks(params.row, tasks, timeEntries)) + } + ]; + return columns; +}; + +export default sprintViewTasksColumns; \ No newline at end of file diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 6c8da86b..9c028752 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -1,5 +1,5 @@ import { Box, Card, CircularProgress, IconButton, Typography } from "@mui/material"; -import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; +import { Projects, Tasks, TimeEntries, UsersAvatars } from "../../generated/homeLambdasClient"; import { DataGrid } from "@mui/x-data-grid"; import { useEffect, useState } from "react"; import { useLambdasApi } from "../../hooks/use-api"; @@ -17,6 +17,7 @@ interface Props { project: Projects; loggedInPersonId?: number; filter?: string; + avatars?: UsersAvatars[]; } /** @@ -24,13 +25,13 @@ interface Props { * * @param props component properties */ -const TaskTable = ({ project, loggedInPersonId, filter }: Props) => { +const TaskTable = ({ project, loggedInPersonId, filter, avatars }: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const columns = sprintViewTasksColumns({ tasks, timeEntries }); + const columns = sprintViewTasksColumns({ tasks, timeEntries, avatars }); const setError = useSetAtom(errorAtom); const [reload, setReload] = useState(false); From 53c563b9daf96745b1a15b37cfa4c1dcea1a4927 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 27 May 2024 11:38:33 +0300 Subject: [PATCH 26/32] update spec --- home-lambdas-API-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home-lambdas-API-spec b/home-lambdas-API-spec index 56434fed..9a6fc410 160000 --- a/home-lambdas-API-spec +++ b/home-lambdas-API-spec @@ -1 +1 @@ -Subproject commit 56434fedbcdd97e5eff82630162bae07403bad5b +Subproject commit 9a6fc410ac14b0ee81fe96dd9dee6982f13b3ce1 From a0d8e8a9a5a68dd83dc965d66904bf9b8377bd04 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 27 May 2024 15:32:22 +0300 Subject: [PATCH 27/32] fixed rule position in biome --- biome.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/biome.json b/biome.json index 93a80105..ec62a7d3 100644 --- a/biome.json +++ b/biome.json @@ -12,11 +12,11 @@ "noForEach": "off" }, "suspicious": { - "noExplicitAny": "off", - "useExhaustiveDependencies": "off" + "noExplicitAny": "off" }, "correctness": { - "noUnusedVariables": "error" + "noUnusedVariables": "error", + "useExhaustiveDependencies": "off" } }, "ignore": ["/generated"] From 128673aef1edf75c313cd7e4dfeed8f376cb0081 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Mon, 27 May 2024 15:40:21 +0300 Subject: [PATCH 28/32] fixed rule position in biome and added indentation changes --- biome.json | 70 +++++++++---------- src/components/screens/sprint-view-screen.tsx | 26 +++++-- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/biome.json b/biome.json index ec62a7d3..27712b22 100644 --- a/biome.json +++ b/biome.json @@ -1,37 +1,37 @@ { - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "complexity": { - "noUselessFragments": "error", - "noForEach": "off" - }, - "suspicious": { - "noExplicitAny": "off" - }, - "correctness": { - "noUnusedVariables": "error", - "useExhaustiveDependencies": "off" - } - }, - "ignore": ["/generated"] - }, - "javascript": { - "formatter": { - "trailingComma": "none" - } - }, - "formatter": { - "enabled": true, - "lineWidth": 100, - "indentWidth": 2, - "indentStyle": "space", - "formatWithErrors": false, - "ignore": ["/generated"] - } + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessFragments": "error", + "noForEach": "off" + }, + "suspicious": { + "noExplicitAny": "off" + }, + "correctness": { + "noUnusedVariables": "error", + "useExhaustiveDependencies": "off" + } + }, + "ignore": ["/generated"] + }, + "javascript": { + "formatter": { + "trailingComma": "none" + } + }, + "formatter": { + "enabled": true, + "lineWidth": 100, + "indentWidth": 2, + "indentStyle": "space", + "formatWithErrors": false, + "ignore": ["/generated"] + } } diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 174c8277..3ea229cf 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -6,14 +6,23 @@ import { useAtomValue, useSetAtom } from "jotai"; import { personsAtom } from "src/atoms/person"; import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; -import type { Allocations, Projects, TimeEntries, UsersAvatars} from "src/generated/homeLambdasClient/models/"; +import type { + Allocations, + Projects, + TimeEntries, + UsersAvatars +} from "src/generated/homeLambdasClient/models/"; import { DataGrid } from "@mui/x-data-grid"; import { getHoursAndMinutes, getSprintEnd, getSprintStart } from "src/utils/time-utils"; import TaskTable from "src/components/sprint-view-table/tasks-table"; import strings from "src/localization/strings"; import sprintViewProjectsColumns from "src/components/sprint-view-table/sprint-projects-columns"; import { errorAtom } from "src/atoms/error"; -import { calculateWorkingLoad, totalAllocations, filterAllocationsAndProjects } from "src/utils/sprint-utils"; +import { + calculateWorkingLoad, + totalAllocations, + filterAllocationsAndProjects +} from "src/utils/sprint-utils"; import { TaskStatusFilter } from "src/components/sprint-view-table/menu-Item-filter-table"; /** @@ -53,7 +62,7 @@ const SprintViewScreen = () => { const fetchSlackAvatars = async () => { const fetchedAvatars = await slackAvatarsApi.slackAvatar(); setAvatars(fetchedAvatars); - } + }; /** * Fetch allocations, project names and time entries @@ -68,7 +77,10 @@ const SprintViewScreen = () => { personId: loggedInPerson?.id.toString() }); const fetchedProjects = await projectsApi.listProjects({ startDate: new Date() }); - const {filteredAllocations, filteredProjects} = filterAllocationsAndProjects(fetchedAllocations, fetchedProjects); + const { filteredAllocations, filteredProjects } = filterAllocationsAndProjects( + fetchedAllocations, + fetchedProjects + ); const fetchedTimeEntries = await Promise.all( filteredAllocations.map(async (allocation) => { try { @@ -223,7 +235,7 @@ const SprintViewScreen = () => { - {projects.map((project) => + {projects.map((project) => ( { filter={filter} avatars={avatars} /> - )} + ))} )} ); }; -export default SprintViewScreen; \ No newline at end of file +export default SprintViewScreen; From 1740c98b3df04b79b31e1e2622c6a3fbc9489dd3 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Tue, 28 May 2024 15:10:48 +0300 Subject: [PATCH 29/32] Fixed required changes, implemented changes related to sonarcloud and included avatarsAtom. Added avatar to navigation bar. --- src/atoms/person.ts | 2 + src/components/constants/index.ts | 4 +- src/components/layout/navbar.tsx | 16 +++- .../providers/authentication-provider.tsx | 19 ++++- src/components/screens/sprint-view-screen.tsx | 19 +---- .../menu-Item-filter-table.tsx | 83 +++++++++--------- .../sprint-tasks-columns.tsx | 59 ++----------- .../sprint-view-table/tasks-table.tsx | 24 +++--- .../user-avatars-component.tsx | 84 +++++++++++++++++++ .../toolbar-form/toolbar-form-fields.tsx | 6 +- src/localization/en.json | 3 +- src/localization/fi.json | 3 +- src/localization/strings.ts | 1 + 13 files changed, 193 insertions(+), 130 deletions(-) create mode 100644 src/components/sprint-view-table/user-avatars-component.tsx diff --git a/src/atoms/person.ts b/src/atoms/person.ts index 6f52be4d..cf164027 100644 --- a/src/atoms/person.ts +++ b/src/atoms/person.ts @@ -1,6 +1,7 @@ import { atom } from "jotai"; import { DailyEntry, Person, PersonTotalTime } from "../generated/client"; import { DailyEntryWithIndexSignature, PersonWithTotalTime } from "../types"; +import { UsersAvatars } from "src/generated/homeLambdasClient"; export const personsAtom = atom([]); export const personTotalTimeAtom = atom(undefined); @@ -8,3 +9,4 @@ export const personsWithTotalTimeAtom = atom([]); export const timebankScreenPersonTotalTimeAtom = atom(undefined); export const personDailyEntryAtom = atom(undefined); export const dailyEntriesAtom = atom([]); +export const avatarsAtom = atom([]); \ No newline at end of file diff --git a/src/components/constants/index.ts b/src/components/constants/index.ts index 17d9505a..bc35072d 100644 --- a/src/components/constants/index.ts +++ b/src/components/constants/index.ts @@ -12,12 +12,12 @@ export const COLORS = [ /** * Days of week array. * */ -export const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; +export const DAYS_OF_WEEK = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; /** * Task statuses. * */ -export const Status = { +export const STATUS = { TODO: "TODO", INPROGRESS: "INPROGRESS", DONE: "DONE", diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index 41bdcf27..88bfd904 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -2,10 +2,14 @@ import { MouseEvent, useState } from "react"; import {MenuItem, AppBar, Box, Toolbar, IconButton, Menu, Container, Tooltip, Avatar } from "@mui/material"; import LocalizationButtons from "../layout-components/localization-buttons"; import strings from "src/localization/strings"; -import { authAtom } from "src/atoms/auth"; +import { authAtom, userProfileAtom } from "src/atoms/auth"; import { useAtomValue } from "jotai"; import NavItems from "./navitems"; import SyncButton from "./sync-button"; +import { avatarsAtom, personsAtom } from "src/atoms/person"; +import { UsersAvatars } from "src/generated/homeLambdasClient"; +import { Person } from "src/generated/client"; +import config from "src/app/config"; /** * NavBar component @@ -13,6 +17,14 @@ import SyncButton from "./sync-button"; const NavBar = () => { const auth = useAtomValue(authAtom); const [anchorElUser, setAnchorElUser] = useState(null); + const avatars: UsersAvatars[] = useAtomValue(avatarsAtom); + const persons: Person[] = useAtomValue(personsAtom); + const userProfile = useAtomValue(userProfileAtom); + const loggedInPerson = persons.find( + (person: Person) => + person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id + ); + const loggedInPersonAvatar = avatars.find(avatar => loggedInPerson?.id===avatar.personId)?.imageOriginal||""; /** * Handles opening user menu @@ -48,7 +60,7 @@ const NavBar = () => { - + { const [auth, setAuth] = useAtom(authAtom); const setUserProfile = useSetAtom(userProfileAtom); const setPersons = useSetAtom(personsAtom); + const setAvatars = useSetAtom(avatarsAtom); const { personsApi } = useApi(); + const { slackAvatarsApi } = useLambdasApi(); const updateAuthData = useCallback(() => { setAuth({ @@ -84,8 +86,19 @@ const AuthenticationProvider = ({ children }: Props) => { setPersons(fetchedPersons); }; + /** + * fetchs avatars + */ + const getSlackAvatars = async () => { + const fetchedAvatars = await slackAvatarsApi.slackAvatar(); + setAvatars(fetchedAvatars); + }; + useEffect(() => { - if (auth) getPersonsList(); + if (auth) { + getPersonsList(); + getSlackAvatars(); + } }, [auth]); /** diff --git a/src/components/screens/sprint-view-screen.tsx b/src/components/screens/sprint-view-screen.tsx index 3ea229cf..6805a8de 100644 --- a/src/components/screens/sprint-view-screen.tsx +++ b/src/components/screens/sprint-view-screen.tsx @@ -9,8 +9,7 @@ import { userProfileAtom } from "src/atoms/auth"; import type { Allocations, Projects, - TimeEntries, - UsersAvatars + TimeEntries } from "src/generated/homeLambdasClient/models/"; import { DataGrid } from "@mui/x-data-grid"; import { getHoursAndMinutes, getSprintEnd, getSprintStart } from "src/utils/time-utils"; @@ -29,7 +28,7 @@ import { TaskStatusFilter } from "src/components/sprint-view-table/menu-Item-fil * Sprint view screen component */ const SprintViewScreen = () => { - const { allocationsApi, projectsApi, timeEntriesApi, slackAvatarsApi } = useLambdasApi(); + const { allocationsApi, projectsApi, timeEntriesApi } = useLambdasApi(); const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); const loggedInPerson = persons.find( @@ -39,7 +38,6 @@ const SprintViewScreen = () => { const [allocations, setAllocations] = useState([]); const [projects, setProjects] = useState([]); const [timeEntries, setTimeEntries] = useState([]); - const [avatars, setAvatars] = useState([]); const [loading, setLoading] = useState(false); const [myTasks, setMyTasks] = useState(true); const [filter, setFilter] = useState(""); @@ -56,20 +54,13 @@ const SprintViewScreen = () => { fetchProjectDetails(); }, [loggedInPerson]); - /** - * fetch avatars - */ - const fetchSlackAvatars = async () => { - const fetchedAvatars = await slackAvatarsApi.slackAvatar(); - setAvatars(fetchedAvatars); - }; - /** * Fetch allocations, project names and time entries */ const fetchProjectDetails = async () => { setLoading(true); if (!loggedInPerson) return; + try { const fetchedAllocations = await allocationsApi.listAllocations({ startDate: new Date(), @@ -113,7 +104,6 @@ const SprintViewScreen = () => { return 0; }) ); - await fetchSlackAvatars(); setProjects(filteredProjects); setAllocations(filteredAllocations); setTimeEntries(fetchedTimeEntries); @@ -174,7 +164,7 @@ const SprintViewScreen = () => { label={strings.sprint.showMyTasks} onClick={() => handleOnClickTask()} /> - {TaskStatusFilter(setFilter)} + { project={project} loggedInPersonId={myTasks ? loggedInPerson?.id : undefined} filter={filter} - avatars={avatars} /> ))} diff --git a/src/components/sprint-view-table/menu-Item-filter-table.tsx b/src/components/sprint-view-table/menu-Item-filter-table.tsx index 0be098b2..b5644f08 100644 --- a/src/components/sprint-view-table/menu-Item-filter-table.tsx +++ b/src/components/sprint-view-table/menu-Item-filter-table.tsx @@ -1,44 +1,47 @@ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import strings from 'src/localization/strings'; -import { Status } from '../constants'; +import { STATUS } from '../constants'; - interface Filter { - key: number; - value: string; - label: string; - } - - export const TaskStatusFilter = (setFilter: (string: string) => void) => { - - const statusFilters : Filter[] = [ - { key: 1, value: Status.TODO, label: strings.sprint.toDo }, - { key: 2, value: Status.INPROGRESS, label: strings.sprint.inProgress }, - { key: 3, value: Status.DONE, label: strings.sprint.completed }, - { key: 4, value: Status.ALL, label: strings.sprint.allTasks } - ]; +/** + * Component properties + */ +interface Props { + setFilter: (string: string) => void; +} - return ( - - {strings.sprint.taskStatus} - - - ) - } \ No newline at end of file +/** + * Filters tasks by status categories + */ +export const TaskStatusFilter = ({setFilter}: Props) => { + const statusFilters = [ + { key: 1, value: STATUS.TODO, label: strings.sprint.toDo }, + { key: 2, value: STATUS.INPROGRESS, label: strings.sprint.inProgress }, + { key: 3, value: STATUS.DONE, label: strings.sprint.completed }, + { key: 4, value: STATUS.ALL, label: strings.sprint.allTasks } + ]; + + return ( + + {strings.sprint.taskStatus} + + + ) +} \ No newline at end of file diff --git a/src/components/sprint-view-table/sprint-tasks-columns.tsx b/src/components/sprint-view-table/sprint-tasks-columns.tsx index d5de22bc..b7f47ba5 100644 --- a/src/components/sprint-view-table/sprint-tasks-columns.tsx +++ b/src/components/sprint-view-table/sprint-tasks-columns.tsx @@ -1,20 +1,16 @@ import type { GridColDef } from "@mui/x-data-grid"; import strings from "../../localization/strings"; import { getHoursAndMinutes } from "../../utils/time-utils"; -import type { Tasks, UsersAvatars } from "../../generated/homeLambdasClient"; +import type { Tasks } from "../../generated/homeLambdasClient"; import { getTotalTimeEntriesTasks } from "src/utils/sprint-utils"; -import { Avatar, AvatarGroup } from "@mui/material"; -import Tooltip from '@mui/material/Tooltip'; -import { personsAtom } from "src/atoms/person"; -import { Person } from "src/generated/client"; -import { useAtomValue } from "jotai"; +import UserAvatars from "./user-avatars-component"; + /** * Component properties */ interface Props { tasks: Tasks[]; timeEntries: number[]; - avatars?: UsersAvatars[]; } /** @@ -22,7 +18,7 @@ interface Props { * * @param props component properties */ -const sprintViewTasksColumns = ({ tasks, timeEntries, avatars }: Props) => { +const sprintViewTasksColumns = ({ tasks, timeEntries }: Props) => { /** * Define columns for data grid */ @@ -39,48 +35,11 @@ const sprintViewTasksColumns = ({ tasks, timeEntries, avatars }: Props) => { headerClassName: "header-color", headerName: strings.sprint.assigned, flex: 1, - renderCell: (params) => { - const persons: Person[] = useAtomValue(personsAtom); - return ( - - {params.row.assignedPersons.map((personId: number, index: number) => { - const avatar = avatars && avatars.find(avatar => avatar.personId === personId); - const person = persons && persons.find(person => person.id === personId); - const maxAvatarsInLine = 3; - if (index - - - ); - } - if (index === maxAvatarsInLine && params.row.assignedPersons.length-maxAvatarsInLine > 0) { - let tooltipTitile = ""; - params.row.assignedPersons.slice(maxAvatarsInLine).map((personId: number) => { - const personFound = persons.find(person => person.id === personId); - tooltipTitile += `${personFound?.firstName} ${personFound?.lastName}, `; - }) - tooltipTitile = tooltipTitile.slice(0, tooltipTitile.length - 2); - if (params.row.assignedPersons.length-maxAvatarsInLine === 1) { - return ( - - - - ) - } - return ( - - ) - } - })} - - ) - } + renderCell: (params) => ( + + ) }, { field: "status", diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index 9c028752..a71384d0 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -25,13 +25,13 @@ interface Props { * * @param props component properties */ -const TaskTable = ({ project, loggedInPersonId, filter, avatars }: Props) => { +const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const columns = sprintViewTasksColumns({ tasks, timeEntries, avatars }); + const columns = sprintViewTasksColumns({ tasks, timeEntries }); const setError = useSetAtom(errorAtom); const [reload, setReload] = useState(false); @@ -84,16 +84,14 @@ const TaskTable = ({ project, loggedInPersonId, filter, avatars }: Props) => { return totalMinutes; } } catch (error) { - if (task.id) { - const message: string = strings - .formatString( - strings.sprintRequestError.fetchTasksError, - task.id || 0, - error as string - ) - .toString(); - setError(message); - } + const message: string = strings + .formatString( + strings.sprintRequestError.fetchTasksError, + task.id || `${strings.sprintRequestError.fetchTaskIdError}`, + error as string + ) + .toString(); + setError(message); } return 0; }) @@ -177,4 +175,4 @@ const TaskTable = ({ project, loggedInPersonId, filter, avatars }: Props) => { ); }; -export default TaskTable; \ No newline at end of file +export default TaskTable; diff --git a/src/components/sprint-view-table/user-avatars-component.tsx b/src/components/sprint-view-table/user-avatars-component.tsx new file mode 100644 index 00000000..02fb2dac --- /dev/null +++ b/src/components/sprint-view-table/user-avatars-component.tsx @@ -0,0 +1,84 @@ +import { Avatar, AvatarGroup, Tooltip } from "@mui/material"; +import { useAtomValue } from "jotai"; +import { avatarsAtom, personsAtom } from "src/atoms/person"; +import type { Person } from "src/generated/client"; +import type { UsersAvatars } from "src/generated/homeLambdasClient"; + +/** + * Component properties + */ +interface Props { + assignedPersons: number[]; +} + +const UserAvatars = ({ assignedPersons }: Props) => { + const persons: Person[] = useAtomValue(personsAtom); + const avatars: UsersAvatars[] = useAtomValue(avatarsAtom); + const maxAvatarsInLine = 3; + + return ( + + {renderAvatars(assignedPersons, persons, avatars, maxAvatarsInLine)} + + ); +}; + +/** + * Render Slack Avatars + * + * @param assignedPersons list of all persons + * @param avatars list of exsisting avatars + * @param persons list of persons assigned to the project + * @param maxAvatarsInLine avatars limitation per table cell + */ +const renderAvatars = ( + assignedPersons: number[], + persons: Person[], + avatars: UsersAvatars[], + maxAvatarsInLine: number +) => { + return assignedPersons.map((personId: number, index: number) => { + const avatar = avatars?.find((avatar) => avatar.personId === personId); + const person = persons?.find((person) => person.id === personId); + const numberOfAssignedPersons = assignedPersons.length; + + if (index < maxAvatarsInLine) { + return ( + + + + ); + } + + if (index === maxAvatarsInLine && numberOfAssignedPersons - maxAvatarsInLine > 0) { + const groupedPersons = assignedPersons.slice(maxAvatarsInLine); + let tooltipTitile = ""; + + groupedPersons.forEach((groupedPersonId: number) => { + const personFound = persons.find((person: { id: number }) => person.id === groupedPersonId); + if (personFound) { + tooltipTitile += `${personFound?.firstName} ${personFound?.lastName}, `; + } + }); + tooltipTitile = tooltipTitile.slice(0, tooltipTitile.length - 2); + if (numberOfAssignedPersons - maxAvatarsInLine === 1) { + return ( + + + + ); + } + return ( + + ); + } + }); +}; + +export default UserAvatars; diff --git a/src/components/vacation-requests-table/vacation-requests-table-toolbar/toolbar-form/toolbar-form-fields.tsx b/src/components/vacation-requests-table/vacation-requests-table-toolbar/toolbar-form/toolbar-form-fields.tsx index 43dcae14..c17b2505 100644 --- a/src/components/vacation-requests-table/vacation-requests-table-toolbar/toolbar-form/toolbar-form-fields.tsx +++ b/src/components/vacation-requests-table/vacation-requests-table-toolbar/toolbar-form/toolbar-form-fields.tsx @@ -13,7 +13,7 @@ import config from "src/app/config"; import { userProfileAtom } from "src/atoms/auth"; import { personsAtom } from "src/atoms/person"; import { VacationType, Person } from "src/generated/client"; -import { daysOfWeek } from "src/components/constants"; +import { DAYS_OF_WEEK } from "src/components/constants"; /** * Component properties @@ -88,10 +88,10 @@ const ToolbarFormFields = ({ * @param loggedInPerson Person */ const getWorkingWeek = (loggedInPerson?: Person) => { - const workingWeek = new Array(daysOfWeek.length).fill(false); + const workingWeek = new Array(DAYS_OF_WEEK.length).fill(false); if (!loggedInPerson) return workingWeek; - daysOfWeek.forEach((weekDay, index)=>{ + DAYS_OF_WEEK.forEach((weekDay, index)=>{ if (loggedInPerson[weekDay as keyof typeof loggedInPerson] !== 0) { workingWeek[index] = true; } diff --git a/src/localization/en.json b/src/localization/en.json index 57a8c176..459bbdb1 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -160,7 +160,8 @@ "fetchError": "Fetching sprint data requests has failed", "fetchTimeEntriesError": "Error fetching time entries", "fetchAllocationError": "Error fetching time entries for allocation {0}: {1}", - "fetchTasksError": "Error fetching time entries for task {0}: {1}" + "fetchTasksError": "Error fetching time entries for task {0}: {1}", + "fetchTaskIdError": "Task id not found" }, "form": { "submit": "Submit", diff --git a/src/localization/fi.json b/src/localization/fi.json index afae9984..c678cd9a 100644 --- a/src/localization/fi.json +++ b/src/localization/fi.json @@ -160,7 +160,8 @@ "fetchError": "Sprintin tietopyyntöjen hakeminen epäonnistui", "fetchTimeEntriesError": "Virhe aikamerkintöjen hakemisessa", "fetchAllocationError": "Virhe aikamerkintöjen hakemisessa jakamista varten {0}: {1}", - "fetchTasksError": "Tehtävän {0} aikamerkintöjen noutovirhe: {1}" + "fetchTasksError": "Tehtävän {0} aikamerkintöjen noutovirhe: {1}", + "fetchTaskIdError": "Tehtävän tunnusta ei löytynyt" }, "form": { "submit": "Lähetä", diff --git a/src/localization/strings.ts b/src/localization/strings.ts index 3f139672..189914ae 100644 --- a/src/localization/strings.ts +++ b/src/localization/strings.ts @@ -189,6 +189,7 @@ export interface Localized extends LocalizedStringsMethods { fetchTimeEntriesError: string; fetchAllocationError: string; fetchTasksError: string; + fetchTaskIdError: string; }; /** * Translations related to form From ad0c8da756ecdf9e467795fd43d830d48294e621 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Tue, 28 May 2024 15:20:22 +0300 Subject: [PATCH 30/32] deleted unused code --- src/components/sprint-view-table/tasks-table.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index a71384d0..f3017c93 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -1,5 +1,5 @@ import { Box, Card, CircularProgress, IconButton, Typography } from "@mui/material"; -import { Projects, Tasks, TimeEntries, UsersAvatars } from "../../generated/homeLambdasClient"; +import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; import { DataGrid } from "@mui/x-data-grid"; import { useEffect, useState } from "react"; import { useLambdasApi } from "../../hooks/use-api"; @@ -17,7 +17,6 @@ interface Props { project: Projects; loggedInPersonId?: number; filter?: string; - avatars?: UsersAvatars[]; } /** From 71e3519ffebe4f99df7466c61940c5805c652c77 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Tue, 28 May 2024 16:03:14 +0300 Subject: [PATCH 31/32] deleted setReload --- src/components/constants/index.ts | 25 +++++++++++----- .../sprint-view-table/tasks-table.tsx | 30 ++++++++++--------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/constants/index.ts b/src/components/constants/index.ts index bc35072d..5cc03321 100644 --- a/src/components/constants/index.ts +++ b/src/components/constants/index.ts @@ -4,22 +4,31 @@ import { theme } from "src/theme"; * Colors array to be mapped in elements such as the pie chart. */ export const COLORS = [ - theme.palette.success.dark, - theme.palette.success.light, - theme.palette.warning.main - ]; + theme.palette.success.dark, + theme.palette.success.light, + theme.palette.warning.main +]; /** * Days of week array. - * */ -export const DAYS_OF_WEEK = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + * */ +export const DAYS_OF_WEEK = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" +]; +// TODO: These statuses should be updated in the future to include all statuses from the forecast data. /** * Task statuses. - * */ + * */ export const STATUS = { TODO: "TODO", INPROGRESS: "INPROGRESS", DONE: "DONE", ALL: "" -}; \ No newline at end of file +}; diff --git a/src/components/sprint-view-table/tasks-table.tsx b/src/components/sprint-view-table/tasks-table.tsx index f3017c93..7a4f3f41 100644 --- a/src/components/sprint-view-table/tasks-table.tsx +++ b/src/components/sprint-view-table/tasks-table.tsx @@ -1,13 +1,13 @@ import { Box, Card, CircularProgress, IconButton, Typography } from "@mui/material"; -import { Projects, Tasks, TimeEntries } from "../../generated/homeLambdasClient"; +import { Projects, Tasks, TimeEntries } from "src/generated/homeLambdasClient"; import { DataGrid } from "@mui/x-data-grid"; import { useEffect, useState } from "react"; -import { useLambdasApi } from "../../hooks/use-api"; +import { useLambdasApi } from "src/hooks/use-api"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import strings from "../../localization/strings"; +import strings from "src/localization/strings"; import sprintViewTasksColumns from "./sprint-tasks-columns"; -import { errorAtom } from "../../atoms/error"; +import { errorAtom } from "src/atoms/error"; import { useSetAtom } from "jotai"; /** @@ -24,7 +24,7 @@ interface Props { * * @param props component properties */ -const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { +const TaskTable = ({ project, loggedInPersonId, filter }: Props) => { const { tasksApi, timeEntriesApi } = useLambdasApi(); const [tasks, setTasks] = useState([]); const [timeEntries, setTimeEntries] = useState([]); @@ -32,7 +32,6 @@ const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { const [loading, setLoading] = useState(false); const columns = sprintViewTasksColumns({ tasks, timeEntries }); const setError = useSetAtom(errorAtom); - const [reload, setReload] = useState(false); /** * Gather tasks and time entries when project is available and update reload state @@ -41,13 +40,14 @@ const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { if (project && open) { getTasksAndTimeEntries(); } - }, [project, open, filter, reload]); + }, [project, open, filter]); /** * Handle loggenInPersonId change */ useEffect(() => { - setReload(true); + setTasks([]); + setOpen(false); }, [loggedInPersonId]); /** @@ -55,12 +55,15 @@ const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { */ const getTasksAndTimeEntries = async () => { setLoading(true); - if (reload || !tasks.length || !timeEntries.length) { + if (!tasks.length || !timeEntries.length) { try { - const fetchedTasks = !loggedInPersonId ? await tasksApi.listProjectTasks({ projectId: project.id }) - : await tasksApi.listProjectTasks({ projectId: project.id }).then(result => result.filter((task) => - task.assignedPersons?.includes(loggedInPersonId || 0) - )); + const fetchedTasks = !loggedInPersonId + ? await tasksApi.listProjectTasks({ projectId: project.id }) + : await tasksApi + .listProjectTasks({ projectId: project.id }) + .then((result) => + result.filter((task) => task.assignedPersons?.includes(loggedInPersonId || 0)) + ); setTasks(fetchedTasks); const fetchedTimeEntries = await Promise.all( @@ -100,7 +103,6 @@ const TaskTable = ({ project, loggedInPersonId, filter}: Props) => { setError(`${strings.sprintRequestError.fetchTimeEntriesError} ${error}`); } } - setReload(false); setLoading(false); }; From dc558d4a58a20227107df99fa1ff4869184f3469 Mon Sep 17 00:00:00 2001 From: DZotoff Date: Wed, 29 May 2024 13:10:48 +0300 Subject: [PATCH 32/32] Added changes --- src/components/layout/navbar.tsx | 36 +++++++++++++++---- .../providers/authentication-provider.tsx | 15 ++------ .../user-avatars-component.tsx | 27 ++++++++------ 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index 88bfd904..01d358d6 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -1,15 +1,25 @@ -import { MouseEvent, useState } from "react"; -import {MenuItem, AppBar, Box, Toolbar, IconButton, Menu, Container, Tooltip, Avatar } from "@mui/material"; +import { MouseEvent, useEffect, useState } from "react"; +import { + MenuItem, + AppBar, + Box, + Toolbar, + IconButton, + Menu, + Container, + Tooltip, + Avatar +} from "@mui/material"; import LocalizationButtons from "../layout-components/localization-buttons"; import strings from "src/localization/strings"; import { authAtom, userProfileAtom } from "src/atoms/auth"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import NavItems from "./navitems"; import SyncButton from "./sync-button"; import { avatarsAtom, personsAtom } from "src/atoms/person"; -import { UsersAvatars } from "src/generated/homeLambdasClient"; import { Person } from "src/generated/client"; import config from "src/app/config"; +import { useLambdasApi } from "src/hooks/use-api"; /** * NavBar component @@ -17,14 +27,16 @@ import config from "src/app/config"; const NavBar = () => { const auth = useAtomValue(authAtom); const [anchorElUser, setAnchorElUser] = useState(null); - const avatars: UsersAvatars[] = useAtomValue(avatarsAtom); + const [avatars, setAvatars] = useAtom(avatarsAtom); const persons: Person[] = useAtomValue(personsAtom); const userProfile = useAtomValue(userProfileAtom); + const { slackAvatarsApi } = useLambdasApi(); const loggedInPerson = persons.find( (person: Person) => person.id === config.person.forecastUserIdOverride || person.keycloakId === userProfile?.id ); - const loggedInPersonAvatar = avatars.find(avatar => loggedInPerson?.id===avatar.personId)?.imageOriginal||""; + const loggedInPersonAvatar = + avatars.find((avatar) => loggedInPerson?.id === avatar.personId)?.imageOriginal || ""; /** * Handles opening user menu @@ -49,6 +61,18 @@ const NavBar = () => { auth?.logout(); }; + /** + * fetch avatars + */ + const getSlackAvatars = async () => { + const fetchedAvatars = await slackAvatarsApi.slackAvatar(); + setAvatars(fetchedAvatars); + }; + + useEffect(() => { + getSlackAvatars(); + }, [loggedInPerson]); + return ( <> diff --git a/src/components/providers/authentication-provider.tsx b/src/components/providers/authentication-provider.tsx index 234cf71a..37bed786 100644 --- a/src/components/providers/authentication-provider.tsx +++ b/src/components/providers/authentication-provider.tsx @@ -3,8 +3,8 @@ import { authAtom, userProfileAtom } from "src/atoms/auth"; import { useAtom, useSetAtom } from "jotai"; import Keycloak from "keycloak-js"; import { ReactNode, useCallback, useEffect } from "react"; -import { avatarsAtom, personsAtom } from "src/atoms/person"; -import { useApi, useLambdasApi } from "src/hooks/use-api"; +import { personsAtom } from "src/atoms/person"; +import { useApi } from "src/hooks/use-api"; interface Props { children: ReactNode; @@ -19,9 +19,7 @@ const AuthenticationProvider = ({ children }: Props) => { const [auth, setAuth] = useAtom(authAtom); const setUserProfile = useSetAtom(userProfileAtom); const setPersons = useSetAtom(personsAtom); - const setAvatars = useSetAtom(avatarsAtom); const { personsApi } = useApi(); - const { slackAvatarsApi } = useLambdasApi(); const updateAuthData = useCallback(() => { setAuth({ @@ -86,18 +84,9 @@ const AuthenticationProvider = ({ children }: Props) => { setPersons(fetchedPersons); }; - /** - * fetchs avatars - */ - const getSlackAvatars = async () => { - const fetchedAvatars = await slackAvatarsApi.slackAvatar(); - setAvatars(fetchedAvatars); - }; - useEffect(() => { if (auth) { getPersonsList(); - getSlackAvatars(); } }, [auth]); diff --git a/src/components/sprint-view-table/user-avatars-component.tsx b/src/components/sprint-view-table/user-avatars-component.tsx index 02fb2dac..de8f1f55 100644 --- a/src/components/sprint-view-table/user-avatars-component.tsx +++ b/src/components/sprint-view-table/user-avatars-component.tsx @@ -11,6 +11,9 @@ interface Props { assignedPersons: number[]; } +/** + * List of avatars component + */ const UserAvatars = ({ assignedPersons }: Props) => { const persons: Person[] = useAtomValue(personsAtom); const avatars: UsersAvatars[] = useAtomValue(avatarsAtom); @@ -30,8 +33,8 @@ const UserAvatars = ({ assignedPersons }: Props) => { /** * Render Slack Avatars * - * @param assignedPersons list of all persons - * @param avatars list of exsisting avatars + * @param assignedPersons list of all persons + * @param avatars list of exsisting avatars * @param persons list of persons assigned to the project * @param maxAvatarsInLine avatars limitation per table cell */ @@ -45,6 +48,7 @@ const renderAvatars = ( const avatar = avatars?.find((avatar) => avatar.personId === personId); const person = persons?.find((person) => person.id === personId); const numberOfAssignedPersons = assignedPersons.length; + const hiddenAssignedPersons = numberOfAssignedPersons - maxAvatarsInLine; if (index < maxAvatarsInLine) { return ( @@ -54,27 +58,30 @@ const renderAvatars = ( ); } - if (index === maxAvatarsInLine && numberOfAssignedPersons - maxAvatarsInLine > 0) { + if (index === maxAvatarsInLine && hiddenAssignedPersons > 0) { const groupedPersons = assignedPersons.slice(maxAvatarsInLine); - let tooltipTitile = ""; + let tooltipTitle = ""; groupedPersons.forEach((groupedPersonId: number) => { const personFound = persons.find((person: { id: number }) => person.id === groupedPersonId); if (personFound) { - tooltipTitile += `${personFound?.firstName} ${personFound?.lastName}, `; + tooltipTitle += `${personFound?.firstName} ${personFound?.lastName}, `; } }); - tooltipTitile = tooltipTitile.slice(0, tooltipTitile.length - 2); - if (numberOfAssignedPersons - maxAvatarsInLine === 1) { + tooltipTitle = tooltipTitle.slice(0, tooltipTitle.length - 2); + if (hiddenAssignedPersons === 1) { return ( - + ); } return ( -