diff --git a/.cspell.json b/.cspell.json index 56963e340..b662c7478 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", @@ -356,7 +360,12 @@ "xlcard", "xlight", "yellowbox", - "vhidden" + "vhidden", + "POSTHOG", + "posthog", + "pageviews", + "pageleave", + "pageview" ], "useGitignore": true, "ignorePaths": [ 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/.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]/calendar/page.tsx b/apps/web/app/[locale]/calendar/page.tsx new file mode 100644 index 000000000..e805038f3 --- /dev/null +++ b/apps/web/app/[locale]/calendar/page.tsx @@ -0,0 +1,92 @@ +"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'; +import { LuCalendarDays } from "react-icons/lu"; + + +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]/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]/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/[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/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 5e1860dbd..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'; @@ -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]); @@ -137,9 +149,9 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId {/* TaskFilter */} -
+ {/*
-
+
*/} 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/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/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/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index 8aa44d04e..67f79d4f4 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 an existing plan + const isPlanExist = profileDailyPlans.items.find((plan) => + plan.date?.toString()?.startsWith(new Date(data.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; + } + + return plan; + }); + + setProfileDailyPlans({ + total: updatedPlans.length, + items: updatedPlans + }); + } else { + setProfileDailyPlans({ + total: profileDailyPlans.total + 1, + items: [...profileDailyPlans.items, res.data] + }); + } + setEmployeePlans([...employeePlans, res.data]); getMyDailyPlans(); return res; 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/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/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/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/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 new file mode 100644 index 000000000..7eb58cdae --- /dev/null +++ b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx @@ -0,0 +1,230 @@ +"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 FullCalendar from '@fullcalendar/react'; +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 CalendarComponent from './calendar-component'; +import { PiTimerBold } from "react-icons/pi"; +import { formatWithSuffix } 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 ( +
+
+
+
+ + +
+
+ +
+
+
+ + {isDialogOpen && ( +
+ +
+ )} +
+ +
+ + + +
+ ) +} + + +export const CardItems = ({ selectedDate }: { selectedDate: Date }) => { + return ( +
+
+ + {formatWithSuffix(new Date(selectedDate))} + +
+ + + + + +
+
+
+ ) +} + +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/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/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/daily-plan/task-estimated-count.tsx b/apps/web/lib/features/task/daily-plan/task-estimated-count.tsx index 811f7cbf7..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}
); @@ -42,3 +43,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-card.tsx b/apps/web/lib/features/task/task-card.tsx index 197f1dbad..7ccd4b84a 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -101,59 +101,72 @@ 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 ( <>