diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 3df5aebe6..126c7be80 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -23,7 +23,7 @@ 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 { ActivityCalendar } from 'lib/features/activity/calendar'; +// import { ActivityCalendar } from 'lib/features/activity/calendar'; export type FilterTab = 'Tasks' | 'Screenshots' | 'Apps' | 'Visited Sites'; @@ -149,9 +149,9 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId {/* TaskFilter */} <TaskFilter profile={profile} hook={hook} /> </MainHeader> - <div className="p-1"> + {/* <div className="p-1"> <ActivityCalendar /> - </div> + </div> */} </ResizablePanel> <ResizableHandle withHandle /> <ResizablePanel defaultSize={65} maxSize={95} className="!overflow-y-scroll custom-scrollbar"> diff --git a/apps/web/lib/features/activity/calendar.tsx b/apps/web/lib/features/activity/calendar.tsx deleted file mode 100644 index b250ccf6b..000000000 --- a/apps/web/lib/features/activity/calendar.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { useTimeLogs } from '@app/hooks/features/useTimeLogs'; -import { useEffect, useState } from 'react'; -import { CalendarDatum, ResponsiveCalendar } from '@nivo/calendar'; -import Skeleton from 'react-loading-skeleton'; -import moment from 'moment'; - -export function ActivityCalendar() { - const { timerLogsDailyReport, timerLogsDailyReportLoading } = useTimeLogs(); - const [calendarData, setCalendarData] = useState<CalendarDatum[]>([]); - - useEffect(() => { - setCalendarData( - timerLogsDailyReport.map((el) => ({ value: Number((el.sum / 3600).toPrecision(2)), day: el.date })) - ); - }, [timerLogsDailyReport]); - - return ( - <div className=" h-40 w-full flex items-center justify-center overflow-y-hidden overflow-x-auto"> - {timerLogsDailyReportLoading ? ( - <ActivityCalendarSkeleton /> - ) : ( - <ResponsiveCalendar - data={calendarData} - from={moment().startOf('year').toDate()} - to={moment().startOf('year').toDate()} - emptyColor="#ffffff" - colors={['#D5D8F6', '#ACB2EC', '#838DE2', '#5B67D7', '#3343CC']} - yearSpacing={40} - monthBorderWidth={0} - dayBorderWidth={0} - daySpacing={2} - monthLegendPosition="before" - margin={{ top: 25, right: 5, bottom: 10, left: 5 }} - legends={[ - { - anchor: 'bottom-right', - direction: 'row', - translateY: 36, - itemCount: 4, - itemWidth: 42, - itemHeight: 36, - itemsSpacing: 14, - itemDirection: 'right-to-left' - } - ]} - monthSpacing={20} - monthLegend={(_, __, d) => d.toLocaleString('en-US', { month: 'short' })} - /> - )} - </div> - ); -} - -// Skeletons -function ActivityCalendarSkeleton() { - const { innerWidth: deviceWith } = window; - - const skeletons = Array.from(Array(12)); - - return ( - <div className="w-full overflow-hidden flex h-32 items-center justify-around"> - {skeletons.map((_, index) => ( - <Skeleton - key={index} - width={(deviceWith - (deviceWith * 10) / 100) / 12} - className=" dark:bg-transparent h-32" - /> - ))} - </div> - ); -} diff --git a/apps/web/lib/features/activity/screen-calendar.tsx b/apps/web/lib/features/activity/screen-calendar.tsx new file mode 100644 index 000000000..5d0ff1f3e --- /dev/null +++ b/apps/web/lib/features/activity/screen-calendar.tsx @@ -0,0 +1,7 @@ +"use client" +import React from 'react' +import { ActivityCalendar } from '../integrations/activity-calendar' + +export function ScreenCalendar() { + return (<ActivityCalendar />) +} diff --git a/apps/web/lib/features/integrations/activity-calendar/index.tsx b/apps/web/lib/features/integrations/activity-calendar/index.tsx new file mode 100644 index 000000000..96e5ef7f1 --- /dev/null +++ b/apps/web/lib/features/integrations/activity-calendar/index.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { useTimeLogs } from '@app/hooks/features/useTimeLogs'; +import { useEffect, useState } from 'react'; +import { CalendarDatum, ResponsiveCalendar } from '@nivo/calendar'; +import Skeleton from 'react-loading-skeleton'; +import moment from 'moment'; + +export function ActivityCalendar() { + const { timerLogsDailyReport, timerLogsDailyReportLoading } = useTimeLogs(); + const [calendarData, setCalendarData] = useState<CalendarDatum[]>([]); + useEffect(() => { + setCalendarData( + timerLogsDailyReport.map((el) => ({ value: Number((el.sum / 3600).toPrecision(2)), day: el.date })) + ); + }, [timerLogsDailyReport]); + + return ( + <div className=" h-[650px] w-full flex items-center justify-center overflow-y-hidden overflow-x-auto"> + {timerLogsDailyReportLoading ? ( + <ActivityCalendarSkeleton /> + ) : ( + <div className='flex flex-col w-full h-full relative'> + <ActivityLegend /> + <ResponsiveCalendar + data={calendarData} + from={moment().startOf('year').toDate()} + to={moment().startOf('year').toDate()} + emptyColor="#ffffff" + colors={['#ADD8E6', '#9370DB', '#6A5ACD', '#0000FF']} + yearSpacing={40} + monthBorderWidth={0} + dayBorderWidth={0} + daySpacing={2} + monthLegendPosition="before" + margin={{ top: 10, right: 5, bottom: 10, left: 5 }} + legends={[ + { + anchor: 'bottom-right', + direction: 'row', + translateY: 36, + itemCount: 3, + itemWidth: 42, + itemHeight: 36, + itemsSpacing: 14, + itemDirection: 'right-to-left', + data: [ + { color: '#0000FF', label: '8 hours or more', id: 'legend-blue' }, + { color: '#6A5ACD', label: '6 hours', id: 'legend-slateblue' }, + { color: '#9370DB', label: '4 hours', id: 'legend-purple' }, + { color: '#ADD8E6', label: '2 hours', id: 'legend-light-blue' } + ] + } + ]} + monthSpacing={20} + monthLegend={(_, __, d) => d.toLocaleString('en-US', { month: 'short' })} + /> + + + </div> + )} + </div> + ); +} + +// Skeletons +function ActivityCalendarSkeleton() { + const { innerWidth: deviceWith } = window; + + const skeletons = Array.from(Array(12)); + + return ( + <div className="w-full overflow-hidden flex h-32 items-center justify-around"> + {skeletons.map((_, index) => ( + <Skeleton + key={index} + width={(deviceWith - (deviceWith * 10) / 100) / 12} + className=" dark:bg-transparent h-32" + /> + ))} + </div> + ); +} + +function ActivityLegend() { + return ( + <div className="flex flex-col w-full items-start p-4 bg-white dark:bg-dark--theme-light rounded-lg shadow-sm"> + <h3 className="text-lg font-bold mb-2">Legend</h3> + <div className="flex items-center mb-2" id="legend-blue"> + <span className="w-4 h-4 inline-block mr-2" style={{ backgroundColor: '#0000FF' }}></span> + <span>8 Hours or more</span> + </div> + <div className="flex items-center mb-2" id="legend-slateblue"> + <span className="w-4 h-4 inline-block mr-2" style={{ backgroundColor: '#6A5ACD' }}></span> + <span>6 Hours</span> + </div> + <div className="flex items-center mb-2" id="legend-purple"> + <span className="w-4 h-4 inline-block mr-2" style={{ backgroundColor: '#9370DB' }}></span> + <span>4 Hours</span> + </div> + <div className="flex items-center mb-2" id="legend-light-blue"> + <span className="w-4 h-4 inline-block mr-2" style={{ backgroundColor: '#ADD8E6' }}></span> + <span>2 Hours</span> + </div> + </div> + ) +} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 6c7415910..22c311cae 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -27,13 +27,14 @@ import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; import '../../../styles/style.css'; import { AddManualTimeModal } from '../manual-time/add-manual-time-modal'; +import { useTimeLogs } from '@app/hooks/features/useTimeLogs'; -export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; +export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan' | 'stats'; type ITabs = { tab: ITab; name: string; - count: number; + count?: number; description: string; }; @@ -55,7 +56,7 @@ export function useTaskFilter(profile: I_UserProfilePage) { const { activeTeamManagers, activeTeam } = useOrganizationTeams(); const { user } = useAuthenticateUser(); const { profileDailyPlans } = useDailyPlan(); - + const { timerLogsDailyReport } = useTimeLogs(); const isManagerConnectedUser = useMemo( () => activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id), [activeTeamManagers, user?.id] @@ -79,6 +80,7 @@ export function useTaskFilter(profile: I_UserProfilePage) { unassigned: profile.tasksGrouped.unassignedTasks, assigned: profile.tasksGrouped.assignedTasks, worked: profile.tasksGrouped.workedTasks, + stats: [], dailyplan: [] // Change this soon }), [profile.tasksGrouped.assignedTasks, profile.tasksGrouped.unassignedTasks, profile.tasksGrouped.workedTasks] @@ -109,17 +111,25 @@ export function useTaskFilter(profile: I_UserProfilePage) { name: t('common.UNASSIGNED'), description: t('task.tabFilter.UNASSIGNED_DESCRIPTION'), count: profile.tasksGrouped.unassignedTasks.length - } + }, + ]; // For tabs on profile page, display "Worked" and "Daily Plan" only for the logged in user or managers if (activeTeam?.shareProfileView || canSeeActivity) { + tabs.push({ tab: 'dailyplan', name: 'Daily Plan', description: 'This tab shows all yours tasks planned', count: profile.tasksGrouped.planned }); + tabs.push({ + tab: 'stats', + name: 'Stats', + description: 'This tab shows all stats', + count: timerLogsDailyReport.length, + }); tabs.unshift({ tab: 'worked', name: t('common.WORKED'), @@ -192,9 +202,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); diff --git a/apps/web/lib/features/user-profile-tasks.tsx b/apps/web/lib/features/user-profile-tasks.tsx index f7857f225..fcd17d383 100644 --- a/apps/web/lib/features/user-profile-tasks.tsx +++ b/apps/web/lib/features/user-profile-tasks.tsx @@ -5,6 +5,7 @@ import { TaskCard } from './task/task-card'; import { I_TaskFilter } from './task/task-filters'; import { useTranslations } from 'next-intl'; import { useMemo } from 'react'; +import { ScreenCalendar } from './activity/screen-calendar'; type Props = { tabFiltered: I_TaskFilter; profile: I_UserProfilePage; @@ -65,15 +66,14 @@ export function UserProfileTask({ profile, tabFiltered }: Props) { isAuthUser={profile.isAuthUser} activeAuthTask={true} profile={profile} - taskBadgeClassName={` ${ - profile.activeUserTeamTask?.issueType === 'Bug' - ? '!px-[0.3312rem] py-[0.2875rem]' - : '!px-[0.375rem] py-[0.375rem]' - } rounded-sm`} + taskBadgeClassName={` ${profile.activeUserTeamTask?.issueType === 'Bug' + ? '!px-[0.3312rem] py-[0.2875rem]' + : '!px-[0.375rem] py-[0.375rem]' + } rounded-sm`} taskTitleClassName="mt-[0.0625rem]" /> )} - + {tabFiltered.tab === 'stats' && <ScreenCalendar />} {tabFiltered.tab === 'dailyplan' && <UserProfilePlans />} {tabFiltered.tab === 'worked' && otherTasks.length > 0 && ( @@ -96,11 +96,10 @@ export function UserProfileTask({ profile, tabFiltered }: Props) { activeAuthTask={false} viewType={tabFiltered.tab === 'unassigned' ? 'unassign' : 'default'} profile={profile} - taskBadgeClassName={`${ - task.issueType === 'Bug' - ? '!px-[0.3312rem] py-[0.2875rem]' - : '!px-[0.375rem] py-[0.375rem]' - } rounded-sm`} + taskBadgeClassName={`${task.issueType === 'Bug' + ? '!px-[0.3312rem] py-[0.2875rem]' + : '!px-[0.375rem] py-[0.375rem]' + } rounded-sm`} taskTitleClassName="mt-[0.0625rem]" /> </li>