diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index 3a2b3b778..00c1ce3fa 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -8,13 +8,7 @@ import { clsxm } from '@app/utils'; import NoTeam from '@components/pages/main/no-team'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Card } from 'lib/components'; -import { - AuthUserTaskInput, - TeamInvitations, - TeamMembers, - Timer, - UnverifiedEmail -} from 'lib/features'; +import { AuthUserTaskInput, TeamInvitations, TeamMembers, Timer, UnverifiedEmail } from 'lib/features'; import { MainLayout } from 'lib/layout'; import { IssuesView } from '@app/constants'; import { useNetworkState } from '@uidotdev/usehooks'; @@ -35,163 +29,144 @@ import { headerTabs } from '@app/stores/header-tabs'; import { usePathname } from 'next/navigation'; import { PeoplesIcon } from 'assets/svg'; import TeamMemberHeader from 'lib/features/team-member-header'; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from '@components/ui/resizable'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; import { TeamOutstandingNotifications } from 'lib/features/team/team-outstanding-notifications'; function MainPage() { - const t = useTranslations(); - const [headerSize, setHeaderSize] = useState(10); - const { - isTeamMember, - isTrackingEnabled, - activeTeam - } = useOrganizationTeams(); - const [fullWidth, setFullWidth] = useAtom(fullWidthState); - const [view, setView] = useAtom(headerTabs); - const path = usePathname(); - const breadcrumb = [ - { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, - { title: activeTeam?.name || '', href: '/' }, - { title: t(`common.${view}`), href: `/` } - ]; - const { online } = useNetworkState(); - useEffect(() => { - if (view == IssuesView.KANBAN && path == '/') { - setView(IssuesView.CARDS); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [path, setView]); - - React.useEffect(() => { - window && window?.localStorage.getItem('conf-fullWidth-mode'); - setFullWidth( - JSON.parse(window?.localStorage.getItem('conf-fullWidth-mode') || 'true') - ); - }, [setFullWidth]); - - if (!online) { - return ; - } - return ( - <> -
- {/*
*/} - - -
- - {/* */} - setHeaderSize(size)} - > -
-
-
-
- - -
-
- -
-
-
-
- - - + const t = useTranslations(); + const [headerSize, setHeaderSize] = useState(10); + const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams(); + const [fullWidth, setFullWidth] = useAtom(fullWidthState); + const [view, setView] = useAtom(headerTabs); + const path = usePathname(); + const breadcrumb = [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: t(`common.${view}`), href: `/` } + ]; + const { online } = useNetworkState(); + useEffect(() => { + if (view == IssuesView.KANBAN && path == '/') { + setView(IssuesView.CARDS); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [path, setView]); + + React.useEffect(() => { + window && window?.localStorage.getItem('conf-fullWidth-mode'); + setFullWidth(JSON.parse(window?.localStorage.getItem('conf-fullWidth-mode') || 'true')); + }, [setFullWidth]); + + if (!online) { + return ; + } + return ( + <> +
+ {/*
*/} + + + +
+ + {/* */} + + setHeaderSize(size)} + > +
+
+
+
+ + + +
+ +
+ +
+
+ +
+
+ + + + + +
+ + {isTeamMember ? ( + + ) : null} +
+ +
+
+
+ + + + {/*
*/} + +
{isTeamMember ? : }
+
+
- {isTeamMember ? ( - - ) : null} -
- -
-
- - - {/* */} - -
- {isTeamMember ? ( - - ) : ( - - )} -
-
- -
- -
- - - ); + +
+ + + ); } -function TaskTimerSection({ - isTrackingEnabled -}: { - isTrackingEnabled: boolean; -}) { - const [showInput, setShowInput] = React.useState(false); - return ( - - -
setShowInput((p) => !p)} - className="border dark:border-[#26272C] w-full rounded p-2 md:hidden flex justify-center mt-2" - > - - {showInput ? 'hide the issue input' : 'show the issue input'} - -
- {isTrackingEnabled ? ( -
- -
- ) : null} -
- ); +function TaskTimerSection({ isTrackingEnabled }: { isTrackingEnabled: boolean }) { + const [showInput, setShowInput] = React.useState(false); + return ( + + +
setShowInput((p) => !p)} + className="border dark:border-[#26272C] w-full rounded p-2 md:hidden flex justify-center mt-2" + > + + {showInput ? 'hide the issue input' : 'show the issue input'} + +
+ {isTrackingEnabled ? ( +
+ +
+ ) : null} +
+ ); } export default withAuthentication(MainPage, { displayName: 'MainPage' }); diff --git a/apps/web/app/[locale]/team/[teamId]/[profileLink]/page.tsx b/apps/web/app/[locale]/team/[teamId]/[profileLink]/page.tsx index 62b2ef55c..b6ef5f98e 100644 --- a/apps/web/app/[locale]/team/[teamId]/[profileLink]/page.tsx +++ b/apps/web/app/[locale]/team/[teamId]/[profileLink]/page.tsx @@ -5,7 +5,7 @@ import { useRefreshIntervalV2 } from '@app/hooks'; import { usePublicOrganizationTeams } from '@app/hooks/features/usePublicOrganizationTeams'; import { publicState } from '@app/stores/public'; import { Breadcrumb, Container } from 'lib/components'; -import { TeamMembers, UnverifiedEmail, UserTeamCardHeader } from 'lib/features'; +import { TeamMembersView, UnverifiedEmail, UserTeamCardHeader } from 'lib/features'; import { MainHeader, MainLayout } from 'lib/layout'; import { useRouter, useParams, notFound } from 'next/navigation'; import { useCallback, useEffect } from 'react'; @@ -13,84 +13,88 @@ import { useTranslations } from 'next-intl'; import { useAtom, useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; +import { IssuesView } from '@app/constants'; const Team = () => { - const router = useRouter(); - const params = useParams(); - - const { - loadPublicTeamData, - loadPublicTeamMiscData, - publicTeam: publicTeamData - } = usePublicOrganizationTeams(); - const t = useTranslations(); - const [publicTeam, setPublic] = useAtom(publicState); - const fullWidth = useAtomValue(fullWidthState); - - useEffect(() => { - const userId = getActiveUserIdCookie(); - - if ( - userId && - publicTeamData && - publicTeamData.members.find((member) => member.employee.userId === userId) - ) { - router.replace('/'); - } - }, [publicTeamData, router]); - - const loadData = useCallback(() => { - if (params?.teamId && params?.profileLink) { - loadPublicTeamData( - params?.profileLink as string, - params?.teamId as string - ).then((res: any) => { - if (res?.data?.data?.status === 404) { - notFound(); - } - }); - setPublic(true); - } - }, [loadPublicTeamData, setPublic, params?.teamId, params?.profileLink]); - const loadMicsData = useCallback(() => { - if (params?.teamId && params?.profileLink) { - loadPublicTeamMiscData( - params?.profileLink as string, - params?.teamId as string - ); - } - }, [loadPublicTeamMiscData, params?.teamId, params?.profileLink]); - - useEffect(() => { - loadData(); - }, [loadData]); - useEffect(() => { - loadMicsData(); - }, [loadMicsData]); - - useRefreshIntervalV2(loadData, 10 * 1000, true); - useRefreshIntervalV2(loadMicsData, 30 * 1000, true); - - const breadcrumb = [...JSON.parse(t('pages.home.BREADCRUMB'))]; - return ( - - - - - - - {/* Header user card list */} - - - - {/* Divider */} -
- - - - -
- ); + const router = useRouter(); + const params = useParams(); + + const { + loadPublicTeamData, + loadPublicTeamMiscData, + teamsFetching, + publicTeam: publicTeamData + } = usePublicOrganizationTeams(); + const t = useTranslations(); + const [publicTeam, setPublic] = useAtom(publicState); + const fullWidth = useAtomValue(fullWidthState); + + useEffect(() => { + const userId = getActiveUserIdCookie(); + + if (userId && publicTeamData && publicTeamData.members.find((member) => member.employee.userId === userId)) { + router.replace('/'); + } + }, [publicTeamData, router]); + + const loadData = useCallback(() => { + if (params?.teamId && params?.profileLink) { + loadPublicTeamData(params?.profileLink as string, params?.teamId as string).then((res: any) => { + if (res?.data?.data?.status === 404) { + notFound(); + } + }); + setPublic(true); + } + }, [loadPublicTeamData, setPublic, params?.teamId, params?.profileLink]); + + const loadMicsData = useCallback(() => { + if (params?.teamId && params?.profileLink) { + loadPublicTeamMiscData(params?.profileLink as string, params?.teamId as string); + } + }, [loadPublicTeamMiscData, params?.teamId, params?.profileLink]); + + useEffect(() => { + loadData(); + }, [loadData]); + + useEffect(() => { + loadMicsData(); + }, [loadMicsData]); + + useRefreshIntervalV2(loadData, 10 * 1000, true); + useRefreshIntervalV2(loadMicsData, 30 * 1000, true); + + const breadcrumb = [...JSON.parse(t('pages.home.BREADCRUMB'))]; + + return ( + + + + + + + {/* Header user card list */} + + + + {/* Divider */} +
+ + + + + {/* */} + +
+ ); }; export default Team; diff --git a/apps/web/app/hooks/features/usePublicOrganizationTeams.ts b/apps/web/app/hooks/features/usePublicOrganizationTeams.ts index 064e96c64..bb465897d 100644 --- a/apps/web/app/hooks/features/usePublicOrganizationTeams.ts +++ b/apps/web/app/hooks/features/usePublicOrganizationTeams.ts @@ -1,7 +1,7 @@ import { ITeamTask } from '@app/interfaces'; import { - getPublicOrganizationTeamsAPI, - getPublicOrganizationTeamsMiscDataAPI + getPublicOrganizationTeamsAPI, + getPublicOrganizationTeamsMiscDataAPI } from '@app/services/client/api/public-organization-team'; import { publicactiveTeamState } from '@app/stores'; import isEqual from 'lodash/isEqual'; @@ -17,117 +17,107 @@ import { useTaskStatus } from './useTaskStatus'; import { useTeamTasks } from './useTeamTasks'; export function usePublicOrganizationTeams() { - const { loading, queryCall, loadingRef } = useQuery( - getPublicOrganizationTeamsAPI - ); - const { loading: loadingMiscData, queryCall: queryCallMiscData } = useQuery( - getPublicOrganizationTeamsMiscDataAPI - ); - const { activeTeam, teams, setTeams } = useOrganizationTeams(); - const { setAllTasks } = useTeamTasks(); - const { setTaskStatus } = useTaskStatus(); - const { setTaskSizes } = useTaskSizes(); - const { setTaskPriorities } = useTaskPriorities(); - const { setTaskLabels } = useTaskLabels(); - const [publicTeam, setPublicTeam] = useAtom(publicactiveTeamState); + const { loading, queryCall, loadingRef } = useQuery(getPublicOrganizationTeamsAPI); + const { loading: loadingMiscData, queryCall: queryCallMiscData } = useQuery(getPublicOrganizationTeamsMiscDataAPI); + const { activeTeam, teams, setTeams, teamsFetching } = useOrganizationTeams(); + const { setAllTasks } = useTeamTasks(); + const { setTaskStatus } = useTaskStatus(); + const { setTaskSizes } = useTaskSizes(); + const { setTaskPriorities } = useTaskPriorities(); + const { setTaskLabels } = useTaskLabels(); + const [publicTeam, setPublicTeam] = useAtom(publicactiveTeamState); - const loadPublicTeamData = useCallback( - (profileLink: string, teamId: string) => { - if (loadingRef.current) { - return new Promise((response) => { - response({}); - }); - } + const loadPublicTeamData = useCallback( + (profileLink: string, teamId: string) => { + if (loadingRef.current) { + return new Promise((response) => { + response({}); + }); + } - return queryCall(profileLink, teamId).then((res) => { - if (res.data.status === 404) { - setTeams([]); - return res; - } + return queryCall(profileLink, teamId).then((res) => { + if (res.data.status === 404) { + setTeams([]); + return res; + } - const updatedTeams = cloneDeep(teams); - if (updatedTeams.length) { - const newData = [ - { - ...updatedTeams[0], - ...res.data - } - ]; + const updatedTeams = cloneDeep(teams); + if (updatedTeams.length) { + const newData = [ + { + ...updatedTeams[0], + ...res.data + } + ]; - if (!isEqual(newData, updatedTeams)) { - setTeams([ - { - ...updatedTeams[0], - ...res.data - } - ]); - } - } else { - setTeams([res.data]); - } + if (!isEqual(newData, updatedTeams)) { + setTeams([ + { + ...updatedTeams[0], + ...res.data + } + ]); + } + } else { + setTeams([res.data]); + } - const newPublicTeamData = { - ...publicTeam, - ...res.data - }; - if (!isEqual(newPublicTeamData, publicTeam)) { - setPublicTeam(newPublicTeamData); - } + const newPublicTeamData = { + ...publicTeam, + ...res.data + }; + if (!isEqual(newPublicTeamData, publicTeam)) { + setPublicTeam(newPublicTeamData); + } - let responseTasks = (res.data.tasks as ITeamTask[]) || []; - if (Array.isArray(responseTasks) && responseTasks.length > 0) { - responseTasks = responseTasks.map((task) => { - const clone = cloneDeep(task); - if (task.tags && task.tags?.length) { - clone.label = task.tags[0].name; - } + let responseTasks = (res.data.tasks as ITeamTask[]) || []; + if (Array.isArray(responseTasks) && responseTasks.length > 0) { + responseTasks = responseTasks.map((task) => { + const clone = cloneDeep(task); + if (task.tags && task.tags?.length) { + clone.label = task.tags[0].name; + } - return clone; - }); - } - setAllTasks(responseTasks); + return clone; + }); + } + setAllTasks(responseTasks); - return res; - }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [queryCall, setTeams, setAllTasks, setPublicTeam, teams, publicTeam] - ); + return res; + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [queryCall, setTeams, setAllTasks, setPublicTeam, teams, publicTeam] + ); - const loadPublicTeamMiscData = useCallback( - (profileLink: string, teamId: string) => { - return queryCallMiscData(profileLink, teamId).then((res) => { - if (res.data?.status === 404) { - setTeams([]); - return res; - } + const loadPublicTeamMiscData = useCallback( + (profileLink: string, teamId: string) => { + return queryCallMiscData(profileLink, teamId).then((res) => { + if (res.data?.status === 404) { + setTeams([]); + return res; + } - if (res.data) { - setTaskStatus(res.data?.statuses || []); - setTaskSizes(res.data?.sizes || []); - setTaskPriorities(res.data?.priorities || []); - setTaskLabels(res.data?.labels || []); - } + if (res.data) { + setTaskStatus(res.data?.statuses || []); + setTaskSizes(res.data?.sizes || []); + setTaskPriorities(res.data?.priorities || []); + setTaskLabels(res.data?.labels || []); + } - return res; - }); - }, - [ - queryCallMiscData, - setTaskLabels, - setTaskPriorities, - setTaskSizes, - setTaskStatus, - setTeams - ] - ); + return res; + }); + }, + [queryCallMiscData, setTaskLabels, setTaskPriorities, setTaskSizes, setTaskStatus, setTeams] + ); - return { - loadPublicTeamData, - loadPublicTeamMiscData, - loading, - loadingMiscData, - activeTeam, - publicTeam - }; + return { + teamsFetching, + loadPublicTeamData, + loadPublicTeamMiscData, + loading, + loadingMiscData, + activeTeam, + publicTeam + }; } diff --git a/apps/web/app/hooks/features/useTaskStatistics.ts b/apps/web/app/hooks/features/useTaskStatistics.ts index 3f13b82ca..a02e4f016 100644 --- a/apps/web/app/hooks/features/useTaskStatistics.ts +++ b/apps/web/app/hooks/features/useTaskStatistics.ts @@ -2,17 +2,17 @@ import { ITeamTask, Nullable } from '@app/interfaces'; import { - activeTaskTimesheetStatisticsAPI, - allTaskTimesheetStatisticsAPI, - tasksTimesheetStatisticsAPI + activeTaskTimesheetStatisticsAPI, + allTaskTimesheetStatisticsAPI, + tasksTimesheetStatisticsAPI } from '@app/services/client/api'; import { - activeTaskStatisticsState, - activeTeamTaskState, - allTaskStatisticsState, - tasksFetchingState, - tasksStatisticsState, - timerStatusState + activeTaskStatisticsState, + activeTeamTaskState, + allTaskStatisticsState, + tasksFetchingState, + tasksStatisticsState, + timerStatusState } from '@app/stores'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; @@ -25,220 +25,181 @@ import { useOrganizationTeams } from './useOrganizationTeams'; import { useAuthenticateUser } from './useAuthenticateUser'; export function useTaskStatistics(addSeconds = 0) { - const { user } = useAuthenticateUser(); - const [statActiveTask, setStatActiveTask] = useAtom( - activeTaskStatisticsState - ); - const [statTasks, setStatTasks] = useAtom(tasksStatisticsState); - const setTasksFetching = useSetAtom(tasksFetchingState); - const [allTaskStatistics, setAllTaskStatistics] = useAtom( - allTaskStatisticsState - ); - - const { - firstLoad, - firstLoadData: firstLoadtasksStatisticsData - } = useFirstLoad(); - - const { activeTeam } = useOrganizationTeams(); - - // Refs - const initialLoad = useRef(false); - const statTasksRef = useSyncRef(statTasks); - - // Dep status - const timerStatus = useAtomValue(timerStatusState); - const activeTeamTask = useAtomValue(activeTeamTaskState); - - /** - * Get employee all tasks statistics (API Call) - */ - const getTasksStatsData = useCallback( - (employeeId?: string) => { - if (!user?.employee.tenantId) { - return; - } - tasksTimesheetStatisticsAPI( - user?.employee.tenantId, - '', - user?.employee.organizationId, - employeeId - ).then(({ data }) => { - setStatTasks({ - all: data.global || [], - today: data.today || [] - }); - }); - }, - [setStatTasks, user?.employee.organizationId, user?.employee.tenantId] - ); - const getAllTasksStatsData = useCallback(() => { - allTaskTimesheetStatisticsAPI().then(({ data }) => { - setAllTaskStatistics(data); - }); - }, [setAllTaskStatistics]); - - /** - * Get task timesheet statistics - */ - const getTaskStat = useCallback( - (task: Nullable) => { - const stats = statTasksRef.current; - return { - taskTotalStat: stats.all.find((t) => t.id === task?.id), - taskDailyStat: stats.today.find((t) => t.id === task?.id) - }; - }, - [statTasksRef] - ); - - /** - * Get statistics of the active tasks fresh (API Call) - */ - const getActiveTaskStatData = useCallback(() => { - if (!user?.employee.tenantId || !user?.employee.organizationId) { - return new Promise((resolve) => { - resolve(true); - }); - } - - setTasksFetching(true); - - const promise = activeTaskTimesheetStatisticsAPI( - user?.employee.tenantId, - '', - user?.employee.organizationId, - '' - ); - promise.then(({ data }) => { - setStatActiveTask({ - total: data.global ? data.global[0] || null : null, - today: data.today ? data.today[0] || null : null - }); - }); - promise.finally(() => { - setTasksFetching(false); - }); - return promise; - }, [ - setStatActiveTask, - setTasksFetching, - user?.employee.organizationId, - user?.employee.tenantId - ]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const debounceLoadActiveTaskStat = useCallback( - debounce(getActiveTaskStatData, 100), - [] - ); - - /** - * Get statistics of the active tasks at the component load - */ - useEffect(() => { - if (firstLoad) { - getActiveTaskStatData().then(() => { - initialLoad.current = true; - }); - } - }, [ - firstLoad, - getActiveTaskStatData, - user?.employee.organizationId, - user?.employee.tenantId - ]); - - /** - * Get fresh statistic of the active task - */ - useEffect(() => { - if (firstLoad && initialLoad.current) { - debounceLoadActiveTaskStat(); - } - }, [firstLoad, timerStatus, activeTeamTask?.id, debounceLoadActiveTaskStat]); - - /** - * set null to active team stats when active team or active task are changed - */ - useEffect(() => { - if (firstLoad && initialLoad.current) { - setStatActiveTask({ - today: null, - total: null - }); - } - }, [firstLoad, activeTeamTask?.id, setStatActiveTask]); - - /** - * Get task estimation in - * - * @param timeSheet - * @param _task - * @param addSeconds - * @returns - */ - const getEstimation = useCallback( - ( - timeSheet: Nullable, - _task: Nullable, - addSeconds: number, - estimate = 0 - ) => - Math.min( - Math.floor( - (((_task?.totalWorkedTime || timeSheet?.duration || 0) + addSeconds) * - 100) / - (estimate || _task?.estimate || 0) - ), - 100 - ), - [] - ); - - const activeTaskEstimation = useMemo(() => { - let totalWorkedTasksTimer = 0; - activeTeam?.members?.forEach((member) => { - const totalWorkedTasks = - member?.totalWorkedTasks?.find( - (item) => item.id === activeTeamTask?.id - ) || null; - if (totalWorkedTasks) { - totalWorkedTasksTimer += totalWorkedTasks.duration; - } - }); - - return getEstimation( - null, - activeTeamTask, - totalWorkedTasksTimer, - activeTeamTask?.estimate || 0 - ); - }, [activeTeam, activeTeamTask, getEstimation]); - - const activeTaskDailyEstimation = - activeTeamTask && activeTeamTask.estimate - ? getEstimation(statActiveTask.today, activeTeamTask, addSeconds) - : 0; - - return { - firstLoadtasksStatisticsData, - getAllTasksStatsData, - getTasksStatsData, - getTaskStat, - activeTaskTotalStat: statActiveTask.total, - activeTaskDailyStat: statActiveTask.today, - activeTaskEstimation, - activeTaskDailyEstimation, - activeTeamTask, - addSeconds, - getEstimation, - allTaskStatistics - }; + const { user } = useAuthenticateUser(); + const [statActiveTask, setStatActiveTask] = useAtom(activeTaskStatisticsState); + const [statTasks, setStatTasks] = useAtom(tasksStatisticsState); + const setTasksFetching = useSetAtom(tasksFetchingState); + const [allTaskStatistics, setAllTaskStatistics] = useAtom(allTaskStatisticsState); + + const { firstLoad, firstLoadData: firstLoadtasksStatisticsData } = useFirstLoad(); + + const { activeTeam } = useOrganizationTeams(); + + // Refs + const initialLoad = useRef(false); + const statTasksRef = useSyncRef(statTasks); + + // Dep status + const timerStatus = useAtomValue(timerStatusState); + const activeTeamTask = useAtomValue(activeTeamTaskState); + + /** + * Get employee all tasks statistics (API Call) + */ + const getTasksStatsData = useCallback( + (employeeId?: string) => { + if (!user?.employee.tenantId) { + return; + } + tasksTimesheetStatisticsAPI(user?.employee.tenantId, '', user?.employee.organizationId, employeeId).then( + ({ data }) => { + setStatTasks({ + all: data.global || [], + today: data.today || [] + }); + } + ); + }, + [setStatTasks, user?.employee.organizationId, user?.employee.tenantId] + ); + const getAllTasksStatsData = useCallback(() => { + allTaskTimesheetStatisticsAPI().then(({ data }) => { + setAllTaskStatistics(data); + }); + }, [setAllTaskStatistics]); + + /** + * Get task timesheet statistics + */ + const getTaskStat = useCallback( + (task: Nullable) => { + const stats = statTasksRef.current; + return { + taskTotalStat: stats.all.find((t) => t.id === task?.id), + taskDailyStat: stats.today.find((t) => t.id === task?.id) + }; + }, + [statTasksRef] + ); + + /** + * Get statistics of the active tasks fresh (API Call) + */ + const getActiveTaskStatData = useCallback(() => { + if (!user?.employee.tenantId || !user?.employee.organizationId) { + return new Promise((resolve) => { + resolve(true); + }); + } + + setTasksFetching(true); + + const promise = activeTaskTimesheetStatisticsAPI( + user?.employee.tenantId, + '', + user?.employee.organizationId, + '' + ); + promise.then(({ data }) => { + setStatActiveTask({ + total: data.global ? data.global[0] || null : null, + today: data.today ? data.today[0] || null : null + }); + }); + promise.finally(() => { + setTasksFetching(false); + }); + return promise; + }, [setStatActiveTask, setTasksFetching, user?.employee.organizationId, user?.employee.tenantId]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debounceLoadActiveTaskStat = useCallback(debounce(getActiveTaskStatData, 100), []); + + /** + * Get statistics of the active tasks at the component load + */ + useEffect(() => { + if (firstLoad) { + getActiveTaskStatData().then(() => { + initialLoad.current = true; + }); + } + }, [firstLoad, getActiveTaskStatData, user?.employee.organizationId, user?.employee.tenantId]); + + /** + * Get fresh statistic of the active task + */ + useEffect(() => { + if (firstLoad && initialLoad.current) { + debounceLoadActiveTaskStat(); + } + }, [firstLoad, timerStatus, activeTeamTask?.id, debounceLoadActiveTaskStat]); + + /** + * set null to active team stats when active team or active task are changed + */ + useEffect(() => { + if (firstLoad && initialLoad.current) { + setStatActiveTask({ + today: null, + total: null + }); + } + }, [firstLoad, activeTeamTask?.id, setStatActiveTask]); + + /** + * Get task estimation in + * + * @param timeSheet + * @param _task + * @param addSeconds + * @returns + */ + const getEstimation = useCallback( + (timeSheet: Nullable, _task: Nullable, addSeconds: number, estimate = 0) => + Math.min( + Math.floor( + (((_task?.totalWorkedTime || timeSheet?.duration || 0) + addSeconds) * 100) / + (estimate || _task?.estimate || 0) + ), + 100 + ), + [] + ); + + const activeTaskEstimation = useMemo(() => { + let totalWorkedTasksTimer = 0; + activeTeam?.members?.forEach((member) => { + const totalWorkedTasks = member?.totalWorkedTasks?.find((item) => item.id === activeTeamTask?.id) || null; + if (totalWorkedTasks) { + totalWorkedTasksTimer += totalWorkedTasks.duration; + } + }); + + return getEstimation(null, activeTeamTask, totalWorkedTasksTimer, activeTeamTask?.estimate || 0); + }, [activeTeam, activeTeamTask, getEstimation]); + + const activeTaskDailyEstimation = + activeTeamTask && activeTeamTask.estimate ? getEstimation(statActiveTask.today, activeTeamTask, addSeconds) : 0; + + return { + firstLoadtasksStatisticsData, + getAllTasksStatsData, + getTasksStatsData, + getTaskStat, + activeTaskTotalStat: statActiveTask.total, + activeTaskDailyStat: statActiveTask.today, + activeTaskEstimation, + activeTaskDailyEstimation, + activeTeamTask, + addSeconds, + getEstimation, + allTaskStatistics + }; } export function useAllTaskStatistics() { - const { getAllTasksStatsData } = useTaskStatistics(); + const { getAllTasksStatsData } = useTaskStatistics(); - useRefreshIntervalV2(getAllTasksStatsData, 5000); + useRefreshIntervalV2(getAllTasksStatsData, 5000); } diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts index 38be5aa32..eff23ac40 100644 --- a/apps/web/app/services/client/axios.ts +++ b/apps/web/app/services/client/axios.ts @@ -1,5 +1,5 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { API_BASE_URL, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL } from '@app/constants'; +import { API_BASE_URL, APPLICATION_LANGUAGES_CODE, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL } from '@app/constants'; import { getAccessTokenCookie, getActiveTeamIdCookie, @@ -70,6 +70,14 @@ apiDirect.interceptors.response.use( const statusCode = error.response?.status; if (statusCode === 401) { + const paths = location.pathname.split('/').filter(Boolean); + if ( + !paths.includes('join') && + (paths[0] === 'team' || (APPLICATION_LANGUAGES_CODE.includes(paths[0]) && paths[1] === 'team')) + ) { + return error.response; + } + window.location.assign(DEFAULT_APP_PATH); } diff --git a/apps/web/lib/features/team-members-card-view.tsx b/apps/web/lib/features/team-members-card-view.tsx index 9f246c688..9a0e18090 100644 --- a/apps/web/lib/features/team-members-card-view.tsx +++ b/apps/web/lib/features/team-members-card-view.tsx @@ -52,7 +52,6 @@ const TeamMembersCardView: React.FC = ({ return ( <>
    - {/* Current authenticated user members */} member.employee !== null - ); - const orderedMembers = [...members].sort((a, b) => - sortByWorkStatus(a, b) ? -1 : 1 - ); +export function TeamMembers({ publicTeam = false, kanbanView: view = IssuesView.CARDS }: TeamMembersProps) { + const { user } = useAuthenticateUser(); + const activeFilter = useAtomValue(taskBlockFilterState); + const fullWidth = useAtomValue(fullWidthState); + const { activeTeam, teamsFetching } = useOrganizationTeams(); - const blockViewMembers = useMemo(() => { - return activeFilter == 'all' - ? orderedMembers - : activeFilter == 'idle' - ? orderedMembers.filter( - (m: OT_Member) => - m.timerStatus == undefined || m.timerStatus == 'idle' - ) - : orderedMembers.filter((m) => m.timerStatus === activeFilter); - }, [activeFilter, orderedMembers]); + const [members, orderedMembers] = useMemo(() => { + const members = (activeTeam?.members || []).filter((member) => member.employee !== null); + const orderedMembers = [...members].sort((a, b) => (sortByWorkStatus(a, b) ? -1 : 1)); - const currentUser = members.find((m) => m.employee.userId === user?.id); + return [members, orderedMembers]; + }, [activeTeam]); - const $members = useMemo(() => { - return members - .filter((member) => member.id !== currentUser?.id) - .sort((a, b) => { - if (a.order && b.order) return a.order > b.order ? -1 : 1; - else return -1; - }); - }, [members, currentUser]); + const blockViewMembers = useMemo(() => { + return activeFilter == 'all' + ? orderedMembers + : activeFilter == 'idle' + ? orderedMembers.filter((m: OT_Member) => m.timerStatus == undefined || m.timerStatus == 'idle') + : orderedMembers.filter((m) => m.timerStatus === activeFilter); + }, [activeFilter, orderedMembers]); - const $teamsFetching = teamsFetching && members.length === 0; + const currentUser = members.find((m) => m.employee.userId === user?.id); - let teamMembersView; + const $members = useMemo(() => { + return members + .filter((member) => member.id !== currentUser?.id) + .sort((a, b) => { + if (a.order && b.order) return a.order > b.order ? -1 : 1; + else return -1; + }); + }, [members, currentUser]); - switch (true) { - case members.length === 0: - teamMembersView = ( - -
    - - -
    -
    - - - -
    -
    - ); - break; - case view === IssuesView.CARDS: - teamMembersView = ( - <> - {/* */} - - - - - ); - break; - case view === IssuesView.TABLE: - teamMembersView = ( - - - - - - ); - break; + const $teamsFetching = teamsFetching && members.length === 0; - case view == IssuesView.BLOCKS: - teamMembersView = ( - - - - ); - break; - default: - teamMembersView = ( - - - - ); - } - return teamMembersView; + return ( + + ); +} + +type TeamMembersViewProps = { + fullWidth?: boolean; + members: OT_Member[]; + currentUser?: OT_Member; + teamsFetching: boolean; + view: IssuesView; + blockViewMembers: OT_Member[]; + publicTeam: boolean; + isMemberActive?: boolean; +}; + +export function TeamMembersView({ + fullWidth, + members, + currentUser, + teamsFetching, + view, + blockViewMembers, + publicTeam, + isMemberActive +}: TeamMembersViewProps) { + let teamMembersView; + + switch (true) { + case members.length === 0: + teamMembersView = ( + +
    + + +
    +
    + + + +
    +
    + ); + break; + case view === IssuesView.CARDS: + teamMembersView = ( + <> + {/* */} + + + + + ); + break; + case view === IssuesView.TABLE: + teamMembersView = ( + + + + + + ); + break; + + case view == IssuesView.BLOCKS: + teamMembersView = ( + + + + ); + break; + default: + teamMembersView = ( + + + + ); + } + + return teamMembersView; } const sortByWorkStatus = (user_a: OT_Member, user_b: OT_Member) => { - return user_a.timerStatus == 'running' || - (user_a.timerStatus == 'online' && user_b.timerStatus != 'running') || - (user_a.timerStatus == 'pause' && - user_b.timerStatus !== 'running' && - user_b.timerStatus !== 'online') || - (user_a.timerStatus == 'idle' && user_b.timerStatus == 'suspended') || - (user_a.timerStatus === undefined && user_b.timerStatus == 'suspended') - ? true - : false; + return user_a.timerStatus == 'running' || + (user_a.timerStatus == 'online' && user_b.timerStatus != 'running') || + (user_a.timerStatus == 'pause' && user_b.timerStatus !== 'running' && user_b.timerStatus !== 'online') || + (user_a.timerStatus == 'idle' && user_b.timerStatus == 'suspended') || + (user_a.timerStatus === undefined && user_b.timerStatus == 'suspended') + ? true + : false; }; diff --git a/apps/web/lib/features/team/user-team-card/index.tsx b/apps/web/lib/features/team/user-team-card/index.tsx index b6642b8f8..2f75e630a 100644 --- a/apps/web/lib/features/team/user-team-card/index.tsx +++ b/apps/web/lib/features/team/user-team-card/index.tsx @@ -2,33 +2,19 @@ import { secondsToTime } from '@app/helpers'; import { - useCollaborative, - useTMCardTaskEdit, - useTaskStatistics, - useOrganizationTeams, - useAuthenticateUser, - useTeamMemberCard, - useUserProfilePage + useCollaborative, + useTMCardTaskEdit, + useTaskStatistics, + useOrganizationTeams, + useAuthenticateUser, + useTeamMemberCard, + useUserProfilePage } from '@app/hooks'; import { IClassName, IOrganizationTeamList, OT_Member } from '@app/interfaces'; -import { - timerSecondsState, - userDetailAccordion as userAccordion -} from '@app/stores'; +import { timerSecondsState, userDetailAccordion as userAccordion } from '@app/stores'; import { clsxm } from '@app/utils'; -import { - Card, - Container, - InputField, - Text, - VerticalSeparator -} from 'lib/components'; -import { - TaskTimes, - TodayWorkedTime, - UserProfileTask, - useTaskFilter -} from 'lib/features'; +import { Card, Container, InputField, Text, VerticalSeparator } from 'lib/components'; +import { TaskTimes, TodayWorkedTime, UserProfileTask, useTaskFilter } from 'lib/features'; import { useTranslations } from 'next-intl'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { TaskEstimateInfo } from './task-estimate'; @@ -49,352 +35,305 @@ import { Loader } from 'lucide-react'; import { fullWidthState } from '@app/stores/fullWidth'; type IUserTeamCard = { - active?: boolean; - member?: IOrganizationTeamList['members'][number]; - publicTeam?: boolean; - members?: IOrganizationTeamList['members']; - draggable: boolean; - onDragStart: () => any; - onDragEnter: () => any; - onDragEnd: any; - onDragOver: (e: React.DragEvent) => any; - currentExit: boolean; + active?: boolean; + member?: IOrganizationTeamList['members'][number]; + publicTeam?: boolean; + members?: IOrganizationTeamList['members']; + draggable: boolean; + onDragStart: () => any; + onDragEnter: () => any; + onDragEnd: any; + onDragOver: (e: React.DragEvent) => any; + currentExit: boolean; } & IClassName; export function UserTeamCard({ - className, - active, - member, - publicTeam = false, - draggable = false, - onDragStart = () => null, - onDragEnd = () => null, - onDragEnter = () => null, - onDragOver = () => null + className, + active, + member, + publicTeam = false, + draggable = false, + onDragStart = () => null, + onDragEnd = () => null, + onDragEnter = () => null, + onDragOver = () => null }: IUserTeamCard) { - const t = useTranslations(); - const profile = useUserProfilePage(); - const [userDetailAccordion, setUserDetailAccordion] = useAtom(userAccordion); - const hook = useTaskFilter(profile); - const memberInfo = useTeamMemberCard(member); - const taskEdition = useTMCardTaskEdit(memberInfo.memberTask); - const { collaborativeSelect, user_selected, onUserSelect } = useCollaborative( - memberInfo.memberUser - ); - const fullWidth = useAtomValue(fullWidthState); + const t = useTranslations(); + const profile = useUserProfilePage(); + const [userDetailAccordion, setUserDetailAccordion] = useAtom(userAccordion); + const hook = useTaskFilter(profile); + const memberInfo = useTeamMemberCard(member); + const taskEdition = useTMCardTaskEdit(memberInfo.memberTask); + const { collaborativeSelect, user_selected, onUserSelect } = useCollaborative(memberInfo.memberUser); + const fullWidth = useAtomValue(fullWidthState); - const seconds = useAtomValue(timerSecondsState); - const setActivityFilter = useSetAtom(activityTypeState); - const { activeTaskTotalStat, addSeconds } = useTaskStatistics(seconds); - const [showActivity, setShowActivity] = React.useState(false); - const { activeTeamManagers } = useOrganizationTeams(); - const { user } = useAuthenticateUser(); + const seconds = useAtomValue(timerSecondsState); + const setActivityFilter = useSetAtom(activityTypeState); + const { activeTaskTotalStat, addSeconds } = useTaskStatistics(seconds); + const [showActivity, setShowActivity] = React.useState(false); + const { activeTeamManagers } = useOrganizationTeams(); + const { user } = useAuthenticateUser(); - const isManagerConnectedUser = activeTeamManagers.findIndex( - (member) => member.employee?.user?.id == user?.id - ); + const isManagerConnectedUser = activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id); - const showActivityFilter = ( - type: 'DATE' | 'TICKET', - member: OT_Member | null - ) => { - setShowActivity((prev) => !prev); - setUserDetailAccordion(''); - setActivityFilter((prev) => ({ - ...prev, - type, - member - })); - }; + const showActivityFilter = (type: 'DATE' | 'TICKET', member: OT_Member | null) => { + setShowActivity((prev) => !prev); + setUserDetailAccordion(''); + setActivityFilter((prev) => ({ + ...prev, + type, + member + })); + }; - let totalWork = <>; - if (memberInfo.isAuthUser) { - const { h, m } = secondsToTime( - ((member?.totalTodayTasks && - member?.totalTodayTasks.reduce( - (previousValue, currentValue) => - previousValue + currentValue.duration, - 0 - )) || - activeTaskTotalStat?.duration || - 0) + addSeconds - ); + let totalWork = <>; + if (memberInfo.isAuthUser) { + const { h, m } = secondsToTime( + ((member?.totalTodayTasks && + member?.totalTodayTasks.reduce( + (previousValue, currentValue) => previousValue + currentValue.duration, + 0 + )) || + activeTaskTotalStat?.duration || + 0) + addSeconds + ); - totalWork = ( -
    - {t('common.TOTAL_TIME')}: - - {h}h : {m}m - -
    - ); - } + totalWork = ( +
    + {t('common.TOTAL_TIME')}: + + {h}h : {m}m + +
    + ); + } - const menu = ( - <> - {(!collaborativeSelect || active) && ( - - )} + const menu = ( + <> + {(!collaborativeSelect || active) && } - {collaborativeSelect && !active && ( - - )} - - ); - const [activityFilter, setActivity] = useState('Tasks'); + {collaborativeSelect && !active && ( + + )} + + ); + const [activityFilter, setActivity] = useState('Tasks'); - const activityScreens = { - Tasks: , - Screenshots: , - Apps: , - 'Visited Sites': - }; - const changeActivityFilter = useCallback( - (filter: FilterTab) => { - setActivity(filter); - }, - [setActivity] - ); - const canSeeActivity = - profile.userProfile?.id === user?.id || isManagerConnectedUser != -1; + const activityScreens = { + Tasks: , + Screenshots: , + Apps: , + 'Visited Sites': + }; + const changeActivityFilter = useCallback( + (filter: FilterTab) => { + setActivity(filter); + }, + [setActivity] + ); + const canSeeActivity = profile.userProfile?.id === user?.id || isManagerConnectedUser != -1; - return ( -
    - + {/* {currentExit && ( )} */} -
    - ); +
+ ); } diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index ec3b297ff..a63df8949 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,5 +1,6 @@ import { APPLICATION_DEFAULT_LANGUAGE, + APPLICATION_LANGUAGES_CODE, DEFAULT_APP_PATH, DEFAULT_MAIN_PATH, PROTECTED_APP_URL_PATHS, @@ -34,7 +35,7 @@ export { auth as authMiddleware } from './auth'; export async function middleware(request: NextRequest) { const nextIntlMiddleware = createMiddleware({ defaultLocale: APPLICATION_DEFAULT_LANGUAGE, - locales: ['en', 'de', 'ar', 'bg', 'zh', 'nl', 'de', 'he', 'it', 'pl', 'pt', 'ru', 'es', 'fr'], + locales: APPLICATION_LANGUAGES_CODE, // pathnames, localePrefix: 'as-needed' }); @@ -43,6 +44,15 @@ export async function middleware(request: NextRequest) { // let response = NextResponse.next(); let response = nextIntlMiddleware(request); + const paths = new URL(request.url).pathname.split('/').filter(Boolean); + + if ( + !paths.includes('join') && + (paths[0] === 'team' || (APPLICATION_LANGUAGES_CODE.includes(paths[0]) && paths[1] === 'team')) + ) { + return response; + } + let access_token = null; const totalChunksCookie = request.cookies.get(`${TOKEN_COOKIE_NAME}_totalChunks`)?.value.trim();