From 6c1e2980fdef8f920242c806bde27f33d29462e7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 5 Aug 2024 20:07:24 +0200 Subject: [PATCH 01/18] fix: the same task should not be planned multiple times for the same day --- apps/web/lib/features/task/task-card.tsx | 57 ++++++++++++++---------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 197f1dbad..aa2212556 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -514,16 +514,23 @@ function TaskCardMenu({ const canSeeActivity = useCanSeeActivityScreen(); const { todayPlan, futurePlans } = useDailyPlan(); - const taskPlannedToday = todayPlan[0]?.tasks?.find((_task) => _task.id === task.id); + const taskPlannedToday = useMemo( + () => todayPlan[todayPlan.length - 1]?.tasks?.find((_task) => _task.id === task.id), + [task.id, todayPlan] + ); - const taskPlannedTomorrow = futurePlans - .filter((_plan) => - moment(_plan.date) - .format('YYYY-MM-DD') - ?.toString() - ?.startsWith(moment()?.add(1, 'day').format('YYYY-MM-DD')) - )[0] - ?.tasks?.find((_task) => _task.id === task.id); + const taskPlannedTomorrow = useMemo( + () => + futurePlans + .filter((_plan) => + moment(_plan.date) + .format('YYYY-MM-DD') + ?.toString() + ?.startsWith(moment()?.add(1, 'day').format('YYYY-MM-DD')) + )[0] + ?.tasks?.find((_task) => _task.id === task.id), + [futurePlans, task.id] + ); return ( @@ -575,14 +582,17 @@ function TaskCardMenu({ <>
-
  • - -
  • + {!taskPlannedToday && ( +
  • + +
  • + )} +
  • { @@ -717,12 +727,13 @@ export function PlanTask({ return ( <> - {planMode === 'today' && !taskPlannedToday && ( - - {isPending ? ( + + {isPending || createDailyPlanLoading ? ( ) : ( t('dailyPlan.PLAN_FOR_TODAY') @@ -751,7 +762,7 @@ export function PlanTask({ )} {planMode === 'custom' && t('dailyPlan.PLAN_FOR_SOME_DAY')} - + ); } From d46c400a51bad7045c0b7f658f05009ae0e551a7 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Mon, 5 Aug 2024 20:47:17 +0200 Subject: [PATCH 02/18] 2682 bug daily plan task action menu hidden UI (#2833) * resize the task info area * fix: fix action menu hidden --- apps/web/lib/features/task/task-card.tsx | 41 ++++++++++++------------ 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 197f1dbad..91968ae1c 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -151,9 +151,9 @@ export function TaskCard(props: Props) {
  • -
    +
    {/* Task information */} -
    +
    {/* Active Task Status Dropdown (It's a dropdown that allows the user to change the status of the task.)*/} -
    +
    setLoading(load)} - className="min-w-[10.625rem]" + className="min-w-[10.625rem] text-sm" />
    - {/* TaskCardMenu */} - {task && currentMember && ( - - )} +
    + {task && currentMember && ( + + )} +
    @@ -285,9 +286,7 @@ export function TaskCard(props: Props) { )}
    - setLoading(load)} /> - {task && currentMember && ( - {!loading && } + {!loading && } {loading && } From 2b52a8e8d93d9b0663d50184092022715a48d4ff Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Aug 2024 00:03:40 +0200 Subject: [PATCH 03/18] fix: improve the delay when switching tabs (unassigend,assigned) --- .../app/[locale]/profile/[memberId]/page.tsx | 38 +++++--- apps/web/lib/features/task/task-card.tsx | 95 +++++++++++-------- apps/web/lib/features/task/task-filters.tsx | 34 ++++--- apps/web/lib/features/task/task-status.tsx | 30 +++--- apps/web/lib/features/task/task-times.tsx | 43 ++++++--- apps/web/lib/features/user-profile-tasks.tsx | 8 +- 6 files changed, 149 insertions(+), 99 deletions(-) diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 5e1860dbd..3df5aebe6 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -41,21 +41,33 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId const setActivityTypeFilter = useSetRecoilState(activityTypeState); const hook = useTaskFilter(profile); - const isManagerConnectedUser = activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id); - const canSeeActivity = profile.userProfile?.id === user?.id || isManagerConnectedUser != -1; + const isManagerConnectedUser = useMemo( + () => activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id), + [activeTeamManagers, user?.id] + ); + const canSeeActivity = useMemo( + () => profile.userProfile?.id === user?.id || isManagerConnectedUser != -1, + [isManagerConnectedUser, profile.userProfile?.id, user?.id] + ); const t = useTranslations(); - const breadcrumb = [ - { title: activeTeam?.name || '', href: '/' }, - { title: JSON.parse(t('pages.profile.BREADCRUMB')) || '', href: `/profile/${params.memberId}` } - ]; - - const activityScreens = { - Tasks: , - Screenshots: , - Apps: , - 'Visited Sites': - }; + const breadcrumb = useMemo( + () => [ + { title: activeTeam?.name || '', href: '/' }, + { title: JSON.parse(t('pages.profile.BREADCRUMB')) || '', href: `/profile/${params.memberId}` } + ], + [activeTeam?.name, params.memberId, t] + ); + + const activityScreens = useMemo( + () => ({ + Tasks: , + Screenshots: , + Apps: , + 'Visited Sites': + }), + [hook, profile] + ); const profileIsAuthUser = useMemo(() => profile.isAuthUser, [profile.isAuthUser]); const hookFilterType = useMemo(() => hook.filterType, [hook.filterType]); diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 152ba751d..937afae9e 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -101,51 +101,64 @@ export function TaskCard(props: Props) { const seconds = useRecoilValue(timerSecondsState); const { activeTaskDailyStat, activeTaskTotalStat, addSeconds } = useTaskStatistics(seconds); const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); - const members = activeTeam?.members || []; - const currentMember = members.find((m) => { - return m.employee.user?.id === profile?.userProfile?.id; - }); + const members = useMemo(() => activeTeam?.members || [], [activeTeam?.members]); + const currentMember = useMemo( + () => + members.find((m) => { + return m.employee.user?.id === profile?.userProfile?.id; + }), + [members, profile?.userProfile?.id] + ); const { h, m } = secondsToTime((activeTaskTotalStat?.duration || 0) + addSeconds); - const totalWork = - isAuthUser && activeAuthTask ? ( -
    - {t('pages.taskDetails.TOTAL_TIME')}: - - {h}h : {m}m - -
    - ) : ( - <> - ); - + const totalWork = useMemo( + () => + isAuthUser && activeAuthTask ? ( +
    + {t('pages.taskDetails.TOTAL_TIME')}: + + {h}h : {m}m + +
    + ) : ( + <> + ), + [activeAuthTask, h, isAuthUser, m, t] + ); // Daily work - const { h: dh, m: dm } = secondsToTime((activeTaskDailyStat?.duration || 0) + addSeconds); - const todayWork = - isAuthUser && activeAuthTask ? ( -
    - {t('common.TOTAL_WORK')} - - {dh}h : {dm}m - -
    - ) : ( - <> - ); - + const { h: dh, m: dm } = useMemo( + () => secondsToTime((activeTaskDailyStat?.duration || 0) + addSeconds), + [activeTaskDailyStat?.duration, addSeconds] + ); + const todayWork = useMemo( + () => + isAuthUser && activeAuthTask ? ( +
    + {t('common.TOTAL_WORK')} + + {dh}h : {dm}m + +
    + ) : ( + <> + ), + [activeAuthTask, dh, dm, isAuthUser, t] + ); const memberInfo = useTeamMemberCard(currentMember || undefined); const taskEdition = useTMCardTaskEdit(task); - const activeMembers = task != null && task?.members?.length > 0; - const hasMembers = task?.members && task?.members?.length > 0; - const taskAssignee: ImageOverlapperProps[] = - task?.members?.map((member: any) => { - return { - id: member.user?.id, - url: member.user?.imageUrl, - alt: member.user?.firstName - }; - }) || []; - + const activeMembers = useMemo(() => task != null && task?.members?.length > 0, [task]); + const hasMembers = useMemo(() => task?.members && task?.members?.length > 0, [task?.members]); + const taskAssignee: ImageOverlapperProps[] = useMemo( + () => + task?.members?.map((member: any) => { + return { + id: member.user?.id, + url: member.user?.imageUrl, + alt: member.user?.firstName + }; + }) || [], + [task?.members] + ); return ( <> } & IClassName) { const t = useTranslations(); - const members = task?.members || []; + const members = useMemo(() => task?.members || [], [task?.members]); return (
    diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index ff6c8f95d..9932545f3 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -48,15 +48,22 @@ type StatusFilter = { [x in IStatusType]: string[] }; */ export function useTaskFilter(profile: I_UserProfilePage) { const t = useTranslations(); - const defaultValue = - typeof window !== 'undefined' ? (window.localStorage.getItem('task-tab') as ITab) || null : 'worked'; - + const defaultValue = useMemo( + () => (typeof window !== 'undefined' ? (window.localStorage.getItem('task-tab') as ITab) || null : 'worked'), + [] + ); const { activeTeamManagers, activeTeam } = useOrganizationTeams(); const { user } = useAuthenticateUser(); const { profileDailyPlans } = useDailyPlan(); - const isManagerConnectedUser = activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id); - const canSeeActivity = profile.userProfile?.id === user?.id || isManagerConnectedUser != -1; + const isManagerConnectedUser = useMemo( + () => activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id), + [activeTeamManagers, user?.id] + ); + const canSeeActivity = useMemo( + () => profile.userProfile?.id === user?.id || isManagerConnectedUser != -1, + [isManagerConnectedUser, profile.userProfile?.id, user?.id] + ); const [tab, setTab] = useState(defaultValue || 'worked'); const [filterType, setFilterType] = useState(undefined); @@ -67,14 +74,17 @@ export function useTaskFilter(profile: I_UserProfilePage) { const [taskName, setTaskName] = useState(''); - const tasksFiltered: { [x in ITab]: ITeamTask[] } = { - unassigned: profile.tasksGrouped.unassignedTasks, - assigned: profile.tasksGrouped.assignedTasks, - worked: profile.tasksGrouped.workedTasks, - dailyplan: [] // Change this soon - }; + const tasksFiltered: { [x in ITab]: ITeamTask[] } = useMemo( + () => ({ + unassigned: profile.tasksGrouped.unassignedTasks, + assigned: profile.tasksGrouped.assignedTasks, + worked: profile.tasksGrouped.workedTasks, + dailyplan: [] // Change this soon + }), + [profile.tasksGrouped.assignedTasks, profile.tasksGrouped.unassignedTasks, profile.tasksGrouped.workedTasks] + ); - const tasks = tasksFiltered[tab]; + const tasks = useMemo(() => tasksFiltered[tab], [tab, tasksFiltered]); const outclickFilterCard = useOutsideClick(() => { if (filterType === 'search' && taskName.trim().length === 0) { diff --git a/apps/web/lib/features/task/task-status.tsx b/apps/web/lib/features/task/task-status.tsx index 72d78f1f1..df23da5ab 100644 --- a/apps/web/lib/features/task/task-status.tsx +++ b/apps/web/lib/features/task/task-status.tsx @@ -775,18 +775,18 @@ export function TaskStatus({ isEpic }: PropsWithChildren< TStatusItem & - IClassName & { - active?: boolean; - issueType?: 'status' | 'issue'; - showIssueLabels?: boolean; - forDetails?: boolean; - titleClassName?: string; - cheched?: boolean; - sidebarUI?: boolean; - value?: string; - isVersion?: boolean; - isEpic?: boolean; - } + IClassName & { + active?: boolean; + issueType?: 'status' | 'issue'; + showIssueLabels?: boolean; + forDetails?: boolean; + titleClassName?: string; + cheched?: boolean; + sidebarUI?: boolean; + value?: string; + isVersion?: boolean; + isEpic?: boolean; + } >) { const { theme } = useTheme(); const readableColorHex = readableColor(backgroundColor || (theme === 'light' ? '#FFF' : '#000')); @@ -839,8 +839,8 @@ export function TaskStatus({ style={ isVersion || isEpic ? { - color: theme === 'light' ? '#000' : '#FFF' - } + color: theme === 'light' ? '#000' : '#FFF' + } : {} } > @@ -996,7 +996,7 @@ export function StatusDropdown({ sidebarUI && ['text-xs'], 'text-dark dark:text-white bg-[#F2F2F2] dark:bg-dark--theme-light', forDetails && - 'bg-transparent border dark:border-[#FFFFFF33] dark:bg-[#1B1D22]', + 'bg-transparent border dark:border-[#FFFFFF33] dark:bg-[#1B1D22]', taskStatusClassName )} name={ diff --git a/apps/web/lib/features/task/task-times.tsx b/apps/web/lib/features/task/task-times.tsx index 79fd6b9f3..92d3ea545 100644 --- a/apps/web/lib/features/task/task-times.tsx +++ b/apps/web/lib/features/task/task-times.tsx @@ -4,6 +4,7 @@ import { IClassName, ITeamTask, Nullable, OT_Member } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { Text, Tooltip } from 'lib/components'; import { useTranslations } from 'next-intl'; +import { useMemo } from 'react'; type Props = { task: Nullable; @@ -18,23 +19,35 @@ type Props = { export function TaskTimes({ className, task, memberInfo, showDaily = true, showTotal = true, isBlock = false }: Props) { // For public page const { activeTeam } = useOrganizationTeams(); - const currentMember = activeTeam?.members.find((member) => member.id === memberInfo?.member?.id || memberInfo?.id); + const currentMember = useMemo( + () => activeTeam?.members.find((member) => member.id === memberInfo?.member?.id || memberInfo?.id), + [activeTeam?.members, memberInfo?.id, memberInfo?.member?.id] + ); - const { h, m } = secondsToTime( - (currentMember?.totalWorkedTasks && - currentMember?.totalWorkedTasks?.length && - currentMember?.totalWorkedTasks - .filter((t) => t.id === task?.id) - .reduce((previousValue, currentValue) => previousValue + currentValue.duration, 0)) || - 0 + const { h, m } = useMemo( + () => + secondsToTime( + (currentMember?.totalWorkedTasks && + currentMember?.totalWorkedTasks?.length && + currentMember?.totalWorkedTasks + .filter((t) => t.id === task?.id) + .reduce((previousValue, currentValue) => previousValue + currentValue.duration, 0)) || + 0 + ), + [currentMember?.totalWorkedTasks, task?.id] ); - const { h: dh, m: dm } = secondsToTime( - (currentMember?.totalTodayTasks && - currentMember?.totalTodayTasks.length && - currentMember?.totalTodayTasks - .filter((t) => t.id === task?.id) - .reduce((previousValue, currentValue) => previousValue + currentValue.duration, 0)) || - 0 + + const { h: dh, m: dm } = useMemo( + () => + secondsToTime( + (currentMember?.totalTodayTasks && + currentMember?.totalTodayTasks.length && + currentMember?.totalTodayTasks + .filter((t) => t.id === task?.id) + .reduce((previousValue, currentValue) => previousValue + currentValue.duration, 0)) || + 0 + ), + [currentMember?.totalTodayTasks, task?.id] ); return ( diff --git a/apps/web/lib/features/user-profile-tasks.tsx b/apps/web/lib/features/user-profile-tasks.tsx index 027b545fa..f7857f225 100644 --- a/apps/web/lib/features/user-profile-tasks.tsx +++ b/apps/web/lib/features/user-profile-tasks.tsx @@ -4,6 +4,7 @@ import { UserProfilePlans } from 'lib/features'; import { TaskCard } from './task/task-card'; import { I_TaskFilter } from './task/task-filters'; import { useTranslations } from 'next-intl'; +import { useMemo } from 'react'; type Props = { tabFiltered: I_TaskFilter; profile: I_UserProfilePage; @@ -23,10 +24,11 @@ export function UserProfileTask({ profile, tabFiltered }: Props) { /** * When tab is worked, then filter exclude the active task */ - const tasks = tabFiltered.tasksFiltered; + const tasks = useMemo(() => tabFiltered.tasksFiltered, [tabFiltered.tasksFiltered]); - const otherTasks = tasks.filter((t) => - profile.member?.running == true ? t.id !== profile.activeUserTeamTask?.id : t + const otherTasks = useMemo( + () => tasks.filter((t) => (profile.member?.running == true ? t.id !== profile.activeUserTeamTask?.id : t)), + [profile.activeUserTeamTask?.id, profile.member?.running, tasks] ); // const data = otherTasks.length < 10 ? otherTasks : data; From e6fd10301d9301c5ce470a1cb55461abbfb9068f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Aug 2024 15:15:01 +0200 Subject: [PATCH 04/18] fix: remove duplication of today plan when adding a new task --- apps/web/app/hooks/features/useDailyPlan.ts | 28 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index 8aa44d04e..0b1aca3b6 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -102,10 +102,30 @@ export function useDailyPlan() { async (data: ICreateDailyPlan) => { if (user?.tenantId) { const res = await createQueryCall(data, user?.tenantId || ''); - setProfileDailyPlans({ - total: profileDailyPlans.total + 1, - items: [...profileDailyPlans.items, res.data] - }); + //Check if there is a plan for today + const todayPlan = profileDailyPlans.items.find((plan) => + plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]) + ); + if (todayPlan) { + const updatedProfilDailyPlans = profileDailyPlans.items.map((plan) => { + if (plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])) { + return res.data; + } + + return plan; + }); + + setProfileDailyPlans({ + total: updatedProfilDailyPlans.length, + items: updatedProfilDailyPlans + }); + } else { + setProfileDailyPlans({ + total: profileDailyPlans.total + 1, + items: [...profileDailyPlans.items, res.data] + }); + } + setEmployeePlans([...employeePlans, res.data]); getMyDailyPlans(); return res; From 0b7f8120b8599b5360613e3ba70e7b0ba70ca09d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 6 Aug 2024 22:02:47 +0200 Subject: [PATCH 05/18] fix fix spell typo --- apps/web/app/hooks/features/useDailyPlan.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index 0b1aca3b6..ea76d0535 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -107,7 +107,7 @@ export function useDailyPlan() { plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]) ); if (todayPlan) { - const updatedProfilDailyPlans = profileDailyPlans.items.map((plan) => { + const updatedProfileDailyPlans = profileDailyPlans.items.map((plan) => { if (plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])) { return res.data; } @@ -116,8 +116,8 @@ export function useDailyPlan() { }); setProfileDailyPlans({ - total: updatedProfilDailyPlans.length, - items: updatedProfilDailyPlans + total: updatedProfileDailyPlans.length, + items: updatedProfileDailyPlans }); } else { setProfileDailyPlans({ From 449ac1c8284e0d90ae8e19f062000029ff2f589f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Aug 2024 12:07:40 +0200 Subject: [PATCH 06/18] fix: remove duplication of tomorow plan when adding a new task --- apps/web/app/hooks/features/useDailyPlan.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index ea76d0535..67f79d4f4 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -102,13 +102,13 @@ export function useDailyPlan() { async (data: ICreateDailyPlan) => { if (user?.tenantId) { const res = await createQueryCall(data, user?.tenantId || ''); - //Check if there is a plan for today - const todayPlan = profileDailyPlans.items.find((plan) => - plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0]) + //Check if there is an existing plan + const isPlanExist = profileDailyPlans.items.find((plan) => + plan.date?.toString()?.startsWith(new Date(data.date)?.toISOString().split('T')[0]) ); - if (todayPlan) { - const updatedProfileDailyPlans = profileDailyPlans.items.map((plan) => { - if (plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])) { + if (isPlanExist) { + const updatedPlans = profileDailyPlans.items.map((plan) => { + if (plan.date?.toString()?.startsWith(new Date(data.date)?.toISOString().split('T')[0])) { return res.data; } @@ -116,8 +116,8 @@ export function useDailyPlan() { }); setProfileDailyPlans({ - total: updatedProfileDailyPlans.length, - items: updatedProfileDailyPlans + total: updatedPlans.length, + items: updatedPlans }); } else { setProfileDailyPlans({ From ebc86532ebb018e5ef5bb27d5883dd9b6f7262bf Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:54:54 +0200 Subject: [PATCH 07/18] Feat: Integration of FullCalendar Component (#2840) * feat: integration of a calendar component * fix: cspell and some bugs * fix: Add missing dependency in useMemo hook to ensure correct date formatting --- .cspell.json | 4 + apps/web/app/[locale]/calendar/page.tsx | 84 +++++ apps/web/app/[locale]/kanban/page.tsx | 5 +- apps/web/app/api/livekit/route.ts | 17 +- apps/web/lib/features/index.ts | 3 + .../calendar/setup-full-calendar.tsx | 291 ++++++++++++++++++ .../calendar/year-picker-filter.tsx | 59 ++++ apps/web/package.json | 6 + yarn.lock | 39 +++ 9 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/[locale]/calendar/page.tsx create mode 100644 apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx create mode 100644 apps/web/lib/features/integrations/calendar/year-picker-filter.tsx diff --git a/.cspell.json b/.cspell.json index 56963e340..91488e9ca 100644 --- a/.cspell.json +++ b/.cspell.json @@ -21,6 +21,7 @@ "APPSTORE", "arrowleft", "asel", + "alldays", "Authentificate", "authjs", "barcodes", @@ -74,6 +75,7 @@ "Darkmode", "datas", "dataToDisplay", + "daygrid", "dearmor", "deepscan", "Defaul", @@ -117,6 +119,7 @@ "Filder", "filtmembers", "firstname", + "fullcalendar", "flaticon", "fomated", "Formated", @@ -325,6 +328,7 @@ "Transpiles", "tsbuildinfo", "typeof", + "timegrid", "uicolors", "uidotdev", "UIUX", diff --git a/apps/web/app/[locale]/calendar/page.tsx b/apps/web/app/[locale]/calendar/page.tsx new file mode 100644 index 000000000..bb0bd78e5 --- /dev/null +++ b/apps/web/app/[locale]/calendar/page.tsx @@ -0,0 +1,84 @@ +"use client" +import { useOrganizationTeams } from '@app/hooks'; +import { fullWidthState } from '@app/stores/fullWidth'; +import { clsxm } from '@app/utils'; +import HeaderTabs from '@components/pages/main/header-tabs'; +import { PeoplesIcon } from 'assets/svg'; +import { withAuthentication } from 'lib/app/authenticator'; +import { Breadcrumb, Button, Container, Divider } from 'lib/components'; +import { SetupFullCalendar } from 'lib/features' +import { Footer, MainLayout } from 'lib/layout'; +import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation'; +import React, { useMemo } from 'react' +import { useRecoilValue } from 'recoil'; + +const CalendarPage = () => { + const t = useTranslations(); + const fullWidth = useRecoilValue(fullWidthState); + const { activeTeam, isTrackingEnabled } = useOrganizationTeams(); + const params = useParams<{ locale: string }>(); + const currentLocale = params ? params.locale : null; + const breadcrumbPath = useMemo( + () => [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: "CALENDAR", href: `/${currentLocale}/calendar` } + ], + [activeTeam?.name, currentLocale, t] + ); + return ( + <> + +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +

    + CALENDAR +

    +
    + +
    + +
    + {/*
    */} +
    +
    +
    + +
    +
    +
    + +
    +
    + + ) +} + +export default withAuthentication(CalendarPage, { displayName: 'Calender' }); diff --git a/apps/web/app/[locale]/kanban/page.tsx b/apps/web/app/[locale]/kanban/page.tsx index 646b39716..490d1b61f 100644 --- a/apps/web/app/[locale]/kanban/page.tsx +++ b/apps/web/app/[locale]/kanban/page.tsx @@ -166,11 +166,10 @@ const Kanban = () => {
    setActiveTab(tab.value)} - className={`cursor-pointer pt-2.5 px-5 pb-[30px] text-base font-semibold ${ - activeTab === 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]' - }`} + }`} style={{ borderBottomWidth: '3px', borderBottomStyle: 'solid' diff --git a/apps/web/app/api/livekit/route.ts b/apps/web/app/api/livekit/route.ts index bd485b8fd..8e07b29b9 100644 --- a/apps/web/app/api/livekit/route.ts +++ b/apps/web/app/api/livekit/route.ts @@ -32,8 +32,21 @@ export async function GET(req: NextRequest) { } try { - const at = new AccessToken(apiKey, apiSecret, { identity: username }); - at.addGrant({ room, roomJoin: true, canPublish: true, canSubscribe: true, roomRecord: true }); + const at = new AccessToken(apiKey, apiSecret, { identity: username, ttl: '1h' }); + at.addGrant({ + room, + roomJoin: true, + canPublish: true, + canSubscribe: true, + roomRecord: true, + roomCreate: true, + roomAdmin: true, + recorder: true, + roomList: true, + canUpdateOwnMetadata: true, + agent: true, + canPublishData: true, + }); const token = await at.toJwt(); return NextResponse.json({ token: token }); } catch (error) { diff --git a/apps/web/lib/features/index.ts b/apps/web/lib/features/index.ts index 688b0f985..81fec7bd4 100644 --- a/apps/web/lib/features/index.ts +++ b/apps/web/lib/features/index.ts @@ -36,3 +36,6 @@ export * from './user-profile-tasks'; export * from './languages/language-item'; export * from './timezones/timezone-item'; export * from './position/position-item'; + + +export * from './integrations/calendar/setup-full-calendar' diff --git a/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx new file mode 100644 index 000000000..ffa421a35 --- /dev/null +++ b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx @@ -0,0 +1,291 @@ +"use client" +import React, { useState, useRef } from 'react'; +import { LuCalendarPlus } from "react-icons/lu"; +import { IoIosArrowDown, IoIosArrowForward } from "react-icons/io"; +import { IoTimeSharp } from "react-icons/io5"; +import { MdTimer } from "react-icons/md"; +import FullCalendar from '@fullcalendar/react'; +import interactionPlugin from '@fullcalendar/interaction'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import listPlugin from '@fullcalendar/list'; +import { startOfYear, endOfYear, format } from 'date-fns'; +import Image from 'next/image'; +import { Button } from 'lib/components'; +import { SettingFilterIcon } from 'assets/svg'; +import { YearDateFilter } from './year-picker-filter'; +import { cn } from 'lib/utils'; +// import { IOrganizationTeamList } from '@app/interfaces'; + +interface Event { + id?: string; + title: string; + start: string; + times?: string, + color: string; + textColor?: string, + padding?: number, + extendedProps?: { + icon?: JSX.Element; + }, + +} + +export function SetupFullCalendar() { + const [isDialogOpen, setIsDialogOpen] = useState(false); + // const [newEventTitle, setNewEventTitle] = useState(''); + const calendarRef = useRef(null); + const [selectedDate, setSelectedDate] = useState(''); + const [events, setEvents] = useState([ + { + id: '10', + title: 'Auto', + start: '2024-08-01', + color: '#dcfce7', + textColor: "#16a34a", + extendedProps: { + icon: , + }, + + + }, + { + id: '13', + title: 'Manual', + start: '2024-08-01', + color: '#ffedd5', + textColor: "#f97316", + extendedProps: { + icon: , + }, + }, + { + id: '12', + title: 'Auto', + start: '2024-08-01', + color: '#dcfce7', + textColor: "#16a34a", + extendedProps: { + icon: , + }, + + }, + { + id: '11', + title: 'Manual', + start: '2024-08-02', + color: '#ffedd5', + textColor: "#f97316", + extendedProps: { + icon: , + }, + }, + ]); + + const handleDateClick = (info: { dateStr: string }) => { + setSelectedDate(info?.dateStr); + setIsDialogOpen((prev) => !prev); + }; + + const renderEventContent = (eventInfo: any) => { + return ( +
    +
    + {eventInfo.event.extendedProps.icon} + {eventInfo.event.title} +
    + 05:30h +
    + ); + }; + + const dayCellClassNames = (arg: any) => { + const today = format(new Date(), 'yyyy-MM-dd'); + const dateStr = format(arg.date, 'yyyy-MM-dd'); + if (today === dateStr) { + return ['today-cell']; + } + return ['alldays-cell']; + }; + + const handleEventClick = (info: { event: { id: string; startStr: string } }) => { + const isDelete = confirm(`Do you want to delete the event: ${info.event?.id}?`); + if (isDelete) { + const updatedEvents = events.filter(event => + event.id !== info.event.id || event.start !== info.event.startStr + ); + setEvents(updatedEvents); + } + }; + + const handleEventDrop = (info: { event: { id: string; startStr: string } }) => { + const updatedEvents = events.map(event => + event.id === info.event.id ? { ...event, start: info.event.startStr } : event + ); + setEvents(updatedEvents); + }; + + + + + + + return ( +
    +
    +
    +
    + + +
    +
    + +
    +
    + { + const start = startOfYear(currentDate); + const end = endOfYear(currentDate); + return { start, end }; + }, + titleFormat: { year: 'numeric' }, + eventClassNames: (info) => info.event.classNames, + }, + }} + // weekends={false} + dayCellClassNames={dayCellClassNames} + initialView="dayGridMonth" + events={events} + dateClick={handleDateClick} + eventClick={handleEventClick} + eventDrop={handleEventDrop} + eventContent={renderEventContent} + editable={true} + + /> + +
    + {isDialogOpen && ( +
    + +
    + )} +
    + ) +} + + + +export const CardItems = ({ selectedDate }: { selectedDate: Date }) => { + return ( +
    + + {format(selectedDate, 'PPP')} + +
    + + + + + + + +
    +
    + ) +} + + +export const CardItemsMember = ({ imageUrl, name, time }: { imageUrl?: string, name?: string, time?: string }) => { + return ( +
    +
    + +
    + {name} + {time} +
    + +
    +
    + ) +} + +export const CardItemsProjects = ({ logo, title, totalHours }: { logo?: string, title?: string, totalHours?: string }) => { + return ( +
    +
    + logos +
    + {title} + {totalHours} +
    +
    + +
    + ) +} + + +export function TotalHours() { + return ( +
    +
    + + Total Hours 240 +
    +
    + + ) +} diff --git a/apps/web/lib/features/integrations/calendar/year-picker-filter.tsx b/apps/web/lib/features/integrations/calendar/year-picker-filter.tsx new file mode 100644 index 000000000..ef59f2fd6 --- /dev/null +++ b/apps/web/lib/features/integrations/calendar/year-picker-filter.tsx @@ -0,0 +1,59 @@ +"use client" +import * as React from "react" +import { CalendarDaysIcon as CalendarIcon } from "lucide-react" +import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md"; +import FullCalendar from "@fullcalendar/react"; +import moment from "moment"; +interface IYearDateFilter { + calendarRef: React.MutableRefObject +} +export function YearDateFilter({ calendarRef }: IYearDateFilter) { + const current = calendarRef.current; + const [currentDate, setCurrentDate] = React.useState(new Date()); + + + const updateCurrentDate = () => { + if (calendarRef.current) { + const calendarApi = calendarRef.current.getApi(); + setCurrentDate(calendarApi.getDate()); + + } + }; + + function goNext() { + if (current) { + const calendarApi = current.getApi() + calendarApi.next() + updateCurrentDate(); + } + } + function goPrev() { + if (current) { + const calendarApi = current.getApi() + calendarApi.prev(); + updateCurrentDate(); + } + } + + React.useEffect(() => { + updateCurrentDate(); + }, [updateCurrentDate]); // deepscan-disable-line + + return ( +
    +
    + + {moment(currentDate).format('MMM')}{" "}{currentDate.getFullYear()} +
    +
    + + +
    +
    + + ) +} diff --git a/apps/web/package.json b/apps/web/package.json index 45189cfb7..3607108c5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,12 @@ "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "^0.15.3", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/list": "^6.1.15", + "@fullcalendar/react": "^6.1.15", + "@fullcalendar/timegrid": "^6.1.15", "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.12", "@jitsi/react-sdk": "^1.3.0", diff --git a/yarn.lock b/yarn.lock index 5c2486c87..a0981a975 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2616,6 +2616,40 @@ dependencies: tslib "^2.4.0" +"@fullcalendar/core@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.15.tgz#6c3f5259fc4589870228853072131219bb533f6e" + integrity sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q== + dependencies: + preact "~10.12.1" + +"@fullcalendar/daygrid@^6.1.15", "@fullcalendar/daygrid@~6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz#91208b0955ba805ddad285a53ee6f53855146963" + integrity sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA== + +"@fullcalendar/interaction@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.15.tgz#1c685d5c269388d4877b75ab2185e97d7c386cc7" + integrity sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ== + +"@fullcalendar/list@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/list/-/list-6.1.15.tgz#d9b7ff0a50d7efa0d31234ed6caea06db6090c29" + integrity sha512-U1bce04tYDwkFnuVImJSy2XalYIIQr6YusOWRPM/5ivHcJh67Gm8CIMSWpi3KdRSNKFkqBxLPkfZGBMaOcJYug== + +"@fullcalendar/react@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-6.1.15.tgz#3198b4a64e256afd37c9760c8741a9af89ade894" + integrity sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w== + +"@fullcalendar/timegrid@^6.1.15": + version "6.1.15" + resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz#c4630b7c03c813065154c6e3981f8d51d9d692e5" + integrity sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw== + dependencies: + "@fullcalendar/daygrid" "~6.1.15" + "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -21693,6 +21727,11 @@ preact@10.11.3: resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19" integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg== +preact@~10.12.1: + version "10.12.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21" + integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" From 46be66dc03a7591907c71aa5ceb09840e1ebf3ae Mon Sep 17 00:00:00 2001 From: Paradoxe Ng Date: Wed, 7 Aug 2024 13:32:02 +0200 Subject: [PATCH 08/18] Add PostHog analytics integration (#2844) * Add PostHog analytics integration * chore: update .cspell.json * build(deps): update yarn.lock * chore: update .cspell.json --- .cspell.json | 7 ++- apps/web/.env | 4 ++ .../integration/posthog/page-view.tsx | 29 ++++++++++ .../[locale]/integration/posthog/provider.tsx | 25 +++++++++ apps/web/app/[locale]/layout.tsx | 53 +++++++++++-------- apps/web/app/constants.ts | 4 ++ apps/web/package.json | 1 + yarn.lock | 24 +++++++++ 8 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 apps/web/app/[locale]/integration/posthog/page-view.tsx create mode 100644 apps/web/app/[locale]/integration/posthog/provider.tsx diff --git a/.cspell.json b/.cspell.json index 91488e9ca..b662c7478 100644 --- a/.cspell.json +++ b/.cspell.json @@ -360,7 +360,12 @@ "xlcard", "xlight", "yellowbox", - "vhidden" + "vhidden", + "POSTHOG", + "posthog", + "pageviews", + "pageleave", + "pageview" ], "useGitignore": true, "ignorePaths": [ diff --git a/apps/web/.env b/apps/web/.env index 1702efabc..184d39de3 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -117,3 +117,7 @@ NEXT_PUBLIC_JITSU_BROWSER_WRITE_KEY= # Chatwoot NEXT_PUBLIC_CHATWOOT_API_KEY= + +# PostHog +NEXT_PUBLIC_POSTHOG_KEY= +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com diff --git a/apps/web/app/[locale]/integration/posthog/page-view.tsx b/apps/web/app/[locale]/integration/posthog/page-view.tsx new file mode 100644 index 000000000..9b13d050d --- /dev/null +++ b/apps/web/app/[locale]/integration/posthog/page-view.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { usePathname, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; +import { usePostHog } from 'posthog-js/react'; +import { POSTHOG_HOST, POSTHOG_KEY } from '@app/constants'; + +export default function PostHogPageView(): null { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const posthog = usePostHog(); + + useEffect(() => { + if (!POSTHOG_KEY.value || !POSTHOG_HOST.value) return; + + // Track pageviews + if (pathname && posthog) { + let url = window.origin + pathname; + if (searchParams.toString()) { + url = url + `?${searchParams.toString()}`; + } + posthog.capture('$pageview', { + $current_url: url + }); + } + }, [pathname, searchParams, posthog]); + + return null; +} diff --git a/apps/web/app/[locale]/integration/posthog/provider.tsx b/apps/web/app/[locale]/integration/posthog/provider.tsx new file mode 100644 index 000000000..dff49c51c --- /dev/null +++ b/apps/web/app/[locale]/integration/posthog/provider.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { POSTHOG_HOST, POSTHOG_KEY } from '@app/constants'; +import posthog from 'posthog-js'; +import { PostHogProvider } from 'posthog-js/react'; + +const key = POSTHOG_KEY.value; +const host = POSTHOG_HOST.value; + +if (typeof window !== 'undefined' && key && host) { + posthog.init(key, { + api_host: host, + person_profiles: 'identified_only', + capture_pageview: false, + capture_pageleave: true + }); +} + +export function PHProvider({ children }: { children: React.ReactNode }) { + if (!key || !host) { + return <>{children}; + } + + return {children}; +} diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index 5fe73e277..bca5fff18 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -32,6 +32,8 @@ interface Props { import { Poppins } from 'next/font/google'; import GlobalSkeleton from '@components/ui/global-skeleton'; import NextAuthSessionProvider from 'lib/layout/next-auth-provider'; +import dynamic from 'next/dynamic'; +import { PHProvider } from './integration/posthog/provider'; const poppins = Poppins({ subsets: ['latin'], @@ -39,6 +41,11 @@ const poppins = Poppins({ variable: '--font-poppins', display: 'swap' }); + +const PostHogPageView = dynamic(() => import('./integration/posthog/page-view'), { + ssr: false +}); + // export function generateStaticParams() { // return locales.map((locale: any) => ({ locale })); // } @@ -124,27 +131,31 @@ const LocaleLayout = ({ children, params: { locale }, pageProps }: Props) => { )} */} - - - - - {loading && !pathname?.startsWith('/auth') ? ( - - ) : ( - <> - - {children} - - )} - - - - + + + + + + + + {loading && !pathname?.startsWith('/auth') ? ( + + ) : ( + <> + + {children} + + )} + + + + + ); diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index 501d5d9a6..83ac77069 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -121,6 +121,10 @@ export const BOARD_FIREBASE_CONFIG = getNextPublicEnv( process.env.NEXT_PUBLIC_BOARD_FIREBASE_CONFIG ); +export const POSTHOG_KEY = getNextPublicEnv('NEXT_PUBLIC_POSTHOG_KEY', process.env.NEXT_PUBLIC_POSTHOG_KEY); + +export const POSTHOG_HOST = getNextPublicEnv('NEXT_PUBLIC_POSTHOG_HOST', process.env.NEXT_PUBLIC_POSTHOG_HOST); + // Jitsu export const jitsuConfiguration: () => JitsuOptions = () => ({ host: getNextPublicEnv('NEXT_PUBLIC_JITSU_BROWSER_URL', process.env.NEXT_PUBLIC_JITSU_BROWSER_URL).value, diff --git a/apps/web/package.json b/apps/web/package.json index 3607108c5..c1cad86c5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -102,6 +102,7 @@ "pako": "^2.1.0", "polished": "^4.2.2", "postcss": "^8.4.19", + "posthog-js": "^1.154.5", "qs": "^6.11.2", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", diff --git a/yarn.lock b/yarn.lock index a0981a975..183463e4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14546,6 +14546,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fflate@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" + integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== + figures@3.2.0, figures@^3.0.0, figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -21715,6 +21720,15 @@ postgres-range@^1.1.1: resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76" integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g== +posthog-js@^1.154.5: + version "1.154.5" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.154.5.tgz#1737ce0b31611ae291c3c301f356ec69b835c354" + integrity sha512-YYhWckDIRObfCrQpiLq+fdcDTIbQp8ebiKi0ueGohMRgugIG9LJVSpBgCeCHZm2C7sOxDUNcAr3T5VBDUSQoOg== + dependencies: + fflate "^0.4.8" + preact "^10.19.3" + web-vitals "^4.0.1" + preact-render-to-string@5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz#23d17376182af720b1060d5a4099843c7fe92fe4" @@ -21727,6 +21741,11 @@ preact@10.11.3: resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19" integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg== +preact@^10.19.3: + version "10.23.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.23.1.tgz#d400107289bc979881c5212cb5f5cd22cd1dc38c" + integrity sha512-O5UdRsNh4vdZaTieWe3XOgSpdMAmkIYBCT3VhQDlKrzyCm8lUYsk0fmVEvoQQifoOjFRTaHZO69ylrzTW2BH+A== + preact@~10.12.1: version "10.12.1" resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21" @@ -25908,6 +25927,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-vitals@^4.0.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.2.tgz#e883245180b95e175eb75a5ca8903b1a11597d7a" + integrity sha512-nYfoOqb4EmElljyXU2qdeE76KsvoHdftQKY4DzA9Aw8DervCg2bG634pHLrJ/d6+B4mE3nWTSJv8Mo7B2mbZkw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 945748a0bcd57a3aabd6c966dbb28ed256f636e7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Aug 2024 14:06:55 +0200 Subject: [PATCH 09/18] fix: show number of tasks instead of plans for different tabs --- .../app/hooks/features/useAuthTeamTasks.ts | 23 ++++-- .../task/daily-plan/outstanding-all.tsx | 2 +- .../task/daily-plan/task-estimated-count.tsx | 13 ++++ apps/web/lib/features/task/task-filters.tsx | 2 +- apps/web/lib/features/user-profile-plans.tsx | 72 ++++++++++--------- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/apps/web/app/hooks/features/useAuthTeamTasks.ts b/apps/web/app/hooks/features/useAuthTeamTasks.ts index ec57b486b..79650d83f 100644 --- a/apps/web/app/hooks/features/useAuthTeamTasks.ts +++ b/apps/web/app/hooks/features/useAuthTeamTasks.ts @@ -1,12 +1,14 @@ import { IUser } from '@app/interfaces'; -import { profileDailyPlanListState, tasksByTeamState } from '@app/stores'; +import { tasksByTeamState } from '@app/stores'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { useOrganizationTeams } from './useOrganizationTeams'; +import { useDailyPlan } from './useDailyPlan'; +import { estimatedTotalTime, getTotalTasks } from 'lib/features/task/daily-plan'; export function useAuthTeamTasks(user: IUser | undefined) { const tasks = useRecoilValue(tasksByTeamState); - const plans = useRecoilValue(profileDailyPlanListState); + const { outstandingPlans, todayPlan, futurePlans } = useDailyPlan(); const { activeTeam } = useOrganizationTeams(); const currentMember = activeTeam?.members?.find((member) => member.employee?.userId === user?.id); @@ -25,10 +27,17 @@ export function useAuthTeamTasks(user: IUser | undefined) { }); }, [tasks, user]); - const dailyplan = useMemo(() => { - if (!user) return []; - return plans.items; - }, [plans, user]); + const planned = useMemo(() => { + const outStandingTasksCount = estimatedTotalTime( + outstandingPlans.map((plan) => plan.tasks?.map((task) => task)) + ).totalTasks; + + const todayTasksCOunt = getTotalTasks(todayPlan); + + const futureTasksCount = getTotalTasks(futurePlans); + + return outStandingTasksCount + futureTasksCount + todayTasksCOunt; + }, [futurePlans, outstandingPlans, todayPlan]); const totalTodayTasks = useMemo( () => @@ -48,6 +57,6 @@ export function useAuthTeamTasks(user: IUser | undefined) { assignedTasks, unassignedTasks, workedTasks, - dailyplan + planned }; } diff --git a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx index d6722ad5f..f576333d4 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -77,7 +77,7 @@ export function OutstandingAll({ profile }: OutstandingAll) { taskBadgeClassName={`rounded-sm`} taskTitleClassName="mt-[0.0625rem]" planMode="Outstanding" - className='shadow-[0px_0px_15px_0px_#e2e8f0]' + className="shadow-[0px_0px_15px_0px_#e2e8f0]" />
    )} diff --git a/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx b/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx index 811f7cbf7..fd549e7b2 100644 --- a/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx +++ b/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx @@ -42,3 +42,16 @@ export function estimatedTotalTime(data: any) { return { timesEstimated, totalTasks }; } + +export const getTotalTasks = (plan: IDailyPlan[]) => { + if (!plan) { + return 0; + } + const tasksPerPlan = plan.map((plan) => plan.tasks?.length); + + if (tasksPerPlan.length <= 0) { + return 0; + } + + return tasksPerPlan.reduce((a, b) => (a ?? 0) + (b ?? 0)) ?? 0; +}; diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index ff6c8f95d..ff05d656d 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -108,7 +108,7 @@ export function useTaskFilter(profile: I_UserProfilePage) { tab: 'dailyplan', name: 'Daily Plan', description: 'This tab shows all yours tasks planned', - count: profile.tasksGrouped.dailyplan?.length + count: profile.tasksGrouped.planned }); tabs.unshift({ tab: 'worked', diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index 0c78c5b42..c2aaaf743 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -13,7 +13,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { formatDayPlanDate, formatIntegerToHour } from '@app/helpers'; import { EditPenBoxIcon, CheckCircleTickIcon as TickSaveIcon } from 'assets/svg'; import { ReaderIcon, ReloadIcon, StarIcon } from '@radix-ui/react-icons'; -import { OutstandingAll, PastTasks, Outstanding, OutstandingFilterDate } from './task/daily-plan'; +import { + OutstandingAll, + PastTasks, + Outstanding, + OutstandingFilterDate, + estimatedTotalTime, + getTotalTasks +} from './task/daily-plan'; import { FutureTasks } from './task/daily-plan/future-tasks'; import { Button } from '@components/ui/button'; import { IoCalendarOutline } from 'react-icons/io5'; @@ -45,12 +52,9 @@ export function UserProfilePlans() { const [currentTab, setCurrentTab] = useState(defaultTab || 'Today Tasks'); const [currentOutstanding, setCurrentOutstanding] = useState(defaultOutstanding || 'ALL'); - - const [currentDataDailyPlan, setCurrentDataDailyPlan] = useRecoilState(dataDailyPlanState) + const [currentDataDailyPlan, setCurrentDataDailyPlan] = useRecoilState(dataDailyPlanState); const { setDate, date } = useDateRange(currentTab); - - const screenOutstanding = { ALL: , DATE: @@ -66,24 +70,21 @@ export function UserProfilePlans() { const [filterPastPlanData, setFilteredPastPlanData] = useState(pastPlans); const [filterAllPlanData, setFilterAllPlanData] = useState(sortedPlans); - useEffect(() => { window.localStorage.setItem('daily-plan-tab', currentTab); if (!currentDataDailyPlan) return; if (currentTab === 'All Tasks') { - setCurrentDataDailyPlan(sortedPlans) - setFilterAllPlanData(filterDailyPlan(date as any, sortedPlans)) + setCurrentDataDailyPlan(sortedPlans); + setFilterAllPlanData(filterDailyPlan(date as any, sortedPlans)); } else if (currentTab === 'Past Tasks') { - setCurrentDataDailyPlan(pastPlans) - setFilteredPastPlanData(filterDailyPlan(date as any, pastPlans)) + setCurrentDataDailyPlan(pastPlans); + setFilteredPastPlanData(filterDailyPlan(date as any, pastPlans)); } else if (currentTab === 'Future Tasks') { - setCurrentDataDailyPlan(futurePlans) - setFilterFuturePlanData(filterDailyPlan(date as any, futurePlans)) + setCurrentDataDailyPlan(futurePlans); + setFilterFuturePlanData(filterDailyPlan(date as any, futurePlans)); } - }, [currentTab, setCurrentDataDailyPlan, setDate, date]); - useEffect(() => { window.localStorage.setItem('outstanding', currentOutstanding); }, [currentOutstanding]); @@ -105,8 +106,8 @@ export function UserProfilePlans() { currentTab == filter && 'text-blue-600 dark:text-white font-medium' )} onClick={() => { - setDate(undefined) - setCurrentTab(filter as FilterTabs) + setDate(undefined); + setCurrentTab(filter as FilterTabs); }} > {filter} @@ -116,12 +117,16 @@ export function UserProfilePlans() { currentTab == filter && 'dark:bg-gray-600' )} > - {filter === 'Today Tasks' && todayPlan.length} - {filter === 'Future Tasks' && filterFuturePlanData?.length} - {filter === 'Past Tasks' && filterPastPlanData?.length} - {filter === 'All Tasks' && filterAllPlanData?.length} - {filter === 'Outstanding' && outstandingPlans.length} - + {filter === 'Today Tasks' && getTotalTasks(todayPlan)} + {filter === 'Future Tasks' && getTotalTasks(filterFuturePlanData)} + {filter === 'Past Tasks' && getTotalTasks(filterPastPlanData)} + {filter === 'All Tasks' && getTotalTasks(filterAllPlanData)} + {filter === 'Outstanding' && + estimatedTotalTime( + outstandingPlans.map((plan) => + plan.tasks?.map((task) => task) + ) + ).totalTasks}
    @@ -184,8 +189,9 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const [plans, setPlans] = useState(filteredPlans); useEffect(() => { - setPlans(filterDailyPlan(date as any, filteredPlans)) - }, [date, setDate]) + setPlans(filterDailyPlan(date as any, filteredPlans)); + }, [date, setDate]); + return (
    {Array.isArray(plans) && plans?.length > 0 ? ( @@ -261,7 +267,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current : undefined } plan={plan} - className='shadow-[0px_0px_15px_0px_#e2e8f0]' + className="shadow-[0px_0px_15px_0px_#e2e8f0]" />
    )} @@ -353,17 +359,19 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil const { updateDailyPlan, updateDailyPlanLoading } = useDailyPlan(); // Get all tasks's estimations time // Helper function to sum times - const sumTimes = (tasks: ITeamTask[], key: any) => tasks?.map((task: any) => - task[key]).filter((time): time is number => typeof time === 'number') - .reduce((acc, cur) => acc + cur, 0) ?? 0; + const sumTimes = (tasks: ITeamTask[], key: any) => + tasks + ?.map((task: any) => task[key]) + .filter((time): time is number => typeof time === 'number') + .reduce((acc, cur) => acc + cur, 0) ?? 0; // Get all tasks' estimation and worked times const estimatedTime = sumTimes(plan.tasks!, 'estimate'); const totalWorkTime = sumTimes(plan.tasks!, 'totalWorkedTime'); // Get completed and ready tasks from a plan - const completedTasks = plan.tasks?.filter(task => task.status === 'completed').length ?? 0; - const readyTasks = plan.tasks?.filter(task => task.status === 'ready').length ?? 0; + const completedTasks = plan.tasks?.filter((task) => task.status === 'completed').length ?? 0; + const readyTasks = plan.tasks?.filter((task) => task.status === 'ready').length ?? 0; // Total tasks for the plan const totalTasks = plan.tasks?.length ?? 0; @@ -371,7 +379,6 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil // Completion percent const completionPercent = totalTasks > 0 ? ((completedTasks * 100) / totalTasks).toFixed(0) : '0.0'; - return (
    } /> + component={} + />
    ); } From 3067cf0f6276359eb8b72420cdc42fab817740ce Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:15:47 +0200 Subject: [PATCH 10/18] [Refactored]: Calendar code into a new CalendarComponent file. (#2850) * feat: integration of a calendar component * fix: cspell and some bugs * fix: Add missing dependency in useMemo hook to ensure correct date formatting * feat: FullCalendar with custom events and date picker functionality * fix: cspell * fix: cspell --- apps/web/app/[locale]/calendar/page.tsx | 16 +- .../calendar/calendar-component.tsx | 95 ++++++++++ .../calendar/setup-full-calendar.tsx | 177 ++++++------------ apps/web/lib/utils.ts | 11 ++ 4 files changed, 176 insertions(+), 123 deletions(-) create mode 100644 apps/web/lib/features/integrations/calendar/calendar-component.tsx diff --git a/apps/web/app/[locale]/calendar/page.tsx b/apps/web/app/[locale]/calendar/page.tsx index bb0bd78e5..e805038f3 100644 --- a/apps/web/app/[locale]/calendar/page.tsx +++ b/apps/web/app/[locale]/calendar/page.tsx @@ -12,6 +12,8 @@ import { useTranslations } from 'next-intl'; import { useParams } from 'next/navigation'; import React, { useMemo } from 'react' import { useRecoilValue } from 'recoil'; +import { LuCalendarDays } from "react-icons/lu"; + const CalendarPage = () => { const t = useTranslations(); @@ -55,16 +57,22 @@ const CalendarPage = () => {

    CALENDAR

    -
    +
    + +
    -
    - {/*
    */}
    diff --git a/apps/web/lib/features/integrations/calendar/calendar-component.tsx b/apps/web/lib/features/integrations/calendar/calendar-component.tsx new file mode 100644 index 000000000..ff7c603db --- /dev/null +++ b/apps/web/lib/features/integrations/calendar/calendar-component.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import listPlugin from '@fullcalendar/list'; +import interactionPlugin, { DateClickArg } from '@fullcalendar/interaction'; +import { startOfYear, endOfYear } from 'date-fns'; +import { ClassNamesGenerator, DayCellContentArg, EventContentArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; +import { ScrollArea } from '@components/ui/scroll-bar'; + +type CalendarComponentProps = { + events: EventSourceInput; + handleDateClick: (arg: DateClickArg) => void; + handleEventDrop: (arg: EventDropArg) => void; + renderEventContent: (arg: EventContentArg) => React.ReactNode; + dayCellClassNames: ClassNamesGenerator | undefined; + calendarRef: React.MutableRefObject; +}; + +const CalendarComponent: React.FC = ({ + events, + handleDateClick, + handleEventDrop, + renderEventContent, + dayCellClassNames, + calendarRef, +}) => { + return ( + + { + const start = startOfYear(currentDate); + const end = endOfYear(currentDate); + return { start, end }; + }, + titleFormat: { year: 'numeric' }, + eventClassNames: (info) => info.event.classNames, + }, + }} + dayCellClassNames={dayCellClassNames} + initialView='dayGridMonth' + events={events} + dateClick={handleDateClick} + eventDrop={handleEventDrop} + eventContent={renderEventContent} + editable + /> + + + ); +}; + +export default CalendarComponent; diff --git a/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx index ffa421a35..7eb58cdae 100644 --- a/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx +++ b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx @@ -3,18 +3,16 @@ import React, { useState, useRef } from 'react'; import { LuCalendarPlus } from "react-icons/lu"; import { IoIosArrowDown, IoIosArrowForward } from "react-icons/io"; import { IoTimeSharp } from "react-icons/io5"; -import { MdTimer } from "react-icons/md"; import FullCalendar from '@fullcalendar/react'; -import interactionPlugin from '@fullcalendar/interaction'; -import dayGridPlugin from '@fullcalendar/daygrid'; -import timeGridPlugin from '@fullcalendar/timegrid'; -import listPlugin from '@fullcalendar/list'; -import { startOfYear, endOfYear, format } from 'date-fns'; +import { format } from 'date-fns'; import Image from 'next/image'; import { Button } from 'lib/components'; import { SettingFilterIcon } from 'assets/svg'; import { YearDateFilter } from './year-picker-filter'; -import { cn } from 'lib/utils'; +import CalendarComponent from './calendar-component'; +import { PiTimerBold } from "react-icons/pi"; +import { formatWithSuffix } from 'lib/utils'; + // import { IOrganizationTeamList } from '@app/interfaces'; interface Event { @@ -44,7 +42,7 @@ export function SetupFullCalendar() { color: '#dcfce7', textColor: "#16a34a", extendedProps: { - icon: , + icon: , }, @@ -66,7 +64,7 @@ export function SetupFullCalendar() { color: '#dcfce7', textColor: "#16a34a", extendedProps: { - icon: , + icon: , }, }, @@ -89,7 +87,7 @@ export function SetupFullCalendar() { const renderEventContent = (eventInfo: any) => { return ( -
    +
    {eventInfo.event.extendedProps.icon} {eventInfo.event.title} @@ -108,15 +106,15 @@ export function SetupFullCalendar() { return ['alldays-cell']; }; - const handleEventClick = (info: { event: { id: string; startStr: string } }) => { - const isDelete = confirm(`Do you want to delete the event: ${info.event?.id}?`); - if (isDelete) { - const updatedEvents = events.filter(event => - event.id !== info.event.id || event.start !== info.event.startStr - ); - setEvents(updatedEvents); - } - }; + // const handleEventClick = (info: { event: { id: string; startStr: string } }) => { + // const isDelete = confirm(`Do you want to delete the event: ${info.event?.id}?`); + // if (isDelete) { + // const updatedEvents = events.filter(event => + // event.id !== info.event.id || event.start !== info.event.startStr + // ); + // setEvents(updatedEvents); + // } + // }; const handleEventDrop = (info: { event: { id: string; startStr: string } }) => { const updatedEvents = events.map(event => @@ -125,16 +123,11 @@ export function SetupFullCalendar() { setEvents(updatedEvents); }; - - - - - return ( -
    -
    -
    -
    +
    +
    +
    +
    @@ -147,112 +140,62 @@ export function SetupFullCalendar() {
    - { - const start = startOfYear(currentDate); - const end = endOfYear(currentDate); - return { start, end }; - }, - titleFormat: { year: 'numeric' }, - eventClassNames: (info) => info.event.classNames, - }, - }} - // weekends={false} - dayCellClassNames={dayCellClassNames} - initialView="dayGridMonth" - events={events} - dateClick={handleDateClick} - eventClick={handleEventClick} - eventDrop={handleEventDrop} - eventContent={renderEventContent} - editable={true} +
    + + {isDialogOpen && ( +
    + +
    + )} +
    - /> -
    - {isDialogOpen && ( -
    - -
    - )} + + +
    ) } - export const CardItems = ({ selectedDate }: { selectedDate: Date }) => { return ( -
    - - {format(selectedDate, 'PPP')} - -
    - - - - - - - +
    +
    + + {formatWithSuffix(new Date(selectedDate))} + +
    + + + + + +
    ) } - export const CardItemsMember = ({ imageUrl, name, time }: { imageUrl?: string, name?: string, time?: string }) => { return (
    -
    - +
    +
    - {name} + {name} +
    +
    {time} +
    -
    ) @@ -276,11 +219,7 @@ export const CardItemsProjects = ({ logo, title, totalHours }: { logo?: string, export function TotalHours() { return ( -
    +
    Total Hours 240 diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts index bd8399a5b..b35caa3b9 100644 --- a/apps/web/lib/utils.ts +++ b/apps/web/lib/utils.ts @@ -1,4 +1,5 @@ import { type ClassValue, clsx } from 'clsx'; +import moment from 'moment'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { @@ -49,3 +50,13 @@ export const shortenLink = (value: any): string => { const end = value.substring(value.length - 10); return `${start}...${end}`; }; + + +export function formatWithSuffix(date: Date) { + const day = moment(date).date(); + const suffix = + day === 1 || day === 21 || day === 31 ? 'st' : + day === 2 || day === 22 ? 'nd' : + day === 3 || day === 23 ? 'rd' : 'th'; + return moment(date).format(`D[${suffix}] MMMM YYYY`); +} From 63c199d65fdfc428cd93fed355af01924f81d2c3 Mon Sep 17 00:00:00 2001 From: Paradoxe Ng Date: Wed, 7 Aug 2024 16:31:37 +0200 Subject: [PATCH 11/18] Change timesheet stats requests to POST method (#2853) * Add PostHog analytics integration * chore: update .cspell.json * build(deps): update yarn.lock * chore: update .cspell.json * Change timesheet stats requests to POST method * Add vertical centering to task avatars --- .../app/services/client/requests/timesheet.ts | 3 ++- apps/web/app/services/client/api/tasks.ts | 24 +++++++++---------- .../app/services/server/requests/timesheet.ts | 2 +- .../details-section/blocks/task-plans.tsx | 4 +++- apps/web/lib/features/task/task-item.tsx | 7 ++++-- apps/web/lib/settings/JitsuRoot.tsx | 6 ++--- 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/apps/mobile/app/services/client/requests/timesheet.ts b/apps/mobile/app/services/client/requests/timesheet.ts index f629a2e28..e2ce05321 100644 --- a/apps/mobile/app/services/client/requests/timesheet.ts +++ b/apps/mobile/app/services/client/requests/timesheet.ts @@ -11,12 +11,13 @@ type TTasksTimesheetStatisticsParams = { 'taskIds[0]'?: string; unitOfTime?: 'day'; }; + export function tasksTimesheetStatisticsRequest(params: TTasksTimesheetStatisticsParams, bearer_token: string) { const queries = new URLSearchParams(params); return serverFetch({ path: `/timesheet/statistics/tasks?${queries.toString()}`, - method: 'GET', + method: 'POST', bearer_token, tenantId: params.tenantId }); diff --git a/apps/web/app/services/client/api/tasks.ts b/apps/web/app/services/client/api/tasks.ts index db681e9c0..4635b1f43 100644 --- a/apps/web/app/services/client/api/tasks.ts +++ b/apps/web/app/services/client/api/tasks.ts @@ -150,9 +150,9 @@ export async function tasksTimesheetStatisticsAPI( if (GAUZY_API_BASE_SERVER_URL.value) { const employeesParams = employeeId ? [employeeId].reduce((acc: any, v, i) => { - acc[`employeeIds[${i}]`] = v; - return acc; - }) + acc[`employeeIds[${i}]`] = v; + return acc; + }) : {}; const commonParams = { tenantId, @@ -165,7 +165,7 @@ export async function tasksTimesheetStatisticsAPI( defaultRange: 'false' }); - const globalData = await get(`/timesheet/statistics/tasks?${globalQueries}`, { + const globalData = await post(`/timesheet/statistics/tasks?${globalQueries}`, { tenantId }); @@ -174,7 +174,7 @@ export async function tasksTimesheetStatisticsAPI( defaultRange: 'true', unitOfTime: 'day' }); - const todayData = await get(`/timesheet/statistics/tasks?${todayQueries}`, { + const todayData = await post(`/timesheet/statistics/tasks?${todayQueries}`, { tenantId }); @@ -189,8 +189,6 @@ export async function tasksTimesheetStatisticsAPI( `/timer/timesheet/statistics-tasks${employeeId ? '?employeeId=' + employeeId : ''}` ); } - - } export async function activeTaskTimesheetStatisticsAPI( @@ -202,9 +200,9 @@ export async function activeTaskTimesheetStatisticsAPI( if (GAUZY_API_BASE_SERVER_URL.value) { const employeesParams = employeeId ? [employeeId].reduce((acc: any, v, i) => { - acc[`employeeIds[${i}]`] = v; - return acc; - }) + acc[`employeeIds[${i}]`] = v; + return acc; + }) : {}; const commonParams = { tenantId, @@ -216,12 +214,12 @@ export async function activeTaskTimesheetStatisticsAPI( ...commonParams, defaultRange: 'false' }); - const globalData = await get(`/timesheet/statistics/tasks?${globalQueries}`, { + const globalData = await post(`/timesheet/statistics/tasks?${globalQueries}`, { tenantId }); const todayQueries = qs.stringify({ ...commonParams, defaultRange: 'true', unitOfTime: 'day' }); - const todayData = await get(`/timesheet/statistics/tasks?${todayQueries}`, { + const todayData = await post(`/timesheet/statistics/tasks?${todayQueries}`, { tenantId }); @@ -263,7 +261,7 @@ export function allTaskTimesheetStatisticsAPI() { ) }); - return get(`/timesheet/statistics/tasks?${queries.toString()}`, { tenantId }); + return post(`/timesheet/statistics/tasks?${queries.toString()}`, { tenantId }); } return api.get(`/timer/timesheet/all-statistics-tasks`); diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index 4a48b9bcd..dcb1f296c 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -28,7 +28,7 @@ export function tasksTimesheetStatisticsRequest(params: TTasksTimesheetStatistic return serverFetch({ path: `/timesheet/statistics/tasks?${queries}`, - method: 'GET', + method: 'POST', bearer_token, tenantId: params.tenantId }); diff --git a/apps/web/components/pages/task/details-section/blocks/task-plans.tsx b/apps/web/components/pages/task/details-section/blocks/task-plans.tsx index ff7cbe81d..858316596 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-plans.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-plans.tsx @@ -10,7 +10,9 @@ export function TaskPlans() { const { taskPlanList, getPlansByTask } = useDailyPlan(); useEffect(() => { - getPlansByTask(task?.id); + if (task?.id) { + getPlansByTask(task?.id); + } }, [getPlansByTask, task?.id]); const groupedByEmployee: { [key: string]: any[] } = {}; diff --git a/apps/web/lib/features/task/task-item.tsx b/apps/web/lib/features/task/task-item.tsx index 07af1a041..850e2234b 100644 --- a/apps/web/lib/features/task/task-item.tsx +++ b/apps/web/lib/features/task/task-item.tsx @@ -132,14 +132,17 @@ export function TaskAvatars({ task, limit = 2 }: { task: PartialITeamTask; limit if (!members.length) { return ( -
    +
    ); } return ( -
    e.stopPropagation()}> +
    e.stopPropagation()} + > {members.slice(0, limit).map((member, i) => { const user = member.user; const userName = `${user?.firstName || ''} ${user?.lastName || ''}`; diff --git a/apps/web/lib/settings/JitsuRoot.tsx b/apps/web/lib/settings/JitsuRoot.tsx index 006c8b026..4d6782a44 100644 --- a/apps/web/lib/settings/JitsuRoot.tsx +++ b/apps/web/lib/settings/JitsuRoot.tsx @@ -24,7 +24,7 @@ export function JitsuRoot({ pageProps, children }: MyAppProps) { cookieDomain: process.env.NEXT_PUBLIC_JITSU_COOKIE_DOMAIN, echoEvents: false }; - const isJitsuEnvs: boolean = jitsuConf.host !== '' && jitsuConf.writeKey !== ''; + const isJitsuEnvs: boolean = !!jitsuConf.host && !!jitsuConf.writeKey; console.log(`Jitsu Enabled: ${isJitsuEnvs}`); console.log(`Jitsu Configuration: ${JSON.stringify(jitsuConf)}`); @@ -38,10 +38,10 @@ export function JitsuRoot({ pageProps, children }: MyAppProps) { debug: jitsuConf.debug, cookieDomain: jitsuConf.cookieDomain ?? undefined, echoEvents: jitsuConf.echoEvents - } + } : { disabled: true - } + } } > From 4d86260ee1707729bcceba2cc040f0fd6fff8ceb Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Wed, 7 Aug 2024 19:42:09 +0200 Subject: [PATCH 12/18] feat: Display Remove from all plans option only if task is planned --- apps/web/lib/features/task/task-card.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 937afae9e..3b163c400 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -531,6 +531,15 @@ function TaskCardMenu({ [task.id, todayPlan] ); + const allPlans = [...todayPlan, ...futurePlans]; + const isTaskPlannedMultipleTimes = allPlans.reduce((count, plan) => { + if (plan?.tasks) { + const taskCount = plan.tasks.filter(_task => _task.id === task.id).length; + return count + taskCount; + } + return count; + }, 0) > 1; + const taskPlannedTomorrow = useMemo( () => futurePlans @@ -647,12 +656,13 @@ function TaskCardMenu({ plan={plan} />
    -
    - -
    + {isTaskPlannedMultipleTimes && ( +
    + +
    )}
    ) : ( <> From 613991c8b0c9f3a56975aeef3349598f358281ec Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Aug 2024 21:00:33 +0200 Subject: [PATCH 13/18] fix: user should not edit past plans --- .../features/task/daily-plan/past-tasks.tsx | 1 + apps/web/lib/features/task/task-card.tsx | 7 ++- .../team/user-team-card/task-estimate.tsx | 52 +++++++++++-------- apps/web/lib/features/user-profile-plans.tsx | 6 ++- apps/web/lib/utils.ts | 27 ++++++++-- 5 files changed, 63 insertions(+), 30 deletions(-) diff --git a/apps/web/lib/features/task/daily-plan/past-tasks.tsx b/apps/web/lib/features/task/daily-plan/past-tasks.tsx index c8d61a9c4..3c5ac73c0 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -88,6 +88,7 @@ export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any viewType={'dailyplan'} task={task} profile={profile} + plan={plan} type="HORIZONTAL" taskBadgeClassName={`rounded-sm`} taskTitleClassName="mt-[0.0625rem]" diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 3b163c400..7ccd4b84a 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -190,7 +190,12 @@ export function TaskCard(props: Props) { <> {/* TaskEstimateInfo */}
    - +
    )} diff --git a/apps/web/lib/features/team/user-team-card/task-estimate.tsx b/apps/web/lib/features/team/user-team-card/task-estimate.tsx index e33188e37..fc7332189 100644 --- a/apps/web/lib/features/team/user-team-card/task-estimate.tsx +++ b/apps/web/lib/features/team/user-team-card/task-estimate.tsx @@ -1,14 +1,15 @@ 'use client'; import { mergeRefs, secondsToTime } from '@app/helpers'; -import { I_TMCardTaskEditHook, I_TeamMemberCardHook } from '@app/hooks'; -import { IClassName } from '@app/interfaces'; +import { I_TMCardTaskEditHook, I_TeamMemberCardHook, useAuthenticateUser } from '@app/hooks'; +import { IClassName, IDailyPlan } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { SpinnerLoader, Text } from 'lib/components'; import { EditPenBoxIcon, CheckCircleTickIcon as TickSaveIcon } from 'assets/svg'; import { TaskEstimate, TaskProgressBar } from 'lib/features'; import { useRef } from 'react'; import { useTranslations } from 'next-intl'; +import { checkPastDate } from 'lib/utils'; type Props = IClassName & { memberInfo: I_TeamMemberCardHook; @@ -16,6 +17,7 @@ type Props = IClassName & { activeAuthTask: boolean; showTime?: boolean; radial?: boolean; + plan?: IDailyPlan; }; export function TaskEstimateInfo({ className, activeAuthTask, showTime = true, radial = false, ...rest }: Props) { @@ -36,11 +38,11 @@ export function TaskEstimateInfo({ className, activeAuthTask, showTime = true, r ); } -export function TaskEstimateInput({ memberInfo, edition }: Omit) { +export function TaskEstimateInput({ memberInfo, edition, plan }: Omit) { const t = useTranslations(); const loadingRef = useRef(false); const task = edition.task || memberInfo.memberTask; - + const { isTeamManager } = useAuthenticateUser(); const hasEditMode = edition.estimateEditMode && task; const closeFn = () => { @@ -88,27 +90,31 @@ export function TaskEstimateInput({ memberInfo, edition }: Omit {(memberInfo.isAuthUser || memberInfo.isAuthTeamManager) && ( - )} - + )}
    diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index c2aaaf743..98810a9e7 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { useCanSeeActivityScreen, useDailyPlan, useUserProfilePage } from '@app/hooks'; +import { useAuthenticateUser, useCanSeeActivityScreen, useDailyPlan, useUserProfilePage } from '@app/hooks'; import { TaskCard } from './task/task-card'; import { IDailyPlan, ITeamTask } from '@app/interfaces'; import { AlertPopup, Container, HorizontalSeparator, NoData, ProgressBar, VerticalSeparator } from 'lib/components'; @@ -31,6 +31,7 @@ import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; import { handleDragAndDrop } from '@app/helpers/drag-and-drop'; import { DragDropContext, Droppable, Draggable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; +import { checkPastDate } from 'lib/utils'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; @@ -357,6 +358,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil const [editTime, setEditTime] = useState(false); const [time, setTime] = useState(0); const { updateDailyPlan, updateDailyPlanLoading } = useDailyPlan(); + const { isTeamManager } = useAuthenticateUser(); // Get all tasks's estimations time // Helper function to sum times const sumTimes = (tasks: ITeamTask[], key: any) => @@ -392,7 +394,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil Planned time: {formatIntegerToHour(plan.workTimePlanned)}
    - {planMode !== 'Past Tasks' && ( + {(!checkPastDate(plan.date) || isTeamManager) && ( setEditTime(true)} diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts index b35caa3b9..3dd9cde8c 100644 --- a/apps/web/lib/utils.ts +++ b/apps/web/lib/utils.ts @@ -51,12 +51,31 @@ export const shortenLink = (value: any): string => { return `${start}...${end}`; }; - export function formatWithSuffix(date: Date) { const day = moment(date).date(); const suffix = - day === 1 || day === 21 || day === 31 ? 'st' : - day === 2 || day === 22 ? 'nd' : - day === 3 || day === 23 ? 'rd' : 'th'; + day === 1 || day === 21 || day === 31 + ? 'st' + : day === 2 || day === 22 + ? 'nd' + : day === 3 || day === 23 + ? 'rd' + : 'th'; return moment(date).format(`D[${suffix}] MMMM YYYY`); } + +export function checkPastDate(dateToBeChecked?: Date): boolean { + if (dateToBeChecked) { + const todayDate = new Date(new Date().toUTCString()); + const date = new Date(new Date(dateToBeChecked).toUTCString()); + + date.setHours(0, 0, 0, 0); + todayDate.setHours(0, 0, 0, 0); + + console.log(todayDate, date); + + return todayDate > date; + } else { + return false; // Return false if dateToBeChecked is not provided or is null or undefined. + } +} From ce518001b3b10bc83e9ec90701c49011ac0268c4 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 7 Aug 2024 22:07:36 +0200 Subject: [PATCH 14/18] fix: fix the number of outstanding tasks in notifications --- .../features/team/team-outstanding-notifications.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index 69dbef22a..2017b53ea 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -6,6 +6,7 @@ import { Tooltip } from 'lib/components'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { useEffect, useState } from 'react'; +import { estimatedTotalTime } from '../task/daily-plan'; interface IEmployeeWithOutstanding { employeeId: string | undefined; @@ -25,7 +26,7 @@ export function TeamOutstandingNotifications() { return (
    {outstandingPlans && outstandingPlans.length > 0 && ( - + )} {dailyPlan.items && dailyPlan.items.length > 0 && isTeamManager && ( @@ -35,7 +36,7 @@ export function TeamOutstandingNotifications() { ); } -function UserOutstandingNotification({ outstandingTasks, user }: { outstandingTasks: IDailyPlan[]; user?: IUser }) { +function UserOutstandingNotification({ outstandingPlans, user }: { outstandingPlans: IDailyPlan[]; user?: IUser }) { const t = useTranslations(); // Notification will be displayed 6 hours after the user closed it @@ -45,7 +46,9 @@ function UserOutstandingNotification({ outstandingTasks, user }: { outstandingTa const name = user?.name || user?.firstName || user?.lastName || user?.username; const [visible, setVisible] = useState(false); - const tasks = outstandingTasks.flatMap((plan) => plan.tasks); + const outStandingTasksCount = estimatedTotalTime( + outstandingPlans.map((plan) => plan.tasks?.map((task) => task)) + ).totalTasks; useEffect(() => { const checkNotification = () => { @@ -73,7 +76,7 @@ function UserOutstandingNotification({ outstandingTasks, user }: { outstandingTa {visible && (
    - {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {tasks?.length}{' '} + {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {outStandingTasksCount}{' '} {t('pages.home.OUTSTANDING_NOTIFICATIONS.USER_LABEL')}{' '} {t('pages.home.OUTSTANDING_NOTIFICATIONS.OUTSTANDING_VIEW')} From cc605d608fef8f0d9114b1a0d54c9142c9cbf130 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Thu, 8 Aug 2024 11:26:49 +0200 Subject: [PATCH 15/18] feat: add 'Stats' tab with global metrics and enhanced legend component --- .../app/[locale]/profile/[memberId]/page.tsx | 6 +- apps/web/lib/features/activity/calendar.tsx | 73 ------------ .../lib/features/activity/screen-calendar.tsx | 7 ++ .../integrations/activity-calendar/index.tsx | 107 ++++++++++++++++++ apps/web/lib/features/task/task-filters.tsx | 24 ++-- apps/web/lib/features/user-profile-tasks.tsx | 21 ++-- 6 files changed, 144 insertions(+), 94 deletions(-) delete mode 100644 apps/web/lib/features/activity/calendar.tsx create mode 100644 apps/web/lib/features/activity/screen-calendar.tsx create mode 100644 apps/web/lib/features/integrations/activity-calendar/index.tsx 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 */} -
    + {/*
    -
    +
    */} 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([]); - - useEffect(() => { - setCalendarData( - timerLogsDailyReport.map((el) => ({ value: Number((el.sum / 3600).toPrecision(2)), day: el.date })) - ); - }, [timerLogsDailyReport]); - - return ( -
    - {timerLogsDailyReportLoading ? ( - - ) : ( - d.toLocaleString('en-US', { month: 'short' })} - /> - )} -
    - ); -} - -// Skeletons -function ActivityCalendarSkeleton() { - const { innerWidth: deviceWith } = window; - - const skeletons = Array.from(Array(12)); - - return ( -
    - {skeletons.map((_, index) => ( - - ))} -
    - ); -} 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 () +} 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([]); + useEffect(() => { + setCalendarData( + timerLogsDailyReport.map((el) => ({ value: Number((el.sum / 3600).toPrecision(2)), day: el.date })) + ); + }, [timerLogsDailyReport]); + + return ( +
    + {timerLogsDailyReportLoading ? ( + + ) : ( +
    + + d.toLocaleString('en-US', { month: 'short' })} + /> + + +
    + )} +
    + ); +} + +// Skeletons +function ActivityCalendarSkeleton() { + const { innerWidth: deviceWith } = window; + + const skeletons = Array.from(Array(12)); + + return ( +
    + {skeletons.map((_, index) => ( + + ))} +
    + ); +} + +function ActivityLegend() { + return ( +
    +

    Legend

    +
    + + 8 Hours or more +
    +
    + + 6 Hours +
    +
    + + 4 Hours +
    +
    + + 2 Hours +
    +
    + ) +} 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' && } {tabFiltered.tab === 'dailyplan' && } {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]" /> From fafa0319ef2fd2dbdc0d3dce44cfcb5d5a7d468f Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 8 Aug 2024 14:13:44 +0200 Subject: [PATCH 16/18] fix: use the same logic for all start / stop timer button --- apps/web/lib/features/timer/timer.tsx | 47 ++++++++++++++++++++------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/web/lib/features/timer/timer.tsx b/apps/web/lib/features/timer/timer.tsx index 375927edc..c18505ade 100644 --- a/apps/web/lib/features/timer/timer.tsx +++ b/apps/web/lib/features/timer/timer.tsx @@ -177,16 +177,10 @@ export function Timer({ className }: IClassName) { } export function MinTimerFrame({ className }: IClassName) { - const { - hours, - minutes, - seconds, - ms_p, - - timerHanlder, - timerStatus, - disabled - } = useTimerView(); + const { hours, minutes, seconds, ms_p, timerStatus, disabled, hasPlan } = useTimerView(); + const { modals, startStopTimerHandler } = useStartStopTimerHandler(); + const { activeTeam, activeTeamTask } = useTeamTasks(); + const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); return (
    + + + + {hasPlan && hasPlan.tasks && ( + + )} + + {hasPlan && ( + + )} + + {requirePlan && hasPlan && activeTeamTask && ( + + )}
    ); } From a0081301d255747909e2cbec0c9bab39c2aceead Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 8 Aug 2024 14:32:36 +0200 Subject: [PATCH 17/18] feat: remane the dailyplan tab name to Planned --- apps/web/lib/features/task/task-filters.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 6c7415910..5c3272f3f 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -116,7 +116,7 @@ export function useTaskFilter(profile: I_UserProfilePage) { if (activeTeam?.shareProfileView || canSeeActivity) { tabs.push({ tab: 'dailyplan', - name: 'Daily Plan', + name: 'Planned', description: 'This tab shows all yours tasks planned', count: profile.tasksGrouped.planned }); From 82444db6c73045f9dc1b2a5e5d4a937d9e8a5417 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 8 Aug 2024 21:24:20 +0200 Subject: [PATCH 18/18] [Fix]: Daily Plan | Total estimated time on Outstanding UI (#2859) * feat: Display total estimated time for all tasks in the Outstanding tab (read-only) * fix: same bug --- .../features/task/daily-plan/task-estimated-count.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx b/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx index fd549e7b2..94014fd76 100644 --- a/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx +++ b/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx @@ -9,18 +9,19 @@ export function TaskEstimatedCount({ outstandingPlans }: ITaskEstimatedCount) { const element = outstandingPlans?.map((plan: IDailyPlan) => plan.tasks?.map((task) => task)); const { timesEstimated, totalTasks } = estimatedTotalTime(element || []); const { h: hour, m: minute } = secondsToTime(timesEstimated || 0); + return (
    - Estimated: - + Estimated: + {hour}h{minute}m
    - Total tasks: - {totalTasks} + Total tasks: + {totalTasks}
    );