diff --git a/.cspell.json b/.cspell.json index b662c7478..0da70ed82 100644 --- a/.cspell.json +++ b/.cspell.json @@ -32,6 +32,7 @@ "borde", "Bowser", "Btns", + "Konviser", "Bugsnag", "buildjet", "cacheable", diff --git a/apps/web/app/[locale]/all-teams/component.tsx b/apps/web/app/[locale]/all-teams/component.tsx index 3e572b9cf..2f0f0a54e 100644 --- a/apps/web/app/[locale]/all-teams/component.tsx +++ b/apps/web/app/[locale]/all-teams/component.tsx @@ -15,12 +15,14 @@ import { HeaderTabs } from '@components/pages/all-teams/header-tabs'; import { allTeamsHeaderTabs } from '@app/stores/header-tabs'; import AllTeamsMembers from 'lib/features/all-teams-members'; import { MemberFilter } from 'lib/features/all-teams/all-team-members-filter'; +import { useOrganizationTeams } from '@app/hooks'; function AllTeamsPage() { const t = useTranslations(); const fullWidth = useRecoilValue(fullWidthState); const view = useRecoilValue(allTeamsHeaderTabs); const { filteredTeams, userManagedTeams } = useOrganizationAndTeamManagers(); + const { isTrackingEnabled } = useOrganizationTeams(); const breadcrumb = [ { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, @@ -33,7 +35,7 @@ function AllTeamsPage() { if (userManagedTeams.length < 2) return ; return ( - + {/* Breadcrumb */}
diff --git a/apps/web/app/[locale]/auth/passcode/component.tsx b/apps/web/app/[locale]/auth/passcode/component.tsx index 7ebb81c22..2414393d4 100644 --- a/apps/web/app/[locale]/auth/passcode/component.tsx +++ b/apps/web/app/[locale]/auth/passcode/component.tsx @@ -27,6 +27,8 @@ import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; import SocialLogins from '../social-logins-buttons'; import { useSession } from 'next-auth/react'; import { LAST_WORSPACE_AND_TEAM, USER_SAW_OUTSTANDING_NOTIFICATION } from '@app/constants'; +import { MdOutlineKeyboardArrowDown } from 'react-icons/md'; +import { cn } from 'lib/utils'; function AuthPasscode() { const form = useAuthenticationPasscode(); @@ -78,7 +80,8 @@ function AuthPasscode() { {form.authScreen.screen === 'workspace' && ( )} -
+ + {/* Social logins */} @@ -412,6 +415,12 @@ type IWorkSpace = { export function WorkSpaceComponent(props: IWorkSpace) { const t = useTranslations(); + const [expandedWorkspace, setExpandedWorkspace] = useState(props.selectedWorkspace); + + useEffect(() => { + setExpandedWorkspace(props.selectedWorkspace); + }, [props.selectedWorkspace]); + return (
(
- {worksace.user.tenant.name} +
setExpandedWorkspace(index)} + className="flex cursor-pointer items-center justify-center gap-1" + > + {worksace.user.tenant.name} + + + +
{ @@ -456,7 +480,9 @@ export function WorkSpaceComponent(props: IWorkSpace) { )}
- + {/*
*/}
{worksace.current_teams?.map((team) => ( diff --git a/apps/web/app/[locale]/calendar/component.tsx b/apps/web/app/[locale]/calendar/component.tsx index 0b8d6501c..3a44ef27e 100644 --- a/apps/web/app/[locale]/calendar/component.tsx +++ b/apps/web/app/[locale]/calendar/component.tsx @@ -1,31 +1,150 @@ import { clsxm } from "@app/utils"; +import { DatePicker } from "@components/ui/DatePicker"; import { QueueListIcon } from "@heroicons/react/20/solid"; +import { addDays, format } from "date-fns"; import { Button } from "lib/components"; +import { TimeSheetFilter, timesheetCalendar } from "lib/features/integrations/calendar"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@components/ui/select" +import { cn } from "lib/utils"; +import { CalendarDays } from "lucide-react"; +import React from "react"; +import { DateRange } from "react-day-picker"; import { LuCalendarDays } from "react-icons/lu"; -export function HeadCalendar({ openModal }: { openModal?: () => void }) { +export function HeadCalendar({ + openModal, + timesheet, + setCalendarTimeSheet +}: { + openModal?: () => void, + timesheet: timesheetCalendar, + setCalendarTimeSheet: React.Dispatch> +}) { + return (
-

CALENDAR

+

{timesheet === 'Calendar' ? "Calendar" : "Timesheet"}

); } + + +export function HeadTimeSheet({ timesheet }: { timesheet?: timesheetCalendar }) { + + const [date, setDate] = React.useState({ + from: new Date(2022, 0, 20), + to: addDays(new Date(2022, 0, 20), 20) + }); + console.log(timesheet); + return ( +
+
+ {timesheet === 'TimeSheet' && ( +
+
+ +
+ + + + + } + mode={'range'} + numberOfMonths={2} + initialFocus + defaultMonth={date?.from} + selected={date} + onSelect={(value) => { + value && setDate(value); + }} + /> +
+ +
+
+ )} +
+
+ ) +} + + + +export function CustomSelect() { + const [selectedValue, setSelectedValue] = React.useState(undefined); + + const handleSelectChange = (value: string) => { + setSelectedValue(value); + }; + + return ( + + ); +} diff --git a/apps/web/app/[locale]/calendar/page.tsx b/apps/web/app/[locale]/calendar/page.tsx index f149a2b18..1d1472623 100644 --- a/apps/web/app/[locale]/calendar/page.tsx +++ b/apps/web/app/[locale]/calendar/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useModal, useOrganizationTeams } from '@app/hooks'; +import { useLocalStorageState, useModal, useOrganizationTeams } from '@app/hooks'; import { fullWidthState } from '@app/stores/fullWidth'; import { clsxm } from '@app/utils'; import HeaderTabs from '@components/pages/main/header-tabs'; @@ -13,13 +13,15 @@ import { useTranslations } from 'next-intl'; import { useParams } from 'next/navigation'; import React, { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; -import { HeadCalendar } from './component'; +import { HeadCalendar, HeadTimeSheet } from './component'; import { AddManualTimeModal } from 'lib/features/manual-time/add-manual-time-modal'; +import { SetupTimeSheet, timesheetCalendar } from 'lib/features/integrations/calendar'; const CalendarPage = () => { const t = useTranslations(); const fullWidth = useRecoilValue(fullWidthState); const { activeTeam, isTrackingEnabled } = useOrganizationTeams(); + const [calendarTimeSheet, setCalendarTimeSheet] = useLocalStorageState('calendar-timesheet', 'Calendar') const { isOpen: isManualTimeModalOpen, openModal: openManualTimeModal, @@ -37,13 +39,23 @@ const CalendarPage = () => { [activeTeam?.name, currentLocale, t] ); + const renderComponent = () => { + switch (calendarTimeSheet) { + case 'Calendar': + return ; + case 'TimeSheet': + return ; + default: + return null; + } + }; return ( <> { params='AddManuelTime' /> -
+
@@ -65,11 +77,22 @@ const CalendarPage = () => {
- +
+ +
+ + +
-
- + +
+ + {renderComponent()} +
diff --git a/apps/web/app/[locale]/kanban/page.tsx b/apps/web/app/[locale]/kanban/page.tsx index 490d1b61f..c7dff39c2 100644 --- a/apps/web/app/[locale]/kanban/page.tsx +++ b/apps/web/app/[locale]/kanban/page.tsx @@ -167,8 +167,8 @@ const Kanban = () => { key={tab.name} onClick={() => setActiveTab(tab.value)} className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${activeTab === tab.value - ? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white' - : 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]' + ? 'border-b-[#3826A6] text-[#3826A6] dark:text-white dark:border-b-white' + : 'border-b-white dark:border-b-[#191A20] dark:text-white text-[#282048]' }`} style={{ borderBottomWidth: '3px', diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index 2c6c76b80..f2afa3ea3 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -64,7 +64,11 @@ function MainPage() { <>
{/*
*/} - +
diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 126c7be80..2152a1cf4 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -23,15 +23,14 @@ import { AppsTab } from 'lib/features/activity/apps'; import { VisitedSitesTab } from 'lib/features/activity/visited-sites'; import { activityTypeState } from '@app/stores/activity-type'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; +import { TableActionPopover } from 'lib/settings/table-action-popover'; // import { ActivityCalendar } from 'lib/features/activity/calendar'; export type FilterTab = 'Tasks' | 'Screenshots' | 'Apps' | 'Visited Sites'; const Profile = React.memo(function ProfilePage({ params }: { params: { memberId: string } }) { const profile = useUserProfilePage(); - const [headerSize, setHeaderSize] = useState(10); - const { user } = useAuthenticateUser(); const { isTrackingEnabled, activeTeam, activeTeamManagers } = useOrganizationTeams(); const members = activeTeam?.members; @@ -95,7 +94,10 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId <> {Array.isArray(members) && members.length && !profile.member ? ( -
+
{t('common.MEMBER')} {t('common.NOT_FOUND')}! @@ -115,7 +117,7 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId
) : ( - + member?.employee.user, [member?.employee.user]); + const userName = `${user?.firstName || ''} ${user?.lastName || ''}`; const imgUrl = user?.image?.thumbUrl || user?.image?.fullUrl || user?.imageUrl; const imageUrl = useMemo(() => imgUrl, [imgUrl]); const size = 100; const { timerStatus } = useTimer(); + // const isManager = activeTeamManagers.find((member) => member.employee.user?.id === member?.employee.user?.id); const timerStatusValue: ITimerStatusEnum = useMemo(() => { return getTimerStatusValue(timerStatus, member, false); }, [timerStatus, member]); - return (
+
+ {imageUrl && isValidUrl(imageUrl) ? ( + + ) : ( <> + {imgTitle(userName).charAt(0)} + )}
- -
- - {user?.firstName} {user?.lastName} - +
+
+ + {user?.firstName} {user?.lastName} + +
+ +
+
{user?.email}
diff --git a/apps/web/app/[locale]/settings/layout.tsx b/apps/web/app/[locale]/settings/layout.tsx index 92570dc47..677d72944 100644 --- a/apps/web/app/[locale]/settings/layout.tsx +++ b/apps/web/app/[locale]/settings/layout.tsx @@ -13,8 +13,10 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import { clsxm } from '@app/utils'; import { withAuthentication } from 'lib/app/authenticator'; import { usePathname } from 'next/navigation'; +import { useOrganizationTeams } from '@app/hooks'; const SettingsLayout = ({ children }: { children: JSX.Element }) => { + const { isTrackingEnabled, } = useOrganizationTeams(); const t = useTranslations(); const [user] = useRecoilState(userState); const fullWidth = useRecoilValue(fullWidthState); @@ -26,12 +28,13 @@ const SettingsLayout = ({ children }: { children: JSX.Element }) => { { title: t('common.SETTINGS'), href: pathName as string }, { title: t(`common.${endWord}`), href: pathName as string } ]; - + if (!user) { return ; } else { return ( @@ -39,12 +42,12 @@ const SettingsLayout = ({ children }: { children: JSX.Element }) => { + > - +
diff --git a/apps/web/app/[locale]/task/[id]/page.tsx b/apps/web/app/[locale]/task/[id]/page.tsx index bbde108e6..4c029a475 100644 --- a/apps/web/app/[locale]/task/[id]/page.tsx +++ b/apps/web/app/[locale]/task/[id]/page.tsx @@ -53,7 +53,7 @@ const TaskDetails = () => { showTimer={!profile.isAuthUser && isTrackingEnabled} childrenClassName="bg-white dark:bg-dark--theme" > -
+
+ plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]) + ); + + const todayTasks = todayPlan + .map((plan) => { + return plan.tasks ? plan.tasks : []; + }) + .flat(); + + const futureTasks = + futurePlans && + futurePlans + .map((plan) => { + return plan.tasks ? plan.tasks : []; + }) + .flat(); + const outstandingPlans = profileDailyPlans.items && [...profileDailyPlans.items] @@ -290,14 +310,15 @@ export function useDailyPlan() { // Include only no completed tasks tasks: plan.tasks?.filter((task) => task.status !== 'completed') })) + .map((plan) => ({ + ...plan, + // Include only tasks that are not added yet to the today plan or future plans + tasks: plan.tasks?.filter( + (_task) => ![...todayTasks, ...futureTasks].find((task) => task.id === _task.id) + ) + })) .filter((plan) => plan.tasks?.length && plan.tasks.length > 0); - const todayPlan = - profileDailyPlans.items && - [...profileDailyPlans.items].filter((plan) => - plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]) - ); - const sortedPlans = profileDailyPlans.items && [...profileDailyPlans.items].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); diff --git a/apps/web/app/hooks/features/useEmployee.ts b/apps/web/app/hooks/features/useEmployee.ts index cf0cae1aa..916877137 100644 --- a/apps/web/app/hooks/features/useEmployee.ts +++ b/apps/web/app/hooks/features/useEmployee.ts @@ -1,10 +1,11 @@ -import { getWorkingEmployeesAPI } from '@app/services/client/api'; +import { getWorkingEmployeesAPI, updateEmployeeAPI } from '@app/services/client/api'; import { workingEmployeesEmailState, workingEmployeesState } from '@app/stores/employee'; import { useCallback, useEffect } from 'react'; import { useRecoilState } from 'recoil'; import { useQuery } from '../useQuery'; import { useAuthenticateUser } from './useAuthenticateUser'; +import { IUpdateEmployee } from '@app/interfaces'; export const useEmployee = () => { const { user } = useAuthenticateUser(); @@ -39,3 +40,19 @@ export const useEmployee = () => { workingEmployeesEmail }; }; + + +export const useEmployeeUpdate = () => { + const { queryCall: employeeUpdateQuery, loading: isLoading } = useQuery(updateEmployeeAPI); + + const updateEmployee = useCallback(({ id, data + }: { id: string, data: IUpdateEmployee }) => { + employeeUpdateQuery({ id, data }) + .then((res) => res.data) + .catch((error) => { + console.log(error); + }); + }, []); + + return { updateEmployee, isLoading } +} diff --git a/apps/web/app/hooks/features/useGetTasksStatsData.ts b/apps/web/app/hooks/features/useGetTasksStatsData.ts new file mode 100644 index 000000000..37e8548d9 --- /dev/null +++ b/apps/web/app/hooks/features/useGetTasksStatsData.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from 'react'; +import { useTaskStatistics } from './useTaskStatistics'; +import { useIntersectionObserver } from '@uidotdev/usehooks'; + +export function useGetTasksStatsData(employeeId: string | undefined, triggerWithIObserver = true) { + const refMap = useRef(new Map()); + const { getTasksStatsData } = useTaskStatistics(); + const [IObserverRef, entry] = useIntersectionObserver({ + threshold: 0, + root: null, + rootMargin: '0px' + }); + + useEffect(() => { + if (!employeeId) return; + + const map = refMap.current; + + const loadTaskStats = () => { + if (!map.has(employeeId)) { + map.set(employeeId, true); + getTasksStatsData(employeeId); + } + }; + + const supported = 'IntersectionObserver' in window; + + if (!triggerWithIObserver || !supported) { + loadTaskStats(); + } + + if (entry?.isIntersecting && supported) { + loadTaskStats(); + } + }, [employeeId, triggerWithIObserver, entry]); + + return IObserverRef; +} diff --git a/apps/web/app/hooks/features/useTaskActivity.ts b/apps/web/app/hooks/features/useTaskActivity.ts index 4764ec406..13ecbaf2f 100644 --- a/apps/web/app/hooks/features/useTaskActivity.ts +++ b/apps/web/app/hooks/features/useTaskActivity.ts @@ -40,6 +40,7 @@ export function useTaskTimeSheets(id: string) { return { taskTimesheets, getTaskTimesheets, - loading + loading, + loadTaskStatsIObserverRef: profile.loadTaskStatsIObserverRef }; } diff --git a/apps/web/app/hooks/features/useTaskStatistics.ts b/apps/web/app/hooks/features/useTaskStatistics.ts index f92e64fed..39c724ebd 100644 --- a/apps/web/app/hooks/features/useTaskStatistics.ts +++ b/apps/web/app/hooks/features/useTaskStatistics.ts @@ -161,7 +161,7 @@ export function useTaskStatistics(addSeconds = 0) { Math.min( Math.floor( (((_task?.totalWorkedTime || timeSheet?.duration || 0) + addSeconds) * 100) / - (estimate || _task?.estimate || 0) + (estimate || _task?.estimate || 0) ), 100 ), diff --git a/apps/web/app/hooks/features/useTimeLogs.ts b/apps/web/app/hooks/features/useTimeLogs.ts index fe3b31e2e..bd18fa220 100644 --- a/apps/web/app/hooks/features/useTimeLogs.ts +++ b/apps/web/app/hooks/features/useTimeLogs.ts @@ -6,9 +6,11 @@ import { useQuery } from '../useQuery'; import { useCallback, useEffect } from 'react'; import moment from 'moment'; import { useFirstLoad } from '../useFirstLoad'; +import { useUserProfilePage } from '..'; export function useTimeLogs() { const { user } = useAuthenticateUser(); + const profile = useUserProfilePage(); const { firstLoadData: firstLoadTimeLogs, firstLoad } = useFirstLoad(); @@ -23,9 +25,10 @@ export function useTimeLogs() { queryTimerLogsDailyReport({ tenantId: user?.tenantId ?? '', organizationId: user?.employee.organizationId ?? '', - employeeId: user?.employee.id ?? '', + employeeIds: [profile.member?.employeeId ?? ''], startDate, - endDate + endDate, + }) .then((response) => { if (response.data && Array.isArray(response.data)) { @@ -37,11 +40,12 @@ export function useTimeLogs() { }); }, [ + profile.member?.employeeId, queryTimerLogsDailyReport, setTimerLogsDailyReport, user?.employee.id, user?.employee.organizationId, - user?.tenantId + user?.tenantId, ] ); diff --git a/apps/web/app/hooks/features/useUserDetails.ts b/apps/web/app/hooks/features/useUserDetails.ts index e624a71da..0b8a6691f 100644 --- a/apps/web/app/hooks/features/useUserDetails.ts +++ b/apps/web/app/hooks/features/useUserDetails.ts @@ -1,19 +1,18 @@ 'use client'; import { ITeamTask } from '@app/interfaces'; -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { useAuthenticateUser } from './useAuthenticateUser'; import { useAuthTeamTasks } from './useAuthTeamTasks'; import { useOrganizationTeams } from './useOrganizationTeams'; -import { useTaskStatistics } from './useTaskStatistics'; import { useTeamTasks } from './useTeamTasks'; +import { useGetTasksStatsData } from './useGetTasksStatsData'; export function useUserDetails(memberId: string) { const { activeTeam } = useOrganizationTeams(); const { activeTeamTask, updateTask } = useTeamTasks(); const { user: auth } = useAuthenticateUser(); - const { getTasksStatsData } = useTaskStatistics(); const members = activeTeam?.members || []; @@ -32,12 +31,7 @@ export function useUserDetails(memberId: string) { /* Filtering the tasks */ const tasksGrouped = useAuthTeamTasks(userProfile); - useEffect(() => { - if (employeeId) { - getTasksStatsData(employeeId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [employeeId]); + const loadTaskStatsIObserverRef = useGetTasksStatsData(employeeId); const assignTask = useCallback( (task: ITeamTask) => { @@ -59,7 +53,8 @@ export function useUserDetails(memberId: string) { userProfile, tasksGrouped, member: matchUser, - assignTask + assignTask, + loadTaskStatsIObserverRef }; } diff --git a/apps/web/app/hooks/features/useUserProfilePage.ts b/apps/web/app/hooks/features/useUserProfilePage.ts index 037059780..90303976a 100644 --- a/apps/web/app/hooks/features/useUserProfilePage.ts +++ b/apps/web/app/hooks/features/useUserProfilePage.ts @@ -2,14 +2,14 @@ import { ITeamTask } from '@app/interfaces'; import { useParams } from 'next/navigation'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useAuthenticateUser } from './useAuthenticateUser'; import { useAuthTeamTasks } from './useAuthTeamTasks'; import { useOrganizationTeams } from './useOrganizationTeams'; -import { useTaskStatistics } from './useTaskStatistics'; import { useTeamTasks } from './useTeamTasks'; import { useRecoilValue } from 'recoil'; import { userDetailAccordion } from '@app/stores'; +import { useGetTasksStatsData } from './useGetTasksStatsData'; export function useUserProfilePage() { const { activeTeam } = useOrganizationTeams(); @@ -17,7 +17,6 @@ export function useUserProfilePage() { const userMemberId = useRecoilValue(userDetailAccordion); const { user: auth } = useAuthenticateUser(); - const { getTasksStatsData } = useTaskStatistics(); const params = useParams(); const memberId: string = useMemo(() => { return (params?.memberId ?? userMemberId) as string; @@ -36,17 +35,12 @@ export function useUserProfilePage() { const userProfile = isAuthUser ? auth : matchUser?.employee.user; - const employeeId = isAuthUser ? auth?.employee?.id : matchUser?.employeeId; - /* Filtering the tasks */ const tasksGrouped = useAuthTeamTasks(userProfile); - useEffect(() => { - if (employeeId) { - getTasksStatsData(employeeId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [employeeId]); + const employeeId = isAuthUser ? auth?.employee?.id : matchUser?.employeeId; + + const loadTaskStatsIObserverRef = useGetTasksStatsData(employeeId); const assignTask = useCallback( (task: ITeamTask) => { @@ -68,7 +62,8 @@ export function useUserProfilePage() { userProfile, tasksGrouped, member: matchUser, - assignTask + assignTask, + loadTaskStatsIObserverRef }; } diff --git a/apps/web/app/hooks/features/useUserSelectedPage.ts b/apps/web/app/hooks/features/useUserSelectedPage.ts index dd70d5e5b..711dc5f19 100644 --- a/apps/web/app/hooks/features/useUserSelectedPage.ts +++ b/apps/web/app/hooks/features/useUserSelectedPage.ts @@ -1,19 +1,18 @@ 'use client'; import { ITeamTask } from '@app/interfaces'; -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; import { useAuthenticateUser } from './useAuthenticateUser'; import { useAuthTeamTasks } from './useAuthTeamTasks'; import { useOrganizationTeams } from './useOrganizationTeams'; -import { useTaskStatistics } from './useTaskStatistics'; import { useTeamTasks } from './useTeamTasks'; +import { useGetTasksStatsData } from './useGetTasksStatsData'; export function useUserSelectedPage(id?: string) { const { activeTeam } = useOrganizationTeams(); const { activeTeamTask, updateTask } = useTeamTasks(); const { user: auth } = useAuthenticateUser(); - const { getTasksStatsData } = useTaskStatistics(); const memberId: string = id || ''; @@ -29,17 +28,12 @@ export function useUserSelectedPage(id?: string) { const userProfile = isAuthUser ? auth : matchUser?.employee.user; - const employeeId = isAuthUser ? auth?.employee?.id : matchUser?.employeeId; - /* Filtering the tasks */ const tasksGrouped = useAuthTeamTasks(userProfile); - useEffect(() => { - if (employeeId) { - getTasksStatsData(employeeId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [employeeId]); + const employeeId = isAuthUser ? auth?.employee?.id : matchUser?.employeeId; + + const loadTaskStatsIObserverRef = useGetTasksStatsData(employeeId); const assignTask = useCallback( (task: ITeamTask) => { @@ -61,7 +55,8 @@ export function useUserSelectedPage(id?: string) { userProfile, tasksGrouped, member: matchUser, - assignTask + assignTask, + loadTaskStatsIObserverRef }; } diff --git a/apps/web/app/hooks/index.ts b/apps/web/app/hooks/index.ts index 8e1f27f7b..93610f55d 100644 --- a/apps/web/app/hooks/index.ts +++ b/apps/web/app/hooks/index.ts @@ -69,3 +69,6 @@ export * from './integrations/useGitHubIntegration'; export * from './integrations/useIntegration'; export * from './integrations/useIntegrationTenant'; export * from './integrations/useIntegrationTypes'; + +export * from './useLocalStorageState'; +export * from './useScrollListener'; diff --git a/apps/web/app/hooks/useScrollListener.ts b/apps/web/app/hooks/useScrollListener.ts new file mode 100644 index 000000000..eb0ef9c4b --- /dev/null +++ b/apps/web/app/hooks/useScrollListener.ts @@ -0,0 +1,22 @@ +'use client'; +import React from 'react'; + +export function useScrollListener() { + const [scrolling, setScrolling] = React.useState(false); + React.useEffect(() => { + const handleScroll = () => { + if (window.scrollY > 100) { + setScrolling(true); + } else { + setScrolling(false); + } + }; + console.log(window.scrollY); + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + return { scrolling }; +} diff --git a/apps/web/app/interfaces/IEmployee.ts b/apps/web/app/interfaces/IEmployee.ts index 3844c9ff9..bed02d1c0 100644 --- a/apps/web/app/interfaces/IEmployee.ts +++ b/apps/web/app/interfaces/IEmployee.ts @@ -57,6 +57,7 @@ export interface IEmployee { } export type ICreateEmployee = Pick; +export type IUpdateEmployee = Pick export interface IRole { id?: string; diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts index 3c7091549..adf59e95a 100644 --- a/apps/web/app/interfaces/ITask.ts +++ b/apps/web/app/interfaces/ITask.ts @@ -109,7 +109,11 @@ export type ITaskStatus = | 'completed' | 'closed' | 'in review' - | 'open'; + | 'open' + | 'custom' + | 'ready-for-review' + | 'in-review' + | 'done'; export type ITaskIssue = 'Bug' | 'Task' | 'Story' | 'Epic'; @@ -130,7 +134,8 @@ export type ITaskStatusField = | 'epic' | 'project' | 'team' - | 'tags'; + | 'tags' + | 'status type'; export type ITaskStatusStack = { status: ITaskStatus; @@ -140,6 +145,7 @@ export type ITaskStatusStack = { issueType: ITaskIssue; version: IVersionProperty; epic: IEpicProperty; + 'status type': any; project: string; //TODO: these types are not strings, but rather objects for team and project. To reimplement team: string; //TODO: these types are not strings, but rather objects for team and project. To reimplement tags: any; //TODO: these types are not strings, but rather array of objects for tags. To reimplement diff --git a/apps/web/app/interfaces/ITaskStatus.ts b/apps/web/app/interfaces/ITaskStatus.ts index 3769e6163..1a5cdb91c 100644 --- a/apps/web/app/interfaces/ITaskStatus.ts +++ b/apps/web/app/interfaces/ITaskStatus.ts @@ -1,4 +1,4 @@ -export interface ITaskStatusItemList { +export interface ITaskStatusItemList extends TaskStatusWorkFlow { id: string; createdAt: string; updatedAt: string; @@ -14,19 +14,32 @@ export interface ITaskStatusItemList { isSystem?: boolean; projectId?: string; isCollapsed?: boolean; + organizationTeamId?: string | undefined | null; order?: number; + template?: TaskStatusEnum; } -export interface ITaskStatusCreate { - name?: string; - description?: string; - icon?: string; - value?: string; - color?: string; - projectId?: string; - organizationId?: string; - tenantId?: string | undefined | null; - organizationTeamId?: string | undefined | null; - isCollapsed?: boolean; - order?: number; +export interface ITaskStatusCreate + extends Partial>, + Partial> {} + +/** + * Default task statuses + */ +export enum TaskStatusEnum { + BACKLOG = 'backlog', + OPEN = 'open', + IN_PROGRESS = 'in-progress', + READY_FOR_REVIEW = 'ready-for-review', + IN_REVIEW = 'in-review', + BLOCKED = 'blocked', + DONE = 'done', + COMPLETED = 'completed', + CUSTOM = 'custom' +} + +export interface TaskStatusWorkFlow { + isTodo?: boolean; + isInProgress?: boolean; + isDone?: boolean; } diff --git a/apps/web/app/interfaces/timer/ITimerLogs.ts b/apps/web/app/interfaces/timer/ITimerLogs.ts index 5437d922c..4b034caf5 100644 --- a/apps/web/app/interfaces/timer/ITimerLogs.ts +++ b/apps/web/app/interfaces/timer/ITimerLogs.ts @@ -7,7 +7,7 @@ import { ITimerSlot } from './ITimerSlot'; export interface ITimerLogsDailyReportRequest { tenantId: string; organizationId: string; - employeeId: string; + employeeIds: string[]; startDate: Date; endDate: Date; } diff --git a/apps/web/app/services/client/api/activity/time-logs.ts b/apps/web/app/services/client/api/activity/time-logs.ts index 617ea1936..d711dd80e 100644 --- a/apps/web/app/services/client/api/activity/time-logs.ts +++ b/apps/web/app/services/client/api/activity/time-logs.ts @@ -5,14 +5,14 @@ import qs from 'qs'; export async function getTimerLogsDailyReportRequestAPI({ tenantId, organizationId, - employeeId, + employeeIds, startDate, endDate }: ITimerLogsDailyReportRequest) { const params = { tenantId: tenantId, organizationId: organizationId, - employeeId, + employeeIds, todayEnd: startDate.toISOString(), todayStart: endDate.toISOString() }; diff --git a/apps/web/app/services/client/api/employee.ts b/apps/web/app/services/client/api/employee.ts index ca9f0529b..0d5f28362 100644 --- a/apps/web/app/services/client/api/employee.ts +++ b/apps/web/app/services/client/api/employee.ts @@ -1,6 +1,6 @@ import { GAUZY_API_BASE_SERVER_URL } from '@app/constants'; -import { IWorkingEmployee, PaginationResponse } from '@app/interfaces'; -import { get } from '../axios'; +import { IUpdateEmployee, IWorkingEmployee, PaginationResponse } from '@app/interfaces'; +import { get, put } from '../axios'; import qs from 'qs'; export async function getWorkingEmployeesAPI(tenantId: string, organizationId: string) { @@ -15,3 +15,8 @@ export async function getWorkingEmployeesAPI(tenantId: string, organizationId: s return get>(endpoint, { tenantId }); } + + +export function updateEmployeeAPI({ id, data }: { id: string, data: IUpdateEmployee }) { + return put(`/employee/${id}`, data); +} diff --git a/apps/web/app/services/server/requests/employee.ts b/apps/web/app/services/server/requests/employee.ts index 30e65f9f9..a9cbbf33c 100644 --- a/apps/web/app/services/server/requests/employee.ts +++ b/apps/web/app/services/server/requests/employee.ts @@ -1,5 +1,5 @@ import { PaginationResponse } from '@app/interfaces'; -import { ICreateEmployee, IEmployee, IWorkingEmployee } from '@app/interfaces/IEmployee'; +import { ICreateEmployee, IEmployee, IUpdateEmployee, IWorkingEmployee } from '@app/interfaces/IEmployee'; import { serverFetch } from '../fetch'; import qs from 'qs'; @@ -29,3 +29,13 @@ export function getOrganizationEmployees(bearer_token: string, tenantId: string, tenantId }); } + +export function updateEmployees({ bearer_token, id, body }: { bearer_token: string, id: string, body: IUpdateEmployee }) { + return serverFetch({ + path: `/employee/${id}`, + method: 'PUT', + bearer_token, + body + + }) +} diff --git a/apps/web/app/stores/employee.ts b/apps/web/app/stores/employee.ts index 8c9ee1f08..ecd867c97 100644 --- a/apps/web/app/stores/employee.ts +++ b/apps/web/app/stores/employee.ts @@ -1,4 +1,4 @@ -import { IWorkingEmployee } from '@app/interfaces'; +import { IUpdateEmployee, IWorkingEmployee } from '@app/interfaces'; import { atom } from 'recoil'; export const workingEmployeesState = atom({ @@ -10,3 +10,9 @@ export const workingEmployeesEmailState = atom({ key: 'workingEmployeesEmailState', default: [] }); + + +export const employeeUpdateState = atom({ + key: 'employeeUpdateState', + default: null!, +}) diff --git a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx index 23b320fc5..aba87f708 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-main-info.tsx @@ -225,7 +225,7 @@ const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) = memberList.filter((member) => member.employee ? !task?.members.map((item) => item.userId).includes(member.employee.userId) && - member.employee?.isActive + member.employee?.isActive : false ), [memberList, task?.members] @@ -236,7 +236,7 @@ const ManageMembersPopover = (memberList: OT_Member[], task: ITeamTask | null) = memberList.filter((member) => member.employee ? task?.members.map((item) => item.userId).includes(member.employee?.userId) && - member.employee?.isActive + member.employee?.isActive : false ), [memberList, task?.members] diff --git a/apps/web/components/ui/checkbox.tsx b/apps/web/components/ui/checkbox.tsx new file mode 100644 index 000000000..e39edefa4 --- /dev/null +++ b/apps/web/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/apps/web/components/ui/data-table.tsx b/apps/web/components/ui/data-table.tsx index afeb6631e..2baa8f34c 100644 --- a/apps/web/components/ui/data-table.tsx +++ b/apps/web/components/ui/data-table.tsx @@ -86,9 +86,9 @@ function DataTable({ columns, data, footerRows, isHeader }: DataT {header.isPlaceholder ? null : flexRender( - header.column.columnDef.header, - header.getContext() - )} + header.column.columnDef.header, + header.getContext() + )}
diff --git a/apps/web/components/ui/input.tsx b/apps/web/components/ui/input.tsx new file mode 100644 index 000000000..4f3a2820f --- /dev/null +++ b/apps/web/components/ui/input.tsx @@ -0,0 +1,26 @@ +import * as React from "react" + +import { cn } from "lib/utils" + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface InputProps + extends React.InputHTMLAttributes { } + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/web/lib/components/avatar.tsx b/apps/web/lib/components/avatar.tsx index 9f7c2fb0e..5992aac9d 100644 --- a/apps/web/lib/components/avatar.tsx +++ b/apps/web/lib/components/avatar.tsx @@ -61,8 +61,8 @@ export function Avatar({ height: size, ...(backgroundColor ? { - backgroundColor - } + backgroundColor + } : {}) }} > diff --git a/apps/web/lib/components/time-picker/index.tsx b/apps/web/lib/components/time-picker/index.tsx index 8104aef8a..210c1281d 100644 --- a/apps/web/lib/components/time-picker/index.tsx +++ b/apps/web/lib/components/time-picker/index.tsx @@ -39,7 +39,7 @@ export function TimePicker({ onChange, defaultValue }: IPopoverTimePicker) { + +
+); + +const CommentInputArea = () => ( + <> + Add a comment for this change that the employee will see +
+ +