From 13e84030172dcd1d2dea49f2bfc45f0fd57b1f58 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 26 Dec 2024 09:25:16 +0200 Subject: [PATCH] [Feat]: Timesheet Improvements (#3480) * fix(components): enhance CustomSelect component functionality and styling * fix: correct time display format from AM/PM to 24h * fix: correct time display format from AM/PM to 24h * fix: deep scan * fix: deep scan --- .../[memberId]/components/AddTaskModal.tsx | 18 +++-- .../[memberId]/components/CalendarView.tsx | 20 +++-- .../[memberId]/components/EditTaskModal.tsx | 70 ++++++++++++----- .../[memberId]/components/TimesheetCard.tsx | 6 +- .../components/TimesheetDetailModal.tsx | 1 + .../[locale]/timesheet/[memberId]/page.tsx | 3 +- apps/web/app/helpers/date.ts | 35 +++++++++ apps/web/app/hooks/features/useTimesheet.ts | 2 +- .../blocks/task-secondary-info.tsx | 4 +- .../calendar/table-time-sheet.tsx | 2 +- .../lib/features/multiple-select/index.tsx | 76 ++++++++----------- 11 files changed, 152 insertions(+), 85 deletions(-) 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) { *: 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) => (
{option.employee.fullName} 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
- + {rows.map((task) => (
{task.employee.fullName}
@@ -160,9 +161,16 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati dash taskNumberClassName="text-sm" /> -
- {task.project && } - {task.project && task.project.name} +
+ {task.project?.imageUrl && ( + + )} + + {task.project?.name ?? 'No Project'} +
))} @@ -258,7 +266,7 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa
{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 (
for - {dataTimesheet.employee?.fullName ?? ""} - + setTimesheetData({ ...timesheetData, employeeId: value.employeeId })} + renderOption={(option) => ( +
+ {option.employee.fullName} + {option.employee.fullName} +
+ )} + />
- {t('dailyPlan.TASK_TIME')} + {t('dailyPlan.TASK_TIME')}
- {hours}:{minutes} h + {String(hours).padStart(2, '0')}:{String(minutes).padStart(2, '0')} h
@@ -189,13 +218,13 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo * 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 * 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 - {t('timer.TOTAL_HOURS')} - + {t('timer.TOTAL_HOURS').split(' ')[0]}{':'} +
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 = ({ mode, active, icon, ); +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) { <> { return ( -
  • +
  • {' '} {item.name}
  • 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[],
    {task.project?.imageUrl && } - {task.project?.name} + {task.project?.name ?? "No Project"}
    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 - * ( - *
    - * {option.charAt(0).toUpperCase()} - * {option.slice(1)} - *
    - * )} - * /> + * @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 (