diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index b3586b402..c8a561547 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -155,9 +155,21 @@ export function useDailyPlan() { const updatedEmployee = employeePlans.filter((plan) => plan.id != planId); setProfileDailyPlans({ total: profileDailyPlans.total, items: [...updated, res.data] }); setEmployeePlans([...updatedEmployee, res.data]); + // Fetch updated plans + getMyDailyPlans(); + getAllDayPlans(); return res; }, - [employeePlans, profileDailyPlans, setEmployeePlans, setProfileDailyPlans, updateQueryCall] + [ + employeePlans, + getAllDayPlans, + getMyDailyPlans, + profileDailyPlans.items, + profileDailyPlans.total, + setEmployeePlans, + setProfileDailyPlans, + updateQueryCall + ] ); const addTaskToPlan = useCallback( diff --git a/apps/web/components/ui/calendar.tsx b/apps/web/components/ui/calendar.tsx index ac069e2b3..c955522ff 100644 --- a/apps/web/components/ui/calendar.tsx +++ b/apps/web/components/ui/calendar.tsx @@ -6,6 +6,7 @@ import { cn } from 'lib/utils'; import { buttonVariants } from 'components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; import { ScrollArea } from './scroll-bar'; +import { memo, useCallback, useMemo } from 'react'; export type CalendarProps = React.ComponentProps; @@ -44,17 +45,21 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C ...classNames }} components={{ - Dropdown: ({ value, onChange, children, ...props }: DropdownProps) => { - const options = React.Children.toArray(children) as React.ReactElement< - React.HTMLProps - >[]; - const selected = options.find((child) => child.props.value === value); - const handleChange = (value: string) => { + Dropdown: memo(function Dropdown({ value, onChange, children, ...props }: DropdownProps) { + const options = useMemo( + () => + React.Children.toArray(children) as React.ReactElement< + React.HTMLProps + >[], + [] + ); + const selected = useMemo(() => options.find((child) => child.props.value === value), []); + const handleChange = useCallback((value: string) => { const changeEvent = { target: { value } } as React.ChangeEvent; onChange?.(changeEvent); - }; + }, []); return ( ); - }, + }), IconLeft, IconRight }} diff --git a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx index dcd2761b5..60f7c6c99 100644 --- a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx +++ b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx @@ -21,15 +21,28 @@ import { Popover, Transition } from '@headlessui/react'; import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; import { Cross2Icon } from '@radix-ui/react-icons'; +/** + * A modal that allows user to add task estimation / planned work time, etc. + * + * @param {Object} props - The props Object + * @param {boolean} props.open - If true open the modal otherwise close the modal + * @param {() => void} props.closeModal - A function to close the modal + * @param {IDailyPlan} props.plan - The selected plan + * @param {ITeamTask[]} props.tasks - The list of planned tasks + * @param {boolean} props.isRenderedInSoftFlow - If true use the soft flow logic. + * + * @returns {JSX.Element} The modal element + */ interface IAddTasksEstimationHoursModalProps { closeModal: () => void; isOpen: boolean; plan: IDailyPlan; tasks: ITeamTask[]; + isRenderedInSoftFlow?: boolean; } export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModalProps) { - const { isOpen, closeModal, plan, tasks } = props; + const { isOpen, closeModal, plan, tasks, isRenderedInSoftFlow = true } = props; const { isOpen: isActiveTaskHandlerModalOpen, closeModal: closeActiveTaskHandlerModal, @@ -41,7 +54,7 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa const { startTimer } = useTimerView(); const { activeTeam, activeTeamTask, setActiveTask } = useTeamTasks(); const [showSearchInput, setShowSearchInput] = useState(false); - const [workTimePlanned, setworkTimePlanned] = useState(plan.workTimePlanned); + const [workTimePlanned, setWorkTimePlanned] = useState(plan.workTimePlanned); const currentDate = useMemo(() => new Date().toISOString().split('T')[0], []); const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); const tasksEstimationTimes = useMemo(() => estimatedTotalTime(plan.tasks).timesEstimated / 3600, [plan.tasks]); @@ -53,32 +66,57 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa [activeTeamTask?.id, plan.tasks] ); + const canStartWorking = useMemo(() => { + const isTodayPlan = + new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); + + return isTodayPlan; + // Can add others conditions + }, [plan.date]); + const handleCloseModal = useCallback(() => { - localStorage.setItem(TASKS_ESTIMATE_HOURS_MODAL_DATE, currentDate); + if (canStartWorking) { + localStorage.setItem(TASKS_ESTIMATE_HOURS_MODAL_DATE, currentDate); + } closeModal(); - }, [closeModal, currentDate]); + }, [canStartWorking, closeModal, currentDate]); /** * The function that close the Planned tasks modal when the user ignores the modal (Today's plan) */ const closeModalAndStartTimer = useCallback(() => { handleCloseModal(); - startTimer(); - }, [handleCloseModal, startTimer]); + if (canStartWorking) { + startTimer(); + } + }, [canStartWorking, handleCloseModal, startTimer]); /** * The function that opens the Change task modal if conditions are met (or start the timer) */ - const handleChangActiveTask = useCallback(() => { + const handleChangeActiveTask = useCallback(() => { if (isActiveTaskPlanned) { if (defaultTask?.id !== activeTeamTask?.id) { setActiveTask(defaultTask); } + + if (!isRenderedInSoftFlow) { + handleCloseModal(); + } startTimer(); } else { openActiveTaskHandlerModal(); } - }, [activeTeamTask?.id, defaultTask, isActiveTaskPlanned, openActiveTaskHandlerModal, setActiveTask, startTimer]); + }, [ + activeTeamTask?.id, + defaultTask, + handleCloseModal, + isActiveTaskPlanned, + openActiveTaskHandlerModal, + isRenderedInSoftFlow, + setActiveTask, + startTimer + ]); /** * The function which is called when the user clicks on the 'Start working' button @@ -90,14 +128,28 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa // Update the plan work time only if the user changed it plan.workTimePlanned !== workTimePlanned && (await updateDailyPlan({ workTimePlanned }, plan.id ?? '')); - handleChangActiveTask(); - handleCloseModal(); + if (canStartWorking) { + handleChangeActiveTask(); + + if (isRenderedInSoftFlow) { + handleCloseModal(); + } + } } catch (error) { console.log(error); } finally { setLoading(false); } - }, [handleChangActiveTask, handleCloseModal, plan.id, plan.workTimePlanned, updateDailyPlan, workTimePlanned]); + }, [ + canStartWorking, + handleChangeActiveTask, + handleCloseModal, + plan.id, + plan.workTimePlanned, + isRenderedInSoftFlow, + updateDailyPlan, + workTimePlanned + ]); /** * The function that handles warning messages for the @@ -165,131 +217,152 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, tasks]); - return ( - <> - - -
-
- - {t('timer.todayPlanSettings.TITLE')} - - {showSearchInput ? ( - - ) : ( -
- - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')}{' '} - * - -
- setworkTimePlanned(parseFloat(e.target.value))} - required - noWrapper - min={0} - value={workTimePlanned} - defaultValue={plan.workTimePlanned ?? 0} - /> - -
-
- )} + // Update the working planned time + useEffect(() => { + setWorkTimePlanned(plan.workTimePlanned); + }, [plan]); + + const content = ( +
+
+ {isRenderedInSoftFlow && ( + + {t('timer.todayPlanSettings.TITLE')} + + )} -
-
-
-
-
- {t('task.TITLE_PLURAL')} - * -
-
- {t('dailyPlan.TOTAL_ESTIMATED')} : - - {formatIntegerToHour(tasksEstimationTimes)} - -
-
-
- -
    - {sortedTasks.map((task, index) => ( - - ))} -
- -
-
-
-
- {warning && ( - <> - - {warning} - - )} -
+ {showSearchInput ? ( + + ) : ( +
+ + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * + +
+ setWorkTimePlanned(parseFloat(e.target.value))} + required + noWrapper + min={0} + value={workTimePlanned} + defaultValue={plan.workTimePlanned ?? 0} + /> + +
+
+ )} + +
+
+
+
+
+ {t('task.TITLE_PLURAL')} + * +
+
+ {t('dailyPlan.TOTAL_ESTIMATED')} : + {formatIntegerToHour(tasksEstimationTimes)}
-
- - +
+ +
    + {sortedTasks.map((task, index) => ( + + ))} +
+ +
+
+ {warning && ( + <> + + {warning} + + )} +
- - +
+
+ + +
+
+
+ ); + + return ( + <> + {isRenderedInSoftFlow ? ( + + + {content} + + + ) : ( + content + )} + {defaultTask && ( { + if (!isRenderedInSoftFlow) { + handleCloseModal(); + } + closeActiveTaskHandlerModal(); + }} /> )} @@ -383,7 +456,7 @@ function SearchTaskInput(props: ISearchTaskInputProps) { return ( -
+
Select or create task for the plan
- + {tasks.length ? ( - +
    {tasks.map((task, index) => ( @@ -472,6 +545,8 @@ function TaskCard(props: ITaskCardProps) { const { getTaskById } = useTeamTasks(); const { addTaskToPlan } = useDailyPlan(); const [addToPlanLoading, setAddToPlanLoading] = useState(false); + const isTaskRenderedInTodayPlan = + new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); const { isOpen: isTaskDetailsModalOpen, closeModal: closeTaskDetailsModal, @@ -506,13 +581,15 @@ function TaskCard(props: ITaskCardProps) { shadow="custom" className={clsx( 'lg:flex items-center justify-between py-3 md:px-4 hidden min-h-[4.5rem] w-full h-[4.5rem] dark:bg-[#1E2025] border-[0.05rem] dark:border-[#FFFFFF0D] relative !text-xs cursor-pointer', - isDefaultTask && 'border-primary-light border-[0.15rem]' + isTaskRenderedInTodayPlan && isDefaultTask && 'border-primary-light border-[0.15rem]' )} >
    { - setDefaultTask(task); - window && window.localStorage.setItem(DEFAULT_PLANNED_TASK_ID, task.id); + if (isTaskRenderedInTodayPlan) { + setDefaultTask(task); + window && window.localStorage.setItem(DEFAULT_PLANNED_TASK_ID, task.id); + } }} className="min-w-[48%] flex items-center h-full max-w-[50%]" > diff --git a/apps/web/lib/features/daily-plan/all-plans-modal.tsx b/apps/web/lib/features/daily-plan/all-plans-modal.tsx new file mode 100644 index 000000000..55e648054 --- /dev/null +++ b/apps/web/lib/features/daily-plan/all-plans-modal.tsx @@ -0,0 +1,283 @@ +import { Card, Modal, NoData, Tooltip, VerticalSeparator } from 'lib/components'; +import { Dispatch, memo, SetStateAction, useCallback, useMemo, useState } from 'react'; +import { clsxm } from '@app/utils'; +import { Text } from 'lib/components'; +import { ChevronRightIcon } from 'assets/svg'; +import { AddTasksEstimationHoursModal } from './add-task-estimation-hours-modal'; +import { useDailyPlan } from '@app/hooks'; +import { Button } from '@components/ui/button'; +import { Calendar } from '@components/ui/calendar'; +import { IDailyPlan } from '@app/interfaces'; +import moment from 'moment'; +import { ValueNoneIcon } from '@radix-ui/react-icons'; + +interface IAllPlansModal { + closeModal: () => void; + isOpen: boolean; +} + +/** + * A modal that displays all the plans available to the user (Today, Tomorrow and Future). + * + * + * @param {Object} props - The props Object + * @param {boolean} props.open - If true open the modal otherwise close the modal + * @param {() => void} props.closeModal - A function to close the modal + * + * @returns {JSX.Element} The modal element + */ +export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) { + const { isOpen, closeModal } = props; + const [showCalendar, setShowCalendar] = useState(false); + const [showCustomPlan, setShowCustomPlan] = useState(false); + const [customDate, setCustomDate] = useState(); + const { futurePlans, myDailyPlans } = useDailyPlan(); + + // Utility function for checking if two dates are the same + const isSameDate = useCallback( + (date1: Date, date2: Date) => + new Date(date1).toLocaleDateString('en') === new Date(date2).toLocaleDateString('en'), + [] + ); + + // Memoize today, tomorrow, and future plans + const todayPlan = useMemo( + () => myDailyPlans.items.find((plan) => isSameDate(plan.date, moment().toDate())), + [isSameDate, myDailyPlans.items] + ); + + const tomorrowPlan = useMemo( + () => myDailyPlans.items.find((plan) => isSameDate(plan.date, moment().add(1, 'days').toDate())), + [isSameDate, myDailyPlans.items] + ); + + const selectedFuturePlan = useMemo( + () => customDate && myDailyPlans.items.find((plan) => isSameDate(plan.date, customDate)), + [isSameDate, myDailyPlans.items, customDate] + ); + + // Handle modal close + const handleCloseModal = useCallback(() => { + closeModal(); + }, [closeModal]); + + // Define tabs for plan selection + const tabs = useMemo(() => ['Today', 'Tomorrow', 'Future'], []); + + // State to track the active tab + const [selectedTab, setSelectedTab] = useState(tabs[0]); + + // Handle tab switching + const handleTabClick = (tab: string) => { + setSelectedTab(tab); + setShowCalendar(tab === 'Future'); + setShowCustomPlan(false); + }; + + // Determine which plan to display based on the selected tab + const plan = useMemo(() => { + switch (selectedTab) { + case 'Today': + return todayPlan; + case 'Tomorrow': + return tomorrowPlan; + case 'Future': + return selectedFuturePlan; + default: + return undefined; + } + }, [selectedTab, todayPlan, tomorrowPlan, selectedFuturePlan]); + return ( + + +
    +
    + {selectedTab === 'Future' && showCustomPlan && ( + + + + )} + + + {selectedTab == 'Future' + ? showCustomPlan && selectedFuturePlan + ? `PLAN FOR ${new Date(selectedFuturePlan.date).toLocaleDateString('en-GB')}` + : `${selectedTab} PLAN` + : `${selectedTab}'S PLAN`} + +
    +
    +
      + {tabs.map((tab, index) => ( +
    • handleTabClick(tab)} + > + {tab} + {index + 1 < tabs.length && } +
    • + ))} +
    +
    + +
    + {selectedTab === 'Future' && showCalendar ? ( +
    +
    +
    +

    Select a date to be able to see a plan

    +
    + +
    +
    +
    + +
    + + +
    +
    + ) : ( + <> + {plan ? ( + + ) : ( + } text="Plan not found " /> + )} + + )} +
    +
    +
    +
    + ); +}); + +/** + * ------------------------------------------------------- + * ----------------- Calendar ------------------ + * ------------------------------------------------------- + */ + +interface ICalendarProps { + setSelectedFuturePlan: Dispatch>; + selectedFuturePlan: Date | undefined; + futurePlans: IDailyPlan[]; +} + +/** + * The component tha handles the selection of a future plan + * + * @param {Object} props - The props object + * @param {Dispatch>} props.setSelectedFuturePlan - A function that set the selected plan + * @param {IDailyPlan} props.selectedFuturePlan - The selected plan + */ +const FuturePlansCalendar = memo(function FuturePlansCalendar(props: ICalendarProps) { + const { futurePlans, selectedFuturePlan, setSelectedFuturePlan } = props; + + const sortedFuturePlans = useMemo( + () => + futurePlans + .filter( + (plan) => + new Date(plan.date).toLocaleDateString('en') !== + new Date(moment().add(1, 'days').toDate()).toLocaleDateString('en') + ) + .sort((plan1, plan2) => (new Date(plan1.date).getTime() < new Date(plan2.date).getTime() ? 1 : -1)), + [futurePlans] + ); + + /** + * A helper function that checks if a given date has not a plan + * + * @param {Date} date - The date to check + * + * @returns {boolean} true if the date has a plan, false otherwise + */ + const isDateUnplanned = useCallback( + (dateToCheck: Date) => { + return !futurePlans + // Start from the day after tomorrow (Tomorrow has a tab) + .filter( + (plan) => + new Date(plan.date).toLocaleDateString('en') !== + new Date(moment().add(1, 'days').toDate()).toLocaleDateString('en') + ) + .map((plan) => new Date(plan.date)) + .some( + (date) => new Date(date).toLocaleDateString('en') == new Date(dateToCheck).toLocaleDateString('en') + ); + }, + [futurePlans] + ); + + return ( + { + if (date) { + setSelectedFuturePlan(date); + } + }} + initialFocus + disabled={isDateUnplanned} + modifiers={{ + booked: sortedFuturePlans?.map((plan) => new Date(plan.date)) + }} + modifiersClassNames={{ + booked: clsxm( + 'relative after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 after:w-1.5 after:h-1.5 after:bg-primary after:rounded-full' + ), + selected: clsxm('bg-primary text-white !rounded-full') + }} + fromMonth={new Date(sortedFuturePlans?.[0]?.date ?? Date.now())} + toMonth={new Date(sortedFuturePlans?.[sortedFuturePlans?.length - 1]?.date ?? Date.now())} + fromYear={new Date(sortedFuturePlans?.[0]?.date ?? Date.now())?.getFullYear()} + toYear={new Date(sortedFuturePlans?.[sortedFuturePlans?.length - 1]?.date ?? Date.now())?.getFullYear()} + /> + ); +}); + +FuturePlansCalendar.displayName = 'FuturePlansCalendar'; diff --git a/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx b/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx index 90dbe3380..0e331efcd 100644 --- a/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx +++ b/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx @@ -1,5 +1,5 @@ import { mergeRefs } from '@app/helpers'; -import { I_TeamMemberCardHook, I_TMCardTaskEditHook } from '@app/hooks'; +import { I_TeamMemberCardHook, I_TMCardTaskEditHook, useModal } from '@app/hooks'; import { IClassName, ITeamTask } from '@app/interfaces'; import { clsxm } from '@app/utils'; import { Popover, Transition } from '@headlessui/react'; @@ -8,6 +8,7 @@ import { TaskUnOrAssignPopover } from 'lib/features/task/task-assign-popover'; import { useCallback } from 'react'; import { useTranslations } from 'next-intl'; import { ThreeCircleOutlineVerticalIcon } from 'assets/svg'; +import { AllPlansModal } from 'lib/features/daily-plan/all-plans-modal'; type Props = IClassName & { memberInfo: I_TeamMemberCardHook; @@ -27,6 +28,8 @@ function DropdownMenu({ edition, memberInfo }: Props) { const t = useTranslations(); const loading = edition.loading || memberInfo.updateOTeamLoading; + const { isOpen: isAllPlansModalOpen, closeModal: closeAllPlansModal, openModal: openAllPlansModal } = useModal(); + const menu = [ { name: t('common.EDIT_TASK'), @@ -76,129 +79,133 @@ function DropdownMenu({ edition, memberInfo }: Props) { ].filter((item) => item.active || item.active === undefined); return ( - - {!loading && ( - - - - )} - {loading && } - - + - - {({ close }) => { - return ( - -
      - {menu.map((item, i) => { - const text = ( - + + + )} + {loading && } + + + + {({ close }) => { + return ( + +
        + {menu.map((item, i) => { + const text = ( + + {item.name} + + ); + + // When true show combobox component (AssignActionMenu) + const assignAction = item.action === 'assign'; + + const removeAction = item.action === 'remove'; + + return ( +
      • + {assignAction && ( + // Show only for item with combobox menu + { + // Can close all open combobox + item.onClick && + item.onClick({ + task, + closeCombobox1: closeCmbx, + closeCombobox2: close + }); + }} + userProfile={memberInfo.member} + usersTaskCreatedAssignTo={ + memberInfo.member?.employeeId + ? [{ id: memberInfo.member?.employeeId }] + : undefined + } + > + {text} + + )} + + {removeAction && ( + { + item.onClick && item.onClick({ close }); + }} + > + {text} + + )} + + {/* WHen hasn't an action */} + {!assignAction && !removeAction && ( + + )} +
      • + ); + })} + +
          + - )} - - ); - })} - -
            - + See Plan + +
        -
      -
      - ); - }} -
      -
      - + + ); + }} + + + + + ); }