diff --git a/.cspell.json b/.cspell.json index f35fdc724..140cce74b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -326,7 +326,8 @@ "Screenshoot", "screenshoots", "tota", - "Intervall" + "Intervall", + "Timesheets" ], "useGitignore": true, "ignorePaths": [ diff --git a/apps/web/app/[locale]/task/[id]/page.tsx b/apps/web/app/[locale]/task/[id]/page.tsx index b2e0dd908..ac2cf1be3 100644 --- a/apps/web/app/[locale]/task/[id]/page.tsx +++ b/apps/web/app/[locale]/task/[id]/page.tsx @@ -85,7 +85,7 @@ const TaskDetails = () => { {/* */} {/* */} - + {task && }
diff --git a/apps/web/app/api/timer/timesheet/route.ts b/apps/web/app/api/timer/timesheet/route.ts index 4ab1a8c61..b014675d5 100644 --- a/apps/web/app/api/timer/timesheet/route.ts +++ b/apps/web/app/api/timer/timesheet/route.ts @@ -1,8 +1,27 @@ import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app'; +import { taskActivityRequest } from '@app/services/server/requests'; import { NextResponse } from 'next/server'; export async function GET(req: Request) { const res = new NextResponse(); - const { $res, user } = await authenticatedGuard(req, res); - if (!user) return $res('unauthorized'); + const { $res, user, tenantId, organizationId, access_token } = await authenticatedGuard(req, res); + if (!user) return $res('Unauthorized'); + + const { searchParams } = new URL(req.url); + + const { taskId } = searchParams as unknown as { + taskId: string; + }; + + const { data } = await taskActivityRequest( + { + 'taskIds[0]': taskId, + tenantId, + organizationId, + defaultRange: 'false' + }, + access_token + ); + + return $res(data); } diff --git a/apps/web/app/helpers/array-data.ts b/apps/web/app/helpers/array-data.ts index ac0e57c77..d96877bf5 100644 --- a/apps/web/app/helpers/array-data.ts +++ b/apps/web/app/helpers/array-data.ts @@ -1,6 +1,7 @@ import { ITimerSlot } from '@app/interfaces/timer/ITimerSlot'; import { pad } from './number'; import { ITimerApps } from '@app/interfaces/timer/ITimerApp'; +import { ITaskTimesheet } from '@app/interfaces'; export function groupDataByHour(data: ITimerSlot[]) { const groupedData: { startedAt: string; stoppedAt: string; items: ITimerSlot[] }[] = []; @@ -47,6 +48,26 @@ export function groupAppsByHour(apps: ITimerApps[]) { return groupedData.sort((a, b) => (new Date(a.hour) > new Date(b.hour) ? -1 : 1)); } +export function groupByTime(data: ITaskTimesheet[]) { + const groupedData: { date: string; items: ITaskTimesheet[] }[] = []; + + data.forEach((item) => { + const date = new Date(item.date).toDateString(); + + const dateDataIndex = groupedData.findIndex((el) => el.date == date); + + if (dateDataIndex !== -1) { + groupedData[dateDataIndex].items.push(item); + } else + groupedData.push({ + date, + items: [item] + }); + }); + + return groupedData.sort((a, b) => (new Date(a.date) > new Date(b.date) ? -1 : 1)); +} + const formatTime = (d: Date | string, addHour: boolean) => { d = d instanceof Date ? d : new Date(d); if (addHour) diff --git a/apps/web/app/hooks/features/useTaskActivity.ts b/apps/web/app/hooks/features/useTaskActivity.ts new file mode 100644 index 000000000..0645f8104 --- /dev/null +++ b/apps/web/app/hooks/features/useTaskActivity.ts @@ -0,0 +1,46 @@ +'use client'; + +import { useCallback, useEffect } from 'react'; +import { useQuery } from '../useQuery'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { useAuthenticateUser } from './useAuthenticateUser'; +import { useUserProfilePage } from './useUserProfilePage'; +import { activityTypeState } from '@app/stores/activity-type'; +import { taskTimesheetState } from '@app/stores/task-timesheet'; +import { getTaskTimesheetRequestAPI } from '@app/services/client/api'; + +export function useTaskTimeSheets(id: string) { + const { user } = useAuthenticateUser(); + const [taskTimesheets, setTaskTimesheets] = useRecoilState(taskTimesheetState); + const activityFilter = useRecoilValue(activityTypeState); + const profile = useUserProfilePage(); + + const { loading, queryCall } = useQuery(getTaskTimesheetRequestAPI); + const getTaskTimesheets = useCallback(() => { + if (activityFilter.member?.employeeId === user?.employee.id || user?.role?.name?.toUpperCase() == 'MANAGER') { + queryCall({ + tenantId: user?.tenantId ?? '', + organizationId: user?.employee.organizationId ?? '', + taskId: id + }).then((response) => { + console.log(response); + if (response.data) { + console.log(response.data); + setTaskTimesheets(response.data); + } + }); + } else setTaskTimesheets([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activityFilter.member?.employeeId, profile.member?.employeeId, user?.id, queryCall, setTaskTimesheets]); + + useEffect(() => { + getTaskTimesheets(); + }, [getTaskTimesheets]); + + return { + taskTimesheets, + getTaskTimesheets, + loading + }; +} diff --git a/apps/web/app/interfaces/ITaskTimesheet.ts b/apps/web/app/interfaces/ITaskTimesheet.ts new file mode 100644 index 000000000..d0e265f94 --- /dev/null +++ b/apps/web/app/interfaces/ITaskTimesheet.ts @@ -0,0 +1,24 @@ +import { IEmployee } from './IEmployee'; +import { IProject } from './IProject'; +import { ITeamTask } from './ITask'; +import { ITimerSlot } from './timer/ITimerSlot'; + +export interface ITaskTimesheet { + id: string + title: string; + description?: string; + metaData?: string; + date: Date; + time: string; + duration?: number; + type?: string; + source?: string; + employee?: IEmployee; + employeeId?: IEmployee['id']; + project?: IProject; + projectId?: IProject['id']; + timeSlot?: ITimerSlot; + timeSlotId?: ITimerSlot['id']; + task?: ITeamTask; + taskId?: ITeamTask['id']; +} diff --git a/apps/web/app/interfaces/index.ts b/apps/web/app/interfaces/index.ts index 11f63ed70..f6ba725be 100644 --- a/apps/web/app/interfaces/index.ts +++ b/apps/web/app/interfaces/index.ts @@ -13,6 +13,7 @@ export * from './ITaskStatus'; export * from './ITaskVersion'; export * from './ITaskPriorities'; export * from './ITaskSizes'; +export * from './ITaskTimesheet'; export * from './ITaskLabels'; export * from './ITaskRelatedIssueType'; export * from './IColor'; diff --git a/apps/web/app/services/client/api/activity/index.ts b/apps/web/app/services/client/api/activity/index.ts new file mode 100644 index 000000000..eacc92516 --- /dev/null +++ b/apps/web/app/services/client/api/activity/index.ts @@ -0,0 +1,37 @@ +import { ITaskTimesheet } from '@app/interfaces'; +import { get } from '@app/services/client/axios'; +import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; + +export async function getTaskTimesheetRequestAPI({ + taskId, + tenantId, + organizationId, + defaultRange, + unitOfTime +}: { + tenantId: string; + organizationId: string; + defaultRange?: string; + taskId?: string; + unitOfTime?: 'day'; +}) { + const params: { + tenantId: string; + organizationId: string; + defaultRange?: string; + 'taskIds[0]'?: string; + unitOfTime?: 'day'; + } = { + 'taskIds[0]': taskId, + tenantId, + organizationId, + defaultRange, + unitOfTime + }; + const query = new URLSearchParams(params); + const endpoint = GAUZY_API_BASE_SERVER_URL.value + ? `/timesheet/activity?${query.toString()}` + : `/timer/timesheet?${query.toString()}`; + + return get(endpoint); +} diff --git a/apps/web/app/services/client/api/index.ts b/apps/web/app/services/client/api/index.ts index 0564893be..7b8f36e10 100644 --- a/apps/web/app/services/client/api/index.ts +++ b/apps/web/app/services/client/api/index.ts @@ -32,5 +32,6 @@ export * from './organization-projects'; export * from './activity/time-slots'; export * from './activity/activity'; +export * from './activity'; export * from './default'; export * from './projects'; diff --git a/apps/web/app/services/client/api/organization-team.ts b/apps/web/app/services/client/api/organization-team.ts index 541cbbc59..3bbe1b66a 100644 --- a/apps/web/app/services/client/api/organization-team.ts +++ b/apps/web/app/services/client/api/organization-team.ts @@ -102,7 +102,7 @@ export async function getOrganizationTeamAPI(teamId: string, organizationId: str params[`relations[${i}]`] = rl; }); - const queries = new URLSearchParams(params || {}); + const queries = new URLSearchParams(params); const endpoint = `/organization-team/${teamId}?${queries.toString()}`; diff --git a/apps/web/app/services/server/requests/organization-team.ts b/apps/web/app/services/server/requests/organization-team.ts index e98ed6398..b473e7104 100644 --- a/apps/web/app/services/server/requests/organization-team.ts +++ b/apps/web/app/services/server/requests/organization-team.ts @@ -103,7 +103,7 @@ export function getOrganizationTeamRequest( params[`relations[${i}]`] = rl; }); - const queries = new URLSearchParams(params || {}); + const queries = new URLSearchParams(params); return serverFetch({ path: `/organization-team/${teamId}?${queries.toString()}`, method: 'GET', diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index 7b17116a2..4dfda5ce6 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -32,3 +32,22 @@ export function tasksTimesheetStatisticsRequest(params: TTasksTimesheetStatistic tenantId: params.tenantId }); } + +export type TTaskActivityParams = { + tenantId: string; + organizationId: string; + defaultRange?: string; + 'taskIds[0]'?: string; + unitOfTime?: 'day'; +}; + +export function taskActivityRequest(params: TTaskActivityParams, bearer_token: string) { + const queries = new URLSearchParams(params); + + return serverFetch({ + path: `/timesheet/activity?${queries.toString()}`, + method: 'GET', + bearer_token, + tenantId: params.tenantId + }); +} diff --git a/apps/web/app/stores/task-timesheet.ts b/apps/web/app/stores/task-timesheet.ts new file mode 100644 index 000000000..8bf26567c --- /dev/null +++ b/apps/web/app/stores/task-timesheet.ts @@ -0,0 +1,7 @@ +import { ITaskTimesheet } from '@app/interfaces'; +import { atom } from 'recoil'; + +export const taskTimesheetState = atom({ + key: 'taskTimesheetState', + default: [] +}); diff --git a/apps/web/components/pages/maintenance/index.tsx b/apps/web/components/pages/maintenance/index.tsx index 86eadab68..a23461fd9 100644 --- a/apps/web/components/pages/maintenance/index.tsx +++ b/apps/web/components/pages/maintenance/index.tsx @@ -6,8 +6,8 @@ function Maintenance() { const t = useTranslations(); return (
- -
+
+ Maintenance
diff --git a/apps/web/components/pages/task/details-section/blocks/task-estimations-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-estimations-info.tsx index 7c7be7f80..60a17cec9 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-estimations-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-estimations-info.tsx @@ -40,7 +40,7 @@ const TaskEstimationsInfo = () => {
- {task?.members.map((member) => { + {task?.members?.map((member) => { // TODO // Enable other users estimations in v2 return ( diff --git a/apps/web/lib/features/task/activity/user-task-activity.tsx b/apps/web/lib/features/task/activity/user-task-activity.tsx new file mode 100644 index 000000000..97cf47f23 --- /dev/null +++ b/apps/web/lib/features/task/activity/user-task-activity.tsx @@ -0,0 +1,44 @@ +import { clsxm } from '@app/utils'; +import { Tab } from '@headlessui/react'; +import { ActivityFilters } from '@app/constants'; + +export const UserTaskActivity = () => { + // get slots related to Task Id + // get apps visited related to Task Id + // get visited Sites related to Task Id + return ( +
+
+

{'Cedric medium'}

+ {'05:30'} +
+ + + {Object.values(ActivityFilters) + .filter((filter) => filter !== 'Tasks') + .map((filter: string) => ( + + clsxm( + 'w-full rounded-lg py-2.5 text-sm font-medium leading-5', + ' focus:outline-none focus:ring-2', + selected + ? 'bg-white dark:bg-dark text-blue-700 shadow' + : ' hover:bg-white/[0.50]' + ) + } + > + {filter} + + ))} + + + {'Screenshoot Team Tab'} + {'Apps Tab'} + {'VisitedSites Tab'} + + +
+ ); +}; diff --git a/apps/web/lib/features/task/task-activity.tsx b/apps/web/lib/features/task/task-activity.tsx index 110df0e89..a325be303 100644 --- a/apps/web/lib/features/task/task-activity.tsx +++ b/apps/web/lib/features/task/task-activity.tsx @@ -1,68 +1,54 @@ -import { clsxm } from '@app/utils'; -import { Tab } from '@headlessui/react'; +'use client'; + import { Card } from 'lib/components'; -import { ActivityFilters } from '@app/constants'; import React from 'react'; +import { UserTaskActivity } from './activity/user-task-activity'; +import { ITeamTask } from '@app/interfaces'; +import { useTaskTimeSheets } from '@app/hooks/features/useTaskActivity'; +import { groupByTime } from '@app/helpers/array-data'; + +export function TaskActivity({ task }: { task: ITeamTask }) { + // get users tasks + const { getTaskTimesheets, taskTimesheets } = useTaskTimeSheets(task?.id); + // order activity arr by Time + const groupedData = groupByTime(taskTimesheets); + console.log(groupedData); -export function TaskActivity() { + React.useEffect(() => { + getTaskTimesheets(); + }, [task, getTaskTimesheets]); return ( + {/* TO DELETE: start */}

{'05.01.2024'}

+ {/* TO DELETE: end */} + {groupedData.map((timesheet, i) => ( +
+

{timesheet.date}

+ {timesheet.items.map((item) => ( + + ))} +
+ ))} -
-

{'04.01.2024'}

- -
- + {/* TO DELETE: start */}

{'03.01.2024'}

+ {/* TO DELETE: end */}
); } - -const UserTaskActivity = () => { - return ( -
-
-

{'Cedric medium'}

- {'05:30'} -
- - - {Object.values(ActivityFilters).filter(filter => filter !== 'Tasks').map((filter: string) => ( - - clsxm( - 'w-full rounded-lg py-2.5 text-sm font-medium leading-5', - ' focus:outline-none focus:ring-2', - selected - ? 'bg-white dark:bg-dark text-blue-700 shadow' - : ' hover:bg-white/[0.50]' - ) - } - > - {filter} - - ))} - - - {'Screenshoot Team Tab'} - {'Apps Tab'} - {'VisitedSites Tab'} - - -
- ); -};