diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index 7b9159c14..089f36e6d 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -128,8 +128,8 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { } await createTimesheet({ ...payload, - startedAt, - stoppedAt, + startedAt: start, + stoppedAt: end, }); }) ); @@ -182,12 +182,16 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { <span className="text-[#de5505e1] ml-1">*</span>: </label> <CustomSelect - classNameGroup='max-h-[40vh] !text-white ' + valueKey='employeeId' + classNameGroup='max-h-[40vh] dark:!text-white ' ariaLabel='Task issues' - className='w-full font-medium text-white' - options={activeTeam?.members as any} - onChange={(value: any) => updateFormState('employeeId', value.id)} - renderOption={(option: any) => ( + className='w-full font-medium dark:text-white' + options={activeTeam?.members || []} + onChange={(value) => { + console.log(value) + updateFormState('employeeId', value) + }} + renderOption={(option) => ( <div className="flex items-center gap-x-2"> <img className='h-6 w-6 rounded-full' src={option.employee.user.imageUrl} /> <span>{option.employee.fullName}</span> diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx index b7e7a9b05..57a3f0bea 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx @@ -124,7 +124,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati </div> </div> </AccordionTrigger> - <AccordionContent className="flex flex-col w-full gap-y-2"> + <AccordionContent className="flex flex-col w-full gap-y-2 "> {rows.map((task) => ( <div key={task.id} @@ -134,12 +134,13 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati }} className={cn( - 'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 h-[100px]', + 'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 ', )}> <div className="flex px-3 justify-between items-center w-full"> <div className="flex items-center gap-x-1"> <EmployeeAvatar imageUrl={task.employee.user.imageUrl ?? ''} + className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-full" /> <span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span> </div> @@ -160,9 +161,16 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati dash taskNumberClassName="text-sm" /> - <div className="flex items-center gap-2"> - {task.project && <ProjectLogo imageUrl={task.project.imageUrl as string} />} - <span className="flex-1">{task.project && task.project.name}</span> + <div className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0"> + {task.project?.imageUrl && ( + <ProjectLogo + className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]" + imageUrl={task.project.imageUrl} + /> + )} + <span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1"> + {task.project?.name ?? 'No Project'} + </span> </div> </div> ))} @@ -258,7 +266,7 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa <div className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0"> {task.project?.imageUrl && ( <ProjectLogo - className="w-[28px] h-[28px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.15)] rounded-[8px]" + className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]" imageUrl={task.project.imageUrl} /> )} diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx index 1f90136cd..1cd5ddd81 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx @@ -1,20 +1,20 @@ import { Modal, statusColor } from "@/lib/components"; -import { IoMdArrowDropdown } from "react-icons/io"; import { FaRegClock } from "react-icons/fa"; import { DatePickerFilter } from "./TimesheetFilterDate"; import { FormEvent, useCallback, useState } from "react"; import { useTranslations } from "next-intl"; import { clsxm } from "@/app/utils"; import { Item, ManageOrMemberComponent, getNestedValue } from "@/lib/features/manual-time/manage-member-component"; -import { useOrganizationProjects } from "@/app/hooks"; +import { useOrganizationProjects, useOrganizationTeams } from "@/app/hooks"; import { CustomSelect, TaskNameInfoDisplay } from "@/lib/features"; import { statusTable } from "./TimesheetAction"; import { TimesheetLog } from "@/app/interfaces"; -import { secondsToTime } from "@/app/helpers"; +import { differenceBetweenHours, formatTimeFromDate, secondsToTime, toDate } from "@/app/helpers"; import { useTimesheet } from "@/app/hooks/features/useTimesheet"; import { toast } from "@components/ui/use-toast"; import { ToastAction } from "@components/ui/toast"; import { ReloadIcon } from "@radix-ui/react-icons"; +import { addMinutes, format, parseISO } from "date-fns"; export interface IEditTaskModalProps { isOpen: boolean; @@ -23,24 +23,27 @@ export interface IEditTaskModalProps { } export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskModalProps) { const { organizationProjects } = useOrganizationProjects(); + const { activeTeam } = useOrganizationTeams(); const t = useTranslations(); const { updateTimesheet, loadingUpdateTimesheet } = useTimesheet({}) + const initialTimeRange = { + startTime: formatTimeFromDate(dataTimesheet.startedAt), + endTime: formatTimeFromDate(dataTimesheet.stoppedAt), + }; const [dateRange, setDateRange] = useState<{ date: Date | null }>({ date: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(), }); + const seconds = differenceBetweenHours(toDate(dataTimesheet.startedAt), toDate(dataTimesheet.stoppedAt)); + const { h: hours, m: minutes } = secondsToTime(seconds); - const { h: hours, m: minutes } = secondsToTime(dataTimesheet.timesheet.duration); - - const [timeRange, setTimeRange] = useState<{ startTime: string; endTime: string }>({ - startTime: dataTimesheet.timesheet?.startedAt - ? dataTimesheet.timesheet.startedAt.toString().slice(0, 5) - : '', - endTime: dataTimesheet.timesheet?.stoppedAt - ? dataTimesheet.timesheet.stoppedAt.toString().slice(0, 5) - : '', - }); + const [timeRange, setTimeRange] = useState<{ startTime: string; endTime: string }>(initialTimeRange); + /** + * Updates the start or end time in the state based on the provided key and value. + * @param {string} key - The key of the time range to update. This can be either 'startTime' or 'endTime'. + * @param {string} value - The new value for the selected time range. + */ const updateTime = (key: 'startTime' | 'endTime', value: string) => { setTimeRange(prevState => ({ ...prevState, @@ -51,10 +54,16 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo isBillable: dataTimesheet.isBillable ?? true, projectId: dataTimesheet.project?.id || '', notes: dataTimesheet.description || '', + employeeId: dataTimesheet.employeeId || '', }); const memberItemsLists = { Project: organizationProjects, }; + /** + * Updates the project id in the form state when a project is selected or deselected in the dropdown. + * @param {Object} values - An object with the selected values from the dropdown. + * @param {Item | null} values['Project'] - The selected project. + */ const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { setTimesheetData((prev) => ({ ...prev, @@ -96,10 +105,11 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo ...timeRange.endTime.split(':').map(Number) ) ); + const payload = { id: dataTimesheet.id, isBillable: timesheetData.isBillable, - employeeId: dataTimesheet.employeeId, + employeeId: timesheetData.employeeId, logType: dataTimesheet.logType, source: dataTimesheet.source, startedAt, @@ -150,6 +160,12 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo const handleFromChange = (fromDate: Date | null) => { setDateRange((prev) => ({ ...prev, date: fromDate })); }; + const getMinEndTime = (): string => { + if (!timeRange.startTime) return "00:00"; + const startDate = parseISO(`2000-01-01T${timeRange.startTime}`); + return format(addMinutes(startDate, 5), 'HH:mm'); + }; + return ( <Modal closeModal={closeModal} @@ -170,16 +186,29 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo /> <div className="flex items-center gap-x-1 "> <span className="text-gray-400">for</span> - <span className="text-primary dark:text-primary-light">{dataTimesheet.employee?.fullName ?? ""}</span> - <IoMdArrowDropdown className="cursor-pointer" /> + <CustomSelect + defaultValue={dataTimesheet.employee.fullName} + placeholder={dataTimesheet.employee.fullName} + valueKey="employeeId" + className="border border-transparent hover:border-transparent dark:hover:border-transparent" + options={activeTeam?.members || []} + value={timesheetData.employeeId} + onChange={(value) => setTimesheetData({ ...timesheetData, employeeId: value.employeeId })} + renderOption={(option) => ( + <div className="flex items-center gap-x-2"> + <img className='h-6 w-6 rounded-full' src={option.employee.user.imageUrl} alt={option.employee.fullName} /> + <span>{option.employee.fullName}</span> + </div> + )} + /> </div> </div> <div className="flex items-start flex-col justify-center gap-4"> <div> - <span className="text-[#282048] dark:text-gray-500 ">{t('dailyPlan.TASK_TIME')}</span> + <span className="text-[#282048] dark:text-gray-500 capitalize ">{t('dailyPlan.TASK_TIME')}</span> <div className="flex items-center gap-x-2 "> <FaRegClock className="text-[#30B366]" /> - <span>{hours}:{minutes} h</span> + <span>{String(hours).padStart(2, '0')}:{String(minutes).padStart(2, '0')} h</span> </div> </div> <div className="flex items-center w-full"> @@ -189,13 +218,13 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo <span className="text-[#de5505e1] ml-1">*</span> </label> <input + defaultValue={timeRange.startTime || "09:00"} aria-label="Start time" aria-describedby="start-time-error" type="time" min="00:00" max="23:59" pattern="[0-9]{2}:[0-9]{2}" - value={timeRange.startTime} onChange={(e) => updateTime("startTime", e.target.value)} className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md" required @@ -208,10 +237,11 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo <span className="text-[#de5505e1] ml-1">*</span> </label> <input + defaultValue={timeRange.endTime || "10:00"} aria-label="End time" aria-describedby="end-time-error" type="time" - value={timeRange.endTime} + min={getMinEndTime()} onChange={(e) => updateTime('endTime', e.target.value)} className="w-full p-1 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md" required diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetCard.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetCard.tsx index 55bb42912..f0e82584e 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetCard.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetCard.tsx @@ -130,10 +130,10 @@ export const TimesheetCardDetail = ({ data }: { data?: Record<TimesheetStatus, T </div> <Badge variant={'outline'} - className="flex items-center gap-x-2 h-[25px] rounded-md bg-[#E4E4E7] dark:bg-gray-800" + className="box-border flex flex-row items-center px-2 py-1 gap-2 w-[108px] h-[30px] bg-[rgba(247,247,247,0.6)] border border-gray-300 rounded-lg flex-none order-1 flex-grow-0" > - <span className="text-[#5f5f61]">{t('timer.TOTAL_HOURS')}</span> - <TotalTimeDisplay timesheetLog={rows} /> + <span className="text-[#5f5f61]">{t('timer.TOTAL_HOURS').split(' ')[0]}{':'}</span> + <TotalTimeDisplay timesheetLog={rows} className='text-[#293241] text-[14px]' /> </Badge> </div> </div> diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx index 139fdb610..33b3eeb05 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx @@ -69,6 +69,7 @@ export default TimesheetDetailModal + const MembersWorkedCard = ({ element, t }: { element: TimesheetLog[], t: TranslationHooks }) => { const memberWork = groupBy(element, (items) => items.employeeId); const memberWorkItems = Object.entries(memberWork) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index cf0f37493..9f6f9668a 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -271,7 +271,7 @@ const ViewToggleButton: React.FC<ViewToggleButtonProps> = ({ mode, active, icon, <button onClick={onClick} className={clsxm( - 'text-[#7E7991] font-medium w-[191px] h-[40px] flex items-center gap-x-4 text-[14px] px-2 rounded', + 'box-border text-[#7E7991] font-medium w-[191px] h-[76px] flex items-center gap-x-4 text-[14px] px-2 py-6', active && 'border-b-primary text-primary border-b-2 dark:text-primary-light dark:border-b-primary-light bg-[#F1F5F9] dark:bg-gray-800 font-medium' )} @@ -280,3 +280,4 @@ const ViewToggleButton: React.FC<ViewToggleButtonProps> = ({ mode, active, icon, <span>{mode === 'ListView' ? t('pages.timesheet.VIEWS.LIST') : t('pages.timesheet.VIEWS.CALENDAR')}</span> </button> ); +ViewToggleButton.displayName = 'ViewToggleButton'; diff --git a/apps/web/app/helpers/date.ts b/apps/web/app/helpers/date.ts index 922f3a5b1..71d8fc6d3 100644 --- a/apps/web/app/helpers/date.ts +++ b/apps/web/app/helpers/date.ts @@ -79,6 +79,40 @@ export function differenceBetweenHours(startedAt: Date, stoppedAt: Date): number } +/** + * Converts a given date string to a time string in the format HH:mm. + * + * This function takes an optional date string as input. If the input is not + * provided, the function returns an empty string. If the input is a valid date + * string, the function converts the string to a Date object, formats the time + * in the format HH:mm, and returns the result as a string. + * + * @param {string | undefined} dateString - The date string to format + * @returns {string} The formatted time string + */ +export const formatTimeFromDate = (date: string | Date | undefined) => { + if (!date) return ""; + const dateObject = date instanceof Date ? date : new Date(date); + const hours = dateObject.getHours().toString().padStart(2, '0'); + const minutes = dateObject.getMinutes().toString().padStart(2, '0'); + + return `${hours}:${minutes}`; +}; + +/** + * Converts a given input to a Date object. + * + * This function accepts either a Date object or a string representation of a date. + * If the input is already a Date object, it returns the input as-is. If the input + * is a string, it converts the string to a Date object and returns the result. + * + * @param {Date | string} date - The date input, which can be either a Date object or a string. + * @returns {Date} The corresponding Date object. + */ +export const toDate = (date: Date | string) => + (date instanceof Date ? date : new Date(date)); + + export function convertMsToTime(milliseconds: number) { let seconds = Math.floor(milliseconds / 1000); let minutes = Math.floor(seconds / 60); @@ -142,6 +176,7 @@ export const tomorrowDate = moment().add(1, 'days').toDate(); export const yesterdayDate = moment().subtract(1, 'days').toDate(); + export const formatDayPlanDate = (dateString: string | Date, format?: string) => { if (dateString.toString().length > 10) { dateString = dateString.toString().split('T')[0]; diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index ecbcaacda..b96b8dc67 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -167,7 +167,7 @@ export function useTimesheet({ const response = await queryUpdateTimesheet(timesheet); setTimesheet(prevTimesheet => prevTimesheet.map(item => - item.timesheet?.id === response.data.id + item.id === response.data.id ? response.data : item ) diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx index 37707b6ce..56aee673e 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx @@ -358,7 +358,7 @@ export function ProjectDropDown(props: ITaskProjectDropdownProps) { <> <Listbox.Button className={clsxm( - `cursor-pointer outline-none w-full flex + `cursor-pointer outline-none w-full flex dark:text-white items-center justify-between px-4 h-full border-solid border-color-[#F2F2F2] dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33] rounded-lg`, @@ -408,7 +408,7 @@ export function ProjectDropDown(props: ITaskProjectDropdownProps) { {organizationProjects.map((item, i) => { return ( <Listbox.Option key={item.id} value={item} as={Fragment}> - <li className="relative border h-[2rem] flex items-center gap-1 px-2 rounded-lg outline-none cursor-pointer"> + <li className="relative border h-[2rem] flex items-center gap-1 px-2 rounded-lg outline-none cursor-pointer dark:text-white"> <ProjectIcon width={14} height={14} />{' '} <span className=" truncate">{item.name}</span> </li> diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index 03f66bac9..2c73b7deb 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -224,7 +224,7 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[], </div> <div className="flex items-center gap-2 flex-1"> {task.project?.imageUrl && <ProjectLogo className='w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-[8px]' imageUrl={task.project.imageUrl} />} - <span className="font-medium">{task.project?.name}</span> + <span className="font-medium">{task.project?.name ?? "No Project"}</span> </div> <div className="flex items-center flex-1 gap-x-2"> <EmployeeAvatar diff --git a/apps/web/lib/features/multiple-select/index.tsx b/apps/web/lib/features/multiple-select/index.tsx index 5ec9af5da..166878724 100644 --- a/apps/web/lib/features/multiple-select/index.tsx +++ b/apps/web/lib/features/multiple-select/index.tsx @@ -88,47 +88,32 @@ export function MultipleSelect({ type CustomSelectProps = { - - className?: string, ariaLabel?: string defaultValue?: string classNameGroup?: string - /** - * Array of string options to be displayed in the dropdown. - * Each string represents a selectable , such as "daily" or "weekly". - */ - options: string[] | any[]; - - /** - * Optional render function that customizes the display of each option. - * Receives the option string as an argument and returns a ReactNode to render. - * If not provided, options will display with the first letter capitalized. - */ - renderOption?: (option: string) => React.ReactNode; + options: any[]; + renderOption?: (option: any) => React.ReactNode; + value?: any; + onChange?: (value: any) => void; + valueKey?: string; + placeholder?: string; }; /** - * CustomSelect Component - * - * This component renders a customizable dropdown select menu for choosing a . - * It allows passing an array of options and optionally customizes the appearance of each option. + * A customizable dropdown select menu for choosing a . * - * @component * @param {CustomSelectProps} props - Props for configuring the select component. * @param {string[]} props.options - Array of selectable options to be displayed in the dropdown. * @param {(option: string) => React.ReactNode} [props.renderOption] - Optional function for custom rendering of each option. + * @param {string} [props.ariaLabel] - Optional aria-label for the select component. + * @param {string} [props.className] - Optional class name for styling the select component. + * @param {string} [props.classNameGroup] - Optional class name for styling the select group. + * @param {string} [props.defaultValue] - Optional default value of the select component. + * @param {string} [props.value] - Optional value of the select component. + * @param {(value: string) => void} [props.onChange] - Optional function for handling the change event of the select component. * - * @example - * <CustomSelect - * options={['daily', 'weekly', 'monthly']} - * renderOption={(option) => ( - * <div className="flex items-center"> - * <span className="text-blue-500">{option.charAt(0).toUpperCase()}</span> - * <span className="ml-1">{option.slice(1)}</span> - * </div> - * )} - * /> + * @returns {React.ReactNode} A React component representing the customizable dropdown select menu. */ export function CustomSelect({ options, @@ -138,31 +123,34 @@ export function CustomSelect({ value, onChange, defaultValue, - classNameGroup -}: CustomSelectProps & { - value?: string, - onChange?: (value: string) => void -}) { - // Render the select component with dynamic options and optional custom rendering. + classNameGroup, + valueKey = 'id', + placeholder = "Select an option" +}: CustomSelectProps) { return ( <Select defaultValue={defaultValue} value={value} onValueChange={onChange} - aria-label={ariaLabel || "Select an option"} > - + aria-label={ariaLabel || "Select an option"}> <SelectTrigger - className={`overflow-hidden text-clip border border-gray-200 dark:border-gray-700 bg-white dark:bg-dark--theme-light focus:ring-2 focus:ring-transparent ${className}`} + className={`overflow-hidden text-clip bg-white dark:bg-dark--theme-light focus:ring-0 ${className}`} > - <SelectValue placeholder="Select an option" /> + <SelectValue placeholder={placeholder} /> </SelectTrigger> <SelectContent className='z-[10000] dark:bg-dark--theme-light w-auto'> <SelectGroup className={clsxm('overflow-y-auto', classNameGroup)}> - {options.map((value, index) => ( - <SelectItem key={index} value={value}> - {renderOption ? renderOption(value) : value.charAt(0).toUpperCase() + value.slice(1)} - </SelectItem> - ))} + {options?.map((option, index) => { + const optionValue = typeof option === 'object' ? option[valueKey] : option; + return ( + <SelectItem + key={optionValue || index} + value={optionValue} + > + {renderOption ? renderOption(option) : (option.label || option.name || option.toString())} + </SelectItem> + ); + })} </SelectGroup> </SelectContent> </Select> diff --git a/apps/web/lib/features/task/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index 48a6e748b..18c37ed84 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -12,12 +12,12 @@ import { clsxm } from '@app/utils'; import { HorizontalSeparator, AlertPopup } from 'lib/components'; import { useEffect, useState } from 'react'; import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; -import { IDailyPlan } from '@app/interfaces'; +import { IDailyPlan, IUser } from '@app/interfaces'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; import DailyPlanTasksTableView from './table-view'; -export function FutureTasks({ profile }: { profile: any }) { +export function FutureTasks({ profile, user }: { profile: any; user?: IUser }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); const canSeeActivity = useCanSeeActivityScreen(); const [popupOpen, setPopupOpen] = useState(false); @@ -30,6 +30,22 @@ export function FutureTasks({ profile }: { profile: any }) { }, [date, setDate, futurePlans]); const view = useAtomValue(dailyPlanViewHeaderTabs); + useEffect(() => { + let filteredData = futurePlans; + + // Filter tasks for specific user if provided + if (user) { + filteredData = filteredData + .map((plan) => ({ + ...plan, + tasks: plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + })) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + + setFutureDailyPlanTasks(filterDailyPlan(date as any, filteredData)); + } + }, [date, futurePlans, user]); + return ( <div className="flex flex-col gap-6"> {futureDailyPlanTasks?.length > 0 ? ( 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 406d1eca7..b0ba77c32 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -8,25 +8,32 @@ import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; import { DragDropContext, Draggable, Droppable, DroppableProvided } from 'react-beautiful-dnd'; import { useState } from 'react'; -import { ITeamTask } from '@app/interfaces'; +import { ITeamTask, IUser } from '@app/interfaces'; import { handleDragAndDropDailyOutstandingAll } from '@app/helpers'; interface OutstandingAll { profile: any; + user?: IUser; } -export function OutstandingAll({ profile }: OutstandingAll) { +export function OutstandingAll({ profile, user }: OutstandingAll) { const { outstandingPlans } = useDailyPlan(); const view = useAtomValue(dailyPlanViewHeaderTabs); const displayedTaskId = new Set(); - const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); - const [task, setTask] = useState<ITeamTask[]>(() => tasks ?? []); + const tasks = outstandingPlans.flatMap( + (plan) => + (user + ? plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + : plan.tasks) ?? [] + ); + + const [task, setTask] = useState<ITeamTask[]>(() => tasks); return ( <div className="flex flex-col gap-6"> <TaskEstimatedCount outstandingPlans={outstandingPlans} /> - {tasks && tasks?.length > 0 ? ( + {tasks.length > 0 ? ( <> <DragDropContext onDragEnd={(result) => handleDragAndDropDailyOutstandingAll(result, task, setTask)} diff --git a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx index b0b6097bf..92524578f 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -9,28 +9,45 @@ import { clsxm } from '@app/utils'; import { useAtomValue } from 'jotai'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; -import { useState } from 'react'; -import { IDailyPlan } from '@app/interfaces'; +import { useEffect, useState } from 'react'; +import { IDailyPlan, IUser } from '@app/interfaces'; interface IOutstandingFilterDate { profile: any; + user?: IUser; } -export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) { +export function OutstandingFilterDate({ profile, user }: IOutstandingFilterDate) { const { outstandingPlans } = useDailyPlan(); const view = useAtomValue(dailyPlanViewHeaderTabs); - const [outstandingTasks, setOutstandingTasks] = useState<IDailyPlan[]>(outstandingPlans); + const [plans, setPlans] = useState<IDailyPlan[]>(outstandingPlans); + + useEffect(() => { + let data = [...outstandingPlans]; + + // Filter plans for specific user if provided + if (user) { + data = data + .map((plan) => ({ + ...plan, + tasks: plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + })) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + + setPlans(data); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user]); + return ( <div className="flex flex-col gap-6"> - {outstandingTasks?.length > 0 ? ( - <DragDropContext - onDragEnd={(result) => handleDragAndDrop(result, outstandingTasks, setOutstandingTasks)} - > + {plans?.length > 0 ? ( + <DragDropContext onDragEnd={(result) => handleDragAndDrop(result, plans, setPlans)}> <Accordion type="multiple" className="text-sm" - defaultValue={outstandingTasks?.map((plan) => new Date(plan.date).toISOString().split('T')[0])} + defaultValue={plans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])} > - {outstandingTasks?.map((plan) => ( + {plans?.map((plan) => ( <AccordionItem value={plan.date.toString().split('T')[0]} key={plan.id} 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 6773c1db5..969628eec 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -10,32 +10,56 @@ import { clsxm } from '@app/utils'; import TaskBlockCard from '../task-block-card'; import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; import { useEffect, useState } from 'react'; -import { IDailyPlan } from '@app/interfaces'; +import { IDailyPlan, IUser } from '@app/interfaces'; import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; import { useDateRange } from '@app/hooks/useDateRange'; import DailyPlanTasksTableView from './table-view'; -export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { - const { pastPlans } = useDailyPlan(); +export function PastTasks({ + user, + profile, + currentTab = 'Past Tasks' +}: { + profile: any; + currentTab?: FilterTabs; + user?: IUser; +}) { + const { pastPlans: _pastPlans } = useDailyPlan(); const view = useAtomValue(dailyPlanViewHeaderTabs); - const [pastTasks, setPastTasks] = useState<IDailyPlan[]>(pastPlans); + const [pastPlans, setPastPlans] = useState<IDailyPlan[]>(_pastPlans); const { date } = useDateRange(window.localStorage.getItem('daily-plan-tab')); useEffect(() => { - setPastTasks(filterDailyPlan(date as any, pastPlans)); + setPastPlans(filterDailyPlan(date as any, pastPlans)); }, [date, pastPlans]); + useEffect(() => { + let filteredData = pastPlans; + + // Filter tasks for specific user if provided + if (user) { + filteredData = filteredData + .map((plan) => ({ + ...plan, + tasks: plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + })) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + } + + setPastPlans(filteredData); + }, [date, pastPlans, user]); + return ( <div className="flex flex-col gap-6"> - {pastTasks?.length > 0 ? ( - <DragDropContext onDragEnd={(result) => handleDragAndDrop(result, pastPlans, setPastTasks)}> + {pastPlans?.length > 0 ? ( + <DragDropContext onDragEnd={(result) => handleDragAndDrop(result, pastPlans, setPastPlans)}> <Accordion type="multiple" className="text-sm" defaultValue={[yesterdayDate.toISOString().split('T')[0]]} > - {pastTasks?.map((plan) => ( + {pastPlans?.map((plan) => ( <AccordionItem value={plan.date.toString().split('T')[0]} key={plan.id} 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 37940098e..d5209fff2 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 @@ -1,5 +1,5 @@ import { secondsToTime } from '@app/helpers'; -import { IDailyPlan } from '@app/interfaces'; +import { IDailyPlan, IUser } from '@app/interfaces'; import { VerticalSeparator } from 'lib/components'; import { useTranslations } from 'next-intl'; @@ -47,15 +47,18 @@ export function estimatedTotalTime(data: any) { return { timesEstimated, totalTasks }; } -export const getTotalTasks = (plan: IDailyPlan[]) => { - if (!plan) { +export const getTotalTasks = (plans: IDailyPlan[], user?: IUser): number => { + if (!plans || plans.length === 0) { return 0; } - const tasksPerPlan = plan.map((plan) => plan.tasks?.length); - if (tasksPerPlan.length <= 0) { - return 0; - } + const tasksPerPlan = plans.map((plan) => { + const filteredTasks = user + ? plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + : plan.tasks; + + return filteredTasks?.length || 0; + }); - return tasksPerPlan.reduce((a, b) => (a ?? 0) + (b ?? 0)) ?? 0; + return tasksPerPlan.reduce((total, taskCount) => total + taskCount, 0); }; diff --git a/apps/web/lib/features/team/user-team-card/index.tsx b/apps/web/lib/features/team/user-team-card/index.tsx index bf5ab103d..787d6ee9d 100644 --- a/apps/web/lib/features/team/user-team-card/index.tsx +++ b/apps/web/lib/features/team/user-team-card/index.tsx @@ -126,7 +126,7 @@ export function UserTeamCard({ const [activityFilter, setActivity] = useState<FilterTab>('Tasks'); const activityScreens = { - Tasks: <UserProfileTask profile={profile} tabFiltered={hook} />, + Tasks: <UserProfileTask profile={profile} tabFiltered={hook} user={member?.employee.user} />, Screenshots: <ScreenshootTab />, Apps: <AppsTab />, 'Visited Sites': <VisitedSitesTab /> diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index 87c427328..b0471f39a 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -26,7 +26,7 @@ import { HAS_SEEN_DAILY_PLAN_SUGGESTION_MODAL, HAS_VISITED_OUTSTANDING_TASKS } from '@app/constants'; -import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import { IDailyPlan, ITeamTask, IUser } from '@app/interfaces'; import { dataDailyPlanState } from '@app/stores'; import { fullWidthState } from '@app/stores/fullWidth'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; @@ -55,9 +55,15 @@ import DailyPlanTasksTableView from './task/daily-plan/table-view'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; -export function UserProfilePlans() { +interface IUserProfilePlansProps { + user?: IUser; +} + +export function UserProfilePlans(props: IUserProfilePlansProps) { const t = useTranslations(); + const { user } = props; + const profile = useUserProfilePage(); const { todayPlan, @@ -76,14 +82,14 @@ export function UserProfilePlans() { const { setDate, date } = useDateRange(currentTab); const screenOutstanding = { - ALL: <OutstandingAll profile={profile} />, - DATE: <OutstandingFilterDate profile={profile} /> + ALL: <OutstandingAll profile={profile} user={user} />, + DATE: <OutstandingFilterDate profile={profile} user={user} /> }; const tabsScreens = { - 'Today Tasks': <AllPlans profile={profile} currentTab={currentTab} />, - 'Future Tasks': <FutureTasks profile={profile} />, - 'Past Tasks': <PastTasks profile={profile} />, - 'All Tasks': <AllPlans profile={profile} />, + 'Today Tasks': <AllPlans profile={profile} currentTab={currentTab} user={user} />, + 'Future Tasks': <FutureTasks profile={profile} user={user} />, + 'Past Tasks': <PastTasks profile={profile} user={user} />, + 'All Tasks': <AllPlans profile={profile} user={user} />, Outstanding: <Outstanding filter={screenOutstanding[currentOutstanding]} /> }; const [filterFuturePlanData, setFilterFuturePlanData] = useState<IDailyPlan[]>(futurePlans); @@ -169,15 +175,24 @@ export function UserProfilePlans() { currentTab == filter && 'dark:bg-gray-600' )} > - {filter === 'Today Tasks' && getTotalTasks(todayPlan)} - {filter === 'Future Tasks' && getTotalTasks(filterFuturePlanData)} - {filter === 'Past Tasks' && getTotalTasks(filterPastPlanData)} - {filter === 'All Tasks' && getTotalTasks(filterAllPlanData)} + {filter === 'Today Tasks' && getTotalTasks(todayPlan, user)} + {filter === 'Future Tasks' && + getTotalTasks(filterFuturePlanData, user)} + {filter === 'Past Tasks' && getTotalTasks(filterPastPlanData, user)} + {filter === 'All Tasks' && getTotalTasks(filterAllPlanData, user)} {filter === 'Outstanding' && estimatedTotalTime( - outstandingPlans.map((plan) => - plan.tasks?.map((task) => task) - ) + outstandingPlans.map((plan) => { + const tasks = plan.tasks ?? []; + if (user) { + return tasks.filter((task) => + task.members?.some( + (member) => member.userId === user.id + ) + ); + } + return tasks; + }) ).totalTasks} </span> </div> @@ -285,7 +300,15 @@ export function UserProfilePlans() { * @param {{ profile: any; currentTab?: FilterTabs }} { profile, currentTab = 'All Tasks' } * @return {*} */ -function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; currentTab?: FilterTabs }) { +function AllPlans({ + profile, + currentTab = 'All Tasks', + user +}: { + profile: any; + currentTab?: FilterTabs; + user?: IUser; +}) { // Filter plans const filteredPlans = useRef<IDailyPlan[]>([]); const { sortedPlans, todayPlan } = useDailyPlan(); @@ -305,6 +328,22 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current setPlans(filterDailyPlan(date as any, filteredPlans.current)); }, [date, todayPlan]); + useEffect(() => { + let filteredData = filterDailyPlan(date as any, filteredPlans.current); + + // Filter tasks for specific user if provided + if (user) { + filteredData = filteredData + .map((plan) => ({ + ...plan, + tasks: plan.tasks?.filter((task) => task.members?.some((member) => member.userId === user.id)) + })) + .filter((plan) => plan.tasks && plan.tasks.length > 0); + } + + setPlans(filteredData); + }, [date, todayPlan, user]); + return ( <div className="flex flex-col gap-6"> {Array.isArray(plans) && plans?.length > 0 ? ( diff --git a/apps/web/lib/features/user-profile-tasks.tsx b/apps/web/lib/features/user-profile-tasks.tsx index 46b02dfa4..663d49138 100644 --- a/apps/web/lib/features/user-profile-tasks.tsx +++ b/apps/web/lib/features/user-profile-tasks.tsx @@ -8,11 +8,13 @@ import { useEffect, useMemo, useState } from 'react'; import { ScreenCalendar } from './activity/screen-calendar'; import { cn } from 'lib/utils'; import { useScrollPagination } from '@app/hooks/features/usePagination'; +import { IUser } from '@/app/interfaces'; type Props = { tabFiltered: I_TaskFilter; profile: I_UserProfilePage; paginateTasks?: boolean; + user?: IUser; }; /** @@ -20,7 +22,7 @@ type Props = { * @param - `profile` - The user profile page data. * @returns A component that displays a user's profile page. */ -export function UserProfileTask({ profile, paginateTasks, tabFiltered }: Props) { +export function UserProfileTask({ profile, paginateTasks, tabFiltered, user }: Props) { const [scrollableContainer, setScrollableContainer] = useState<HTMLDivElement | null>(null); const t = useTranslations(); @@ -93,7 +95,7 @@ export function UserProfileTask({ profile, paginateTasks, tabFiltered }: Props) /> )} {tabFiltered.tab === 'stats' && <ScreenCalendar />} - {tabFiltered.tab === 'dailyplan' && <UserProfilePlans />} + {tabFiltered.tab === 'dailyplan' && <UserProfilePlans user={user} />} {tabFiltered.tab === 'worked' && otherTasks.length > 0 && ( <div className="flex items-center my-6 space-x-2">