diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index b688b20a0..86a6812f0 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -19,13 +19,14 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { const [isBillable, setIsBillable] = React.useState(true); const projectItemsLists = { - Project: activeTeam?.projects as [], + Project: activeTeam?.projects ?? [], }; + const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { // Handle value changes }; const selectedValues = { - Preject: null, + Project: null, }; const handleChange = (field: string, selectedItem: Item | null) => { // Handle field changes diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx index 9c2daa359..6cfb7a0fd 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx @@ -28,12 +28,12 @@ export function FilterWithStatus({ }; const buttonData = Object.entries({ - 'All Tasks': Object.values(data!).reduce((total, tasks) => total + tasks.length, 0), - Pending: data!.PENDING.length, - Approved: data!.APPROVED.length, - 'In review': data!['IN REVIEW'].length, - Draft: data!.DRAFT.length, - Rejected: data!.DENIED.length, + 'All Tasks': React.useMemo(() => Object.values(data!).reduce((total, tasks) => total + tasks.length, 0), [data]), + Pending: data?.PENDING.length ?? 0, + Approved: data?.APPROVED.length ?? 0, + 'In review': data!['IN REVIEW']?.length ?? 0, + Draft: data?.DRAFT.length ?? 0, + Rejected: data?.DENIED.length ?? 0, }).map(([label, count]) => ({ label: label as FilterStatus, count, diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx index 5ba36512b..1909dcd13 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx @@ -1,4 +1,4 @@ -import { TimesheetStatus } from "@/app/interfaces"; +import { TimesheetFilterByDays, TimesheetStatus } from "@/app/interfaces"; import { clsxm } from "@/app/utils"; import { TranslationHooks } from "next-intl"; import { ReactNode } from "react"; @@ -31,6 +31,7 @@ export type StatusType = "Pending" | "Approved" | "Denied"; export type StatusAction = "Deleted" | "Approved" | "Denied"; + // eslint-disable-next-line @typescript-eslint/no-empty-function export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, disabled: boolean, onClick: (action: StatusAction) => void) => { @@ -69,3 +70,9 @@ export const statusTable: { label: TimesheetStatus; description: string }[] = [ { label: "DRAFT", description: "The item is saved as draft" }, { label: "DENIED", description: "The item has been rejected" }, ]; + +export const DailyTable: { label: TimesheetFilterByDays; description: string }[] = [ + { label: "Daily", description: 'Group by Daily' }, + { label: "Weekly", description: 'Group by Weekly' }, + { label: "Monthly", description: 'Group by Monthly' }, +]; diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index 01987f6a8..0f5b0243b 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -44,7 +44,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb to: endOfDay(new Date()) }); - const { timesheet, statusTimesheet } = useTimesheet({ + const { timesheet, statusTimesheet, timesheetGroupByMonth, timesheetGroupByWeek } = useTimesheet({ startDate: dateRange.from ?? '', endDate: dateRange.to ?? '' }); @@ -52,7 +52,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb const lowerCaseSearch = useMemo(() => search?.toLowerCase() ?? '', [search]); const filterDataTimesheet = useMemo( () => - timesheet.filter((v) => + timesheetGroupByWeek.filter((v) => v.tasks.some( (task) => task.task?.title?.toLowerCase()?.includes(lowerCaseSearch) || @@ -60,7 +60,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb task.project?.name?.toLowerCase()?.includes(lowerCaseSearch) ) ), - [timesheet, lowerCaseSearch] + [timesheet, timesheetGroupByMonth, timesheetGroupByWeek, lowerCaseSearch] ); const { diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index 09dd79446..a0c7ea88c 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -52,6 +52,69 @@ const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => { .map(([date, tasks]) => ({ date, tasks })) .sort((a, b) => b.date.localeCompare(a.date)); } +const getWeekYearKey = (date: Date): string => { + const startOfYear = new Date(date.getFullYear(), 0, 1); + const daysSinceStart = Math.floor((date.getTime() - startOfYear.getTime()) / (1000 * 60 * 60 * 24)); + const week = Math.ceil((daysSinceStart + startOfYear.getDay() + 1) / 7); + return `${date.getFullYear()}-W${week}`; +}; + +const groupByWeek = (items: TimesheetLog[]): GroupedTimesheet[] => { + if (!items?.length) return []; + type GroupedMap = Record; + + const groupedByWeek = items.reduce((acc, item) => { + if (!item?.timesheet?.createdAt) { + console.warn('Skipping item with missing timesheet or createdAt:', item); + return acc; + } + try { + const date = new Date(item.timesheet.createdAt); + const weekKey = getWeekYearKey(date); + if (!acc[weekKey]) acc[weekKey] = []; + acc[weekKey].push(item); + } catch (error) { + console.error( + `Failed to process date for timesheet ${item.timesheet.id}:`, + { createdAt: item.timesheet.createdAt, error } + ); + } + return acc; + }, {}); + + return Object.entries(groupedByWeek) + .map(([week, tasks]) => ({ date: week, tasks })) + .sort((a, b) => b.date.localeCompare(a.date)); +}; + +const groupByMonth = (items: TimesheetLog[]): GroupedTimesheet[] => { + if (!items?.length) return []; + type GroupedMap = Record; + + const groupedByMonth = items.reduce((acc, item) => { + if (!item?.timesheet?.createdAt) { + console.warn('Skipping item with missing timesheet or createdAt:', item); + return acc; + } + try { + const date = new Date(item.timesheet.createdAt); + const monthKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`; + if (!acc[monthKey]) acc[monthKey] = []; + acc[monthKey].push(item); + } catch (error) { + console.error( + `Failed to process date for timesheet ${item.timesheet.id}:`, + { createdAt: item.timesheet.createdAt, error } + ); + } + return acc; + }, {}); + + return Object.entries(groupedByMonth) + .map(([month, tasks]) => ({ date: month, tasks })) + .sort((a, b) => b.date.localeCompare(a.date)); +}; + export function useTimesheet({ @@ -169,6 +232,8 @@ export function useTimesheet({ return { loadingTimesheet, timesheet: groupByDate(timesheet), + timesheetGroupByWeek: groupByWeek(timesheet), + timesheetGroupByMonth: groupByMonth(timesheet), getTaskTimesheet, loadingDeleteTimesheet, deleteTaskTimesheet, diff --git a/apps/web/app/interfaces/ITask.ts b/apps/web/app/interfaces/ITask.ts index 1a50b539c..173288e0f 100644 --- a/apps/web/app/interfaces/ITask.ts +++ b/apps/web/app/interfaces/ITask.ts @@ -144,6 +144,10 @@ export type TimesheetStatus = | "DENIED" | "APPROVED"; +export type TimesheetFilterByDays = + | "Daily" + | "Weekly" + | "Monthly" export type ITaskStatusStack = { status: ITaskStatus;