From e3231e09182a286376ad21553be9140a3e4eea18 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Thu, 25 Jul 2024 09:16:59 +0200 Subject: [PATCH 01/17] feaf: add activity calendar view (#2777) * feaf: add activity calendar view with dummy data * fix: spelling error * feat: connect the calendar activity component with the api * feat: add calendar activity skeleton on loading --- .cspell.json | 1 + .../app/[locale]/profile/[memberId]/page.tsx | 4 + apps/web/app/hooks/features/useTimeLogs.ts | 52 +++++++++++++ apps/web/app/interfaces/timer/ITimerLogs.ts | 13 ++++ .../services/client/api/activity/time-logs.ts | 23 ++++++ apps/web/app/stores/time-logs.ts | 7 ++ apps/web/lib/features/activity/calendar.tsx | 73 +++++++++++++++++++ apps/web/package.json | 4 +- 8 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/hooks/features/useTimeLogs.ts create mode 100644 apps/web/app/interfaces/timer/ITimerLogs.ts create mode 100644 apps/web/app/services/client/api/activity/time-logs.ts create mode 100644 apps/web/app/stores/time-logs.ts create mode 100644 apps/web/lib/features/activity/calendar.tsx diff --git a/.cspell.json b/.cspell.json index 7281b6e66..e94a246dd 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,7 @@ "caseSensitive": false, "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "words": [ + "nivo", "accepte", "Accordian", "adipiscing", diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index e0afccf3c..5e1860dbd 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -23,6 +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'; export type FilterTab = 'Tasks' | 'Screenshots' | 'Apps' | 'Visited Sites'; @@ -136,6 +137,9 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId {/* TaskFilter */} +
+ +
diff --git a/apps/web/app/hooks/features/useTimeLogs.ts b/apps/web/app/hooks/features/useTimeLogs.ts new file mode 100644 index 000000000..eecfc6c64 --- /dev/null +++ b/apps/web/app/hooks/features/useTimeLogs.ts @@ -0,0 +1,52 @@ +import { useRecoilState } from 'recoil'; +import { getTimerLogsDailyReportRequestAPI } from '@app/services/client/api/activity/time-logs'; +import { useAuthenticateUser } from './useAuthenticateUser'; +import { timerLogsDailyReportState } from '@app/stores/time-logs'; +import { useQuery } from '../useQuery'; +import { useCallback, useEffect } from 'react'; +import moment from 'moment'; + +export function useTimeLogs() { + const { user } = useAuthenticateUser(); + const [timerLogsDailyReport, setTimerLogsDailyReport] = useRecoilState(timerLogsDailyReportState); + + const { loading: timerLogsDailyReportLoading, queryCall: queryTimerLogsDailyReport } = useQuery( + getTimerLogsDailyReportRequestAPI + ); + + const getTimerLogsDailyReport = useCallback( + (startDate: Date = moment().startOf('year').toDate(), endDate: Date = moment().endOf('day').toDate()) => { + queryTimerLogsDailyReport({ + tenantId: user?.tenantId ?? '', + organizationId: user?.employee.organizationId ?? '', + employeeId: user?.employee.id ?? '', + startDate, + endDate + }) + .then((response) => { + if (response.data && Array.isArray(response.data)) { + setTimerLogsDailyReport(response.data); + } + }) + .catch((error) => { + console.log(error); + }); + }, + [ + queryTimerLogsDailyReport, + setTimerLogsDailyReport, + user?.employee.id, + user?.employee.organizationId, + user?.tenantId + ] + ); + + useEffect(() => { + getTimerLogsDailyReport(); + }, [getTimerLogsDailyReport]); + + return { + timerLogsDailyReport, + timerLogsDailyReportLoading + }; +} diff --git a/apps/web/app/interfaces/timer/ITimerLogs.ts b/apps/web/app/interfaces/timer/ITimerLogs.ts new file mode 100644 index 000000000..73e1f3a88 --- /dev/null +++ b/apps/web/app/interfaces/timer/ITimerLogs.ts @@ -0,0 +1,13 @@ +export interface ITimerLogsDailyReportRequest { + tenantId: string; + organizationId: string; + employeeId: string; + startDate: Date; + endDate: Date; +} + +export interface ITimerLogsDailyReport { + activity: number; + date: string; // '2024-07-19' + sum: number; // in seconds +} diff --git a/apps/web/app/services/client/api/activity/time-logs.ts b/apps/web/app/services/client/api/activity/time-logs.ts new file mode 100644 index 000000000..617ea1936 --- /dev/null +++ b/apps/web/app/services/client/api/activity/time-logs.ts @@ -0,0 +1,23 @@ +import { get } from '@app/services/client/axios'; +import { ITimerLogsDailyReportRequest, ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs'; +import qs from 'qs'; + +export async function getTimerLogsDailyReportRequestAPI({ + tenantId, + organizationId, + employeeId, + startDate, + endDate +}: ITimerLogsDailyReportRequest) { + const params = { + tenantId: tenantId, + organizationId: organizationId, + employeeId, + todayEnd: startDate.toISOString(), + todayStart: endDate.toISOString() + }; + + const query = qs.stringify(params); + + return get(`/timesheet/time-log/report/daily?${query}`); +} diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts new file mode 100644 index 000000000..c0019445b --- /dev/null +++ b/apps/web/app/stores/time-logs.ts @@ -0,0 +1,7 @@ +import { ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs'; +import { atom } from 'recoil'; + +export const timerLogsDailyReportState = atom({ + key: 'timerLogsDailyReportState', + default: [] +}); diff --git a/apps/web/lib/features/activity/calendar.tsx b/apps/web/lib/features/activity/calendar.tsx new file mode 100644 index 000000000..b250ccf6b --- /dev/null +++ b/apps/web/lib/features/activity/calendar.tsx @@ -0,0 +1,73 @@ +'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/package.json b/apps/web/package.json index 217d809a3..b0cb5fc52 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -33,6 +33,8 @@ "@heroicons/react": "^2.0.12", "@jitsi/react-sdk": "^1.3.0", "@jitsu/jitsu-react": "^1.3.0", + "@nivo/calendar": "^0.87.0", + "@nivo/core": "^0.87.0", "@opentelemetry/api": "^1.7.0", "@opentelemetry/auto-instrumentations-node": "^0.40.1", "@opentelemetry/exporter-trace-otlp-http": "^0.45.1", @@ -105,7 +107,7 @@ "react-popper-tooltip": "^4.4.2", "react-resizable-panels": "^2.0.19", "recoil": "^0.7.7", - "sharp": "^0.32.6", + "sharp": "^0.33.4", "slate": "^0.90.0", "slate-history": "^0.93.0", "slate-hyperscript": "^0.77.0", From 18d356ac2c354568343888a6e3ac5412461c1b3f Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:17:19 +0200 Subject: [PATCH 02/17] feat: Daily Plan compare Estimated tasks time and Planned work hours (#2792) --- apps/web/app/[locale]/page-component.tsx | 27 +------ apps/web/app/helpers/daily-plan-estimated.ts | 57 +++++++++----- apps/web/app/helpers/date.ts | 5 ++ .../daily-plan-compare-estimate-modal.tsx | 75 ++++++++++++------- .../lib/features/team-members-card-view.tsx | 33 +++++++- 5 files changed, 126 insertions(+), 71 deletions(-) diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index 18ba41fa7..f0ee73e76 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -3,14 +3,14 @@ 'use client'; import React, { useEffect, useState } from 'react'; -import { useDailyPlan, useOrganizationTeams, useUserProfilePage } from '@app/hooks'; +import { useOrganizationTeams } from '@app/hooks'; import { clsxm } from '@app/utils'; import NoTeam from '@components/pages/main/no-team'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Card } from 'lib/components'; import { AuthUserTaskInput, TeamInvitations, TeamMembers, Timer, UnverifiedEmail } from 'lib/features'; import { MainLayout } from 'lib/layout'; -import { DAILY_PLAN_SHOW_MODAL, IssuesView } from '@app/constants'; +import { IssuesView } from '@app/constants'; import { useNetworkState } from '@uidotdev/usehooks'; import Offline from '@components/pages/offline'; import { useTranslations } from 'next-intl'; @@ -31,21 +31,10 @@ import { PeoplesIcon } from 'assets/svg'; import TeamMemberHeader from 'lib/features/team-member-header'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; import { TeamOutstandingNotifications } from 'lib/features/team/team-outstanding-notifications'; -import { DailyPlanCompareEstimatedModal } from 'lib/features/daily-plan'; function MainPage() { const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false) - const { todayPlan } = useDailyPlan(); - const profile = useUserProfilePage(); const [headerSize, setHeaderSize] = useState(10); - const plan = todayPlan.find((plan) => plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])); - - const defaultOpenPopup = - typeof window !== 'undefined' - ? (window.localStorage.getItem(DAILY_PLAN_SHOW_MODAL)) || null - : new Date().toISOString().split('T')[0]; - const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams(); const [fullWidth, setFullWidth] = useRecoilState(fullWidthState); const [view, setView] = useRecoilState(headerTabs); @@ -63,12 +52,7 @@ function MainPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [path, setView]); - useEffect(() => { - window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); - if (defaultOpenPopup !== new Date().toISOString().split('T')[0] || defaultOpenPopup === null) { - setIsOpen(true) - } - }, [defaultOpenPopup, plan]) + React.useEffect(() => { @@ -81,10 +65,7 @@ function MainPage() { } return ( <> - setIsOpen((prev) => { - window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); - return !prev; - })} todayPlan={todayPlan} profile={profile} /> +
{/*
*/} diff --git a/apps/web/app/helpers/daily-plan-estimated.ts b/apps/web/app/helpers/daily-plan-estimated.ts index 498648ed7..1c18fb348 100644 --- a/apps/web/app/helpers/daily-plan-estimated.ts +++ b/apps/web/app/helpers/daily-plan-estimated.ts @@ -1,29 +1,48 @@ -"use client" - import { IDailyPlan } from "@app/interfaces"; +import { convertHourToSeconds } from "./date"; -export const dailyPlanCompareEstimated = (plans: IDailyPlan[]) => { - - const plan = plans.find((plan) => plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])); +export interface IDailyPlanCompareEstimated { + difference: boolean, + workTimePlanned?: number, + estimated?: boolean[] | undefined, + plan?: IDailyPlan | undefined +} - const times = plan?.tasks?.map((task) => task?.estimate).filter((time): time is number => typeof time === 'number') ?? []; - const estimated = plan?.tasks?.map((task) => task.estimate! > 0); +export const dailyPlanCompareEstimated = (plans: IDailyPlan[]): IDailyPlanCompareEstimated => { + const plan = plans.find((plan) => plan.date?.toString()?.startsWith(new Date().toISOString().split('T')[0])); - let estimatedTime = 0; - if (times.length > 0) estimatedTime = times.reduce((acc, cur) => acc + cur, 0) ?? 0; + if (!plan) { + return { + difference: false, + workTimePlanned: 0, + estimated: [], + plan: undefined + }; + } - const workedTimes = - plan?.tasks?.map((task) => task.totalWorkedTime).filter((time): time is number => typeof time === 'number') ?? - []; + const workTimePlanned = convertHourToSeconds(plan.workTimePlanned!); + const times = plan.tasks?.map((task) => task.estimate).filter((time): time is number => typeof time === 'number') ?? []; + const estimated = plan.tasks?.map((task) => task.estimate! > 0); - let totalWorkTime = 0; - if (workedTimes?.length > 0) totalWorkTime = workedTimes.reduce((acc, cur) => acc + cur, 0) ?? 0; + let estimatedTime = 0; + if (times.length > 0) { + estimatedTime = times.reduce((acc, cur) => acc + cur, 0) ?? 0; + } - const result = estimated?.every(Boolean) ? estimatedTime - totalWorkTime : null; + const difference = dailyPlanSubtraction(estimatedTime, workTimePlanned); return { - result, - totalWorkTime, - estimatedTime - } + workTimePlanned, + estimated, + difference, + plan + }; +} + +export function dailyPlanSubtraction( + estimatedTime: number, + workTimePlanned: number +): boolean { + const difference = Math.abs(estimatedTime - workTimePlanned) / (60 * 2); + return difference >= -1 && difference <= 1; } diff --git a/apps/web/app/helpers/date.ts b/apps/web/app/helpers/date.ts index 5aa529fd2..6078cf3ac 100644 --- a/apps/web/app/helpers/date.ts +++ b/apps/web/app/helpers/date.ts @@ -163,3 +163,8 @@ export const isTestDateRange = (itemDate: Date, from?: Date, to?: Date) => { return true; // or false, depending on your default logic } } + + +export function convertHourToSeconds(hours: number) { + return hours * 60 * 60; +} diff --git a/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx b/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx index 5e40b0d9d..3f8962988 100644 --- a/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx +++ b/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx @@ -8,11 +8,17 @@ import { IDailyPlan, ITeamTask } from '@app/interfaces'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { clsxm } from '@app/utils'; import { TaskEstimateInput } from '../team/user-team-card/task-estimate'; -import { useTeamMemberCard, useTimer, useTMCardTaskEdit } from '@app/hooks'; +import { useDailyPlan, useTeamMemberCard, useTimer, useTMCardTaskEdit } from '@app/hooks'; import { dailyPlanCompareEstimated } from '@app/helpers/daily-plan-estimated'; import { secondsToTime } from '@app/helpers'; +import { DAILY_PLAN_SHOW_MODAL } from '@app/constants'; - +export interface IDailyPlanCompareEstimated { + difference?: boolean, + workTimePlanned?: number, + estimated?: boolean[] | undefined, + plan?: IDailyPlan | undefined +} export function DailyPlanCompareEstimatedModal({ open, closeModal, @@ -20,20 +26,29 @@ export function DailyPlanCompareEstimatedModal({ profile }: { open: boolean, closeModal: () => void, todayPlan: IDailyPlan[], profile: any }) { - const { estimatedTime } = dailyPlanCompareEstimated(todayPlan); - const { h: dh, m: dm } = secondsToTime(estimatedTime || 0); + const { difference, workTimePlanned, estimated, plan } = dailyPlanCompareEstimated(todayPlan); + const { updateDailyPlan, updateDailyPlanLoading } = useDailyPlan(); + const { h: dh, m: dm } = secondsToTime(workTimePlanned || 0); const { startTimer } = useTimer() const hour = dh.toString()?.padStart(2, '0'); const minute = dm.toString()?.padStart(2, '0'); const [times, setTimes] = useState({ - hours: '--', + hours: (workTimePlanned! / 3600).toString(), meridiem: 'PM', minute: '--' - }) + }); + + const onClick = () => { - startTimer(); - window.localStorage.setItem('daily-plan-modal', new Date().toISOString().split('T')[0]); + updateDailyPlan({ workTimePlanned: parseInt(times.hours) }, plan?.id ?? ''); + if (!updateDailyPlanLoading) { + startTimer(); + closeModal(); + window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); + } + } + return (
@@ -48,11 +63,7 @@ export function DailyPlanCompareEstimatedModal({ meridiem: 'AM', minute: minute, }} - onChange={(value) => { - setTimes(value); - console.log(times) - - }} + onChange={(value) => setTimes(value)} />
@@ -60,21 +71,33 @@ export function DailyPlanCompareEstimatedModal({ {todayPlan.map((plan, i) => { return
{plan.tasks?.map((data, index) => { - return + return
+ +
})}
})}
- - Please correct planned work hours or re-estimate task(s) + {!difference && !estimated?.every(Boolean) && ( + <> + + Please correct planned work hours or re-estimate task(s) + + ) + }
- + 0 ? false : true)} + />
@@ -89,8 +112,8 @@ export function DailyPlanTask({ task, profile }: { task?: ITeamTask, profile: an const memberInfo = useTeamMemberCard(member); return ( -
-
+
+
@@ -113,7 +137,7 @@ export function DailyPlanTask({ task, profile }: { task?: ITeamTask, profile: an } -export function DailyPlanCompareActionButton({ closeModal, onClick, loading }: { closeModal?: () => void, onClick?: () => void, loading?: boolean }) { +export function DailyPlanCompareActionButton({ closeModal, onClick, loading, disabled }: { closeModal?: () => void, onClick?: () => void, loading?: boolean, disabled: boolean }) { return (
-
@@ -151,7 +175,6 @@ export function DailyPlanCompareHeader() { ) } - export function DailyPlanWorkTimeInput() { return ( <> diff --git a/apps/web/lib/features/team-members-card-view.tsx b/apps/web/lib/features/team-members-card-view.tsx index 38f0ad379..c28db91ea 100644 --- a/apps/web/lib/features/team-members-card-view.tsx +++ b/apps/web/lib/features/team-members-card-view.tsx @@ -1,10 +1,12 @@ -import { useAuthenticateUser, useModal, useOrganizationEmployeeTeams, useTeamInvitations } from '@app/hooks'; +import { useAuthenticateUser, useDailyPlan, useModal, useOrganizationEmployeeTeams, useTeamInvitations, useUserProfilePage } from '@app/hooks'; import { Transition } from '@headlessui/react'; import { InviteFormModal } from './team/invite/invite-form-modal'; import { InvitedCard, InviteUserTeamCard } from './team/invite/user-invite-card'; import { InviteUserTeamSkeleton, UserTeamCard, UserTeamCardSkeleton } from '.'; import { OT_Member } from '@app/interfaces'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { DailyPlanCompareEstimatedModal } from './daily-plan'; +import { DAILY_PLAN_SHOW_MODAL } from '@app/constants'; interface Props { teamMembers: OT_Member[]; @@ -19,9 +21,17 @@ const TeamMembersCardView: React.FC = ({ teamsFetching = false, publicTeam = false }) => { - const { isTeamManager } = useAuthenticateUser(); + const { isTeamManager } = useAuthenticateUser(); const { teamInvitations } = useTeamInvitations(); + const [isOpen, setIsOpen] = useState(false) + const { todayPlan } = useDailyPlan(); + const profile = useUserProfilePage(); + const defaultOpenPopup = + typeof window !== 'undefined' + ? (window.localStorage.getItem(DAILY_PLAN_SHOW_MODAL)) || null + : new Date().toISOString().split('T')[0]; + const plan = todayPlan.find((plan) => plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])); const { updateOrganizationTeamEmployeeOrderOnList } = useOrganizationEmployeeTeams(); @@ -32,6 +42,18 @@ const TeamMembersCardView: React.FC = ({ useEffect(() => setMemberOrdereds(members), [members]); + useEffect(() => { + if (plan) { + const currentDateString = new Date().toISOString().split('T')[0]; + window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, currentDateString); + if (defaultOpenPopup !== currentDateString || !defaultOpenPopup) { + setIsOpen(true); + } + } + }, [defaultOpenPopup, plan]); + + + function handleSort() { const peopleClone = [...memberOrdereds]; const temp = peopleClone[dragTeamMember.current]; @@ -49,6 +71,11 @@ const TeamMembersCardView: React.FC = ({ return ( <> + setIsOpen((prev) => { + window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); + return !prev; + })} todayPlan={todayPlan} profile={profile} /> +
    {/* Current authenticated user members */} Date: Thu, 25 Jul 2024 09:17:36 +0200 Subject: [PATCH 03/17] feat:The Planned + date badges should have the RED color for tasks that were planned before today (#2793) --- apps/web/app/helpers/plan-day-badge.ts | 24 +++++++++++++++++++ .../features/task/task-all-status-type.tsx | 10 ++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/web/app/helpers/plan-day-badge.ts b/apps/web/app/helpers/plan-day-badge.ts index 70c1f92ed..1bd23d610 100644 --- a/apps/web/app/helpers/plan-day-badge.ts +++ b/apps/web/app/helpers/plan-day-badge.ts @@ -28,3 +28,27 @@ export const planBadgeContent = ( return null; } }; + + +export const planBadgeContPast = ( + dailyPlan: IDailyPlan[], + taskId: ITeamTask['id'] +): string | null => { + const today = new Date().toISOString().split('T')[0]; + const dailyPlanDataPast = dailyPlan.filter(plan => new Date(plan.date) < new Date(today)); + const allTasks = dailyPlanDataPast.flatMap(plan => plan.tasks); + const taskCount: { [key: string]: number } = allTasks?.reduce((acc, task) => { + if (task && task.id) { acc[task.id] = (acc[task.id] || 0) + 1; } + return acc; + }, {} as { [key: string]: number }); + + const dailyPlanPast = allTasks?.filter(task => task && taskCount[task.id] === 1); + const filterDailyPlan = dailyPlanPast.filter((plan) => plan?.id === taskId); + if (filterDailyPlan.length > 0) { + return 'Planned'; + } else { + return null; + } + + +} diff --git a/apps/web/lib/features/task/task-all-status-type.tsx b/apps/web/lib/features/task/task-all-status-type.tsx index f4224ffd2..7c221d4c9 100644 --- a/apps/web/lib/features/task/task-all-status-type.tsx +++ b/apps/web/lib/features/task/task-all-status-type.tsx @@ -12,7 +12,7 @@ import { useTaskStatusValue } from './task-status'; import { clsxm } from '@app/utils'; -import { planBadgeContent } from '@app/helpers'; +import { planBadgeContent, planBadgeContPast } from '@app/helpers'; import { CalendarIcon } from '@radix-ui/react-icons'; import { FilterTabs } from '../user-profile-plans'; @@ -66,6 +66,11 @@ export function TaskAllStatusTypes({ ); }, [taskLabels, task?.tags]); + const taskId = planBadgeContPast( + dailyPlan.items, + task!.id + ) + return (
    @@ -99,10 +104,11 @@ export function TaskAllStatusTypes({ titleClassName={'text-[0.625rem] font-[500]'} /> )} + {planBadgeContent(dailyPlan.items, task?.id ?? '', tab) && (
    From 058cfa75209e3372fd71975e523e0d1ba4e34103 Mon Sep 17 00:00:00 2001 From: "Thierry CH." Date: Thu, 25 Jul 2024 09:17:52 +0200 Subject: [PATCH 04/17] fixbug: fix outstanding notifications for manager role (#2795) --- apps/web/lib/features/team/team-outstanding-notifications.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index 991e45736..69dbef22a 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -105,6 +105,7 @@ function UserOutstandingNotification({ outstandingTasks, user }: { outstandingTa } function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstandingTasks: IDailyPlan[] }) { + const { user } = useAuthenticateUser(); const t = useTranslations(); // Notification will be displayed 6 hours after the user closed it @@ -114,6 +115,7 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding const [visible, setVisible] = useState(false); const employeeWithOutstanding = outstandingTasks + .filter((plan) => plan.employeeId !== user?.employee.id) .filter((plan) => !plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])) .filter((plan) => { @@ -168,7 +170,7 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding }; return ( <> - {visible && ( + {uniqueEmployees?.length > 0 && visible && (
    {t('pages.home.OUTSTANDING_NOTIFICATIONS.SUBJECT')} {uniqueEmployees?.length} team member(s) From e9c65b6bc801d283195d113dba143e27a43b5853 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:45:32 +0200 Subject: [PATCH 05/17] [Improvement]: Customize select (#2798) * improvemen: manual time-2D-dark-mode-UI dark and fix some bug, customize select * improvemen: manual time-2D-dark-mode-UI dark and fix some bug, customize select * fix: some bug on modal add time --- apps/mobile/app/helpers/date.ts | 1 + apps/web/app/services/client/api/tasks.ts | 14 +- .../lib/components/custom-select/index.tsx | 86 +++++ apps/web/lib/components/emoji-picker.tsx | 7 +- apps/web/lib/components/index.ts | 1 + .../web/lib/components/keyboard-shortcuts.tsx | 2 +- apps/web/lib/components/toggler.tsx | 2 +- apps/web/lib/features/task/task-filters.tsx | 296 +++++++++--------- apps/web/styles/style.css | 6 + 9 files changed, 254 insertions(+), 161 deletions(-) create mode 100644 apps/web/lib/components/custom-select/index.tsx create mode 100644 apps/web/styles/style.css diff --git a/apps/mobile/app/helpers/date.ts b/apps/mobile/app/helpers/date.ts index b46ea0a6f..8f16fc44c 100644 --- a/apps/mobile/app/helpers/date.ts +++ b/apps/mobile/app/helpers/date.ts @@ -20,6 +20,7 @@ export function addHours(numOfHours: number, date = new Date()) { return date; } + export function secondsToTime(secs: number) { const hours = Math.floor(secs / (60 * 60)); diff --git a/apps/web/app/services/client/api/tasks.ts b/apps/web/app/services/client/api/tasks.ts index 5e34439f0..db681e9c0 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, @@ -189,6 +189,8 @@ export async function tasksTimesheetStatisticsAPI( `/timer/timesheet/statistics-tasks${employeeId ? '?employeeId=' + employeeId : ''}` ); } + + } export async function activeTaskTimesheetStatisticsAPI( @@ -200,9 +202,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, diff --git a/apps/web/lib/components/custom-select/index.tsx b/apps/web/lib/components/custom-select/index.tsx new file mode 100644 index 000000000..82d70fc7c --- /dev/null +++ b/apps/web/lib/components/custom-select/index.tsx @@ -0,0 +1,86 @@ +import { Button } from "@components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover"; +import { cn } from "lib/utils"; +import { useState } from "react"; +import { MdOutlineKeyboardArrowDown } from "react-icons/md"; + +interface SelectItemsProps { + items: T[]; + onValueChange?: (value: string) => void; + itemToString: (item: T) => string; + itemId: (item: T) => string; + triggerClassName?: string; + popoverClassName?: string; +} +/** + * + * + * @export + * @template T + * @param {SelectItemsProps} { + * items, + * onValueChange, + * itemToString, + * itemId, + * triggerClassName = '', + * popoverClassName = '' + * } + * @return {*} + */ +export function SelectItems({ + items, + onValueChange, + itemToString, + itemId, + triggerClassName = '', + popoverClassName = '' +}: SelectItemsProps) { + const [selectedItem, setSelectedItem] = useState(null); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const onClick = (item: T) => { + setSelectedItem(item); + setPopoverOpen(false); + if (onValueChange) { + onValueChange(itemId(item)); + } + } + + return ( + + + + + +
    + {items.map((item) => ( + onClick(item)} + key={itemId(item)} + className="truncate hover:cursor-pointer hover:bg-slate-50 w-full text-[13px] hover:rounded-lg p-1 hover:font-bold dark:text-white dark:hover:bg-primary" + style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }}> + {itemToString(item)} + + ))} +
    +
    +
    + ); +} diff --git a/apps/web/lib/components/emoji-picker.tsx b/apps/web/lib/components/emoji-picker.tsx index b92d8a5eb..5e1396a56 100644 --- a/apps/web/lib/components/emoji-picker.tsx +++ b/apps/web/lib/components/emoji-picker.tsx @@ -72,9 +72,8 @@ export const EmojiPicker = ({ as="div" >
    @@ -99,7 +98,7 @@ export const EmojiPicker = ({ }} className={`outline-none `} > - +
    )} diff --git a/apps/web/lib/components/index.ts b/apps/web/lib/components/index.ts index 38faad35d..7c3a6edda 100644 --- a/apps/web/lib/components/index.ts +++ b/apps/web/lib/components/index.ts @@ -22,6 +22,7 @@ export * from './color-picker'; export * from './no-data'; export * from './pagination'; export * from './time-picker' +export * from './custom-select' export * from './inputs/input'; export * from './inputs/auth-code-input'; diff --git a/apps/web/lib/components/keyboard-shortcuts.tsx b/apps/web/lib/components/keyboard-shortcuts.tsx index b11de95f4..78317dd60 100644 --- a/apps/web/lib/components/keyboard-shortcuts.tsx +++ b/apps/web/lib/components/keyboard-shortcuts.tsx @@ -40,7 +40,7 @@ export function KeyboardShortcuts() { {HostKeysMapping.map((item, index) => (

    - {t(`hotkeys.${item.heading}` as DottedLanguageObjectStringPaths)} + {t(`hotkeys.${item.heading}` as DottedLanguageObjectStringPaths)}

    {item.keySequence.map((keySeq, keySeqIndex) => ( diff --git a/apps/web/lib/components/toggler.tsx b/apps/web/lib/components/toggler.tsx index 6fe058b74..27459fe10 100644 --- a/apps/web/lib/components/toggler.tsx +++ b/apps/web/lib/components/toggler.tsx @@ -153,7 +153,7 @@ export function DataSyncModeToggler({ className }: IClassName) { className={clsxm( 'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] ml-[-2px]', dataSyncMode == 'REAL_TIME' && - 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' + 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' )} > diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 5027dd22c..db7f86514 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -13,7 +13,7 @@ import { import { IClassName, ITeamTask } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { Transition } from '@headlessui/react'; -import { Button, InputField, Tooltip, VerticalSeparator } from 'lib/components'; +import { Button, InputField, SelectItems, Tooltip, VerticalSeparator } from 'lib/components'; import { SearchNormalIcon } from 'assets/svg'; import intersection from 'lodash/intersection'; import { useCallback, useEffect, useMemo, useState, FormEvent } from 'react'; @@ -24,14 +24,15 @@ import { SettingFilterIcon } from 'assets/svg'; import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; import { Modal, Divider } from 'lib/components'; import api from '@app/services/client/axios'; -import { MdOutlineMoreTime } from "react-icons/md"; -import { IoIosTimer } from "react-icons/io"; -import { FiLoader } from "react-icons/fi"; import { DatePicker } from '@components/ui/DatePicker'; import { PencilSquareIcon } from '@heroicons/react/20/solid'; import { FaRegCalendarAlt } from "react-icons/fa"; import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; +import { format } from 'date-fns'; +import { HiMiniClock } from "react-icons/hi2"; +import '../../../styles/style.css' + export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; @@ -278,7 +279,7 @@ function InputFilters({ hook, profile }: Props) { const { activeTeam } = useOrganizationTeams(); const members = activeTeam?.members; - const [date, setDate] = useState(''); + const [date, setDate] = useState(new Date()); const [isBillable, setIsBillable] = useState(false); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); @@ -294,10 +295,9 @@ function InputFilters({ hook, profile }: Props) { useEffect(() => { const now = new Date(); - const currentDate = now.toISOString().slice(0, 10); const currentTime = now.toTimeString().slice(0, 5); - setDate(currentDate); + setDate(now); setStartTime(currentTime); setEndTime(currentTime); }, []); @@ -356,7 +356,7 @@ function InputFilters({ hook, profile }: Props) { const hours = Math.floor(diffMinutes / 60); const minutes = diffMinutes % 60; - setTimeDifference(`${hours} Hours ${minutes} Minutes`); + setTimeDifference(`${String(hours).padStart(2, '0')}h ${String(minutes).padStart(2, '0')}m`); }; useEffect(() => { @@ -389,9 +389,7 @@ function InputFilters({ hook, profile }: Props) { )} /> - - - + + + Add time + {/* Assign task combobox */} -
    - - -
    -
    - -
    - - { - date ? -
    - - {date} -
    - : ( - - )} -
    - } - selected={new Date()} - onSelect={(dateI) => { - dateI && setDate(dateI.toDateString()); - }} - mode={'single'} - /> -
    -
    -
    - -
    setIsBillable(!isBillable)} - style={isBillable ? { background: 'linear-gradient(to right, #3726a662, transparent)' } : { background: '#3726a662' }} - > -
    -
    -
    -
    -
    - - setStartTime(e.target.value)} - className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" - required - /> -
    - -
    - - setEndTime(e.target.value)} - className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" - required - /> -
    -
    - -
    - -
    - - {timeDifference} -
    -
    - -
    - - + selected={date} + onSelect={(value) => { + value && setDate(value); + }} + mode={'single'} + />
    - -
    - - +
    + +
    + +
    setIsBillable(!isBillable)} + style={isBillable ? { background: 'linear-gradient(to right, #9d91efb7, #8a7bedb7)' } : { background: '#6c57f4b7' }} + > +
    - -
    - -