From e6a9b33e44cb2aa91d4c03a68adf3b4e3f188910 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:28:51 +0200 Subject: [PATCH 1/4] [Feat]: Improve entry selection logic (#3484) * feat(timesheet): Improve entry selection logic * refactor(timesheet): Improve search and filtering functionality * fix:coderabbitai --- .../[memberId]/components/AddTaskModal.tsx | 1 - .../[memberId]/components/EditTaskModal.tsx | 2 +- .../components/RejectSelectedModal.tsx | 16 +++- .../[locale]/timesheet/[memberId]/page.tsx | 22 +---- .../hooks/features/useTimelogFilterOptions.ts | 28 ++++-- apps/web/app/hooks/features/useTimesheet.ts | 88 ++++++++++++++++--- .../calendar/table-time-sheet.tsx | 20 ++++- 7 files changed, 130 insertions(+), 47 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index 089f36e6d..cfdc306ad 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -188,7 +188,6 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { className='w-full font-medium dark:text-white' options={activeTeam?.members || []} onChange={(value) => { - console.log(value) updateFormState('employeeId', value) }} renderOption={(option) => ( diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx index 1cd5ddd81..66a19e4de 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/EditTaskModal.tsx @@ -171,7 +171,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo closeModal={closeModal} isOpen={isOpen} showCloseIcon - title={'Edit Task'} + title={t('common.EDIT_TASK')} className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[32rem] justify-start h-[auto]" titleClass="font-bold flex justify-start w-full">
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx index 1aea6661d..44dc52055 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx @@ -8,13 +8,27 @@ export interface IRejectSelectedModalProps { onReject: (reason: string) => void; minReasonLength?: number; maxReasonLength?: number; + selectTimesheetId?: string[]; } +/** + * A modal for rejecting selected timesheet entries. + * + * @param isOpen - If true, show the modal. Otherwise, hide the modal. + * @param closeModal - A function to close the modal. + * @param maxReasonLength - The maximum length of the rejection reason. + * @param onReject - A function to call when the user rejects the selected entries. + * @param minReasonLength - The minimum length of the rejection reason. + * @param selectTimesheetId - The IDs of the timesheet entries to be rejected. + * + * @returns A modal component. + */ export function RejectSelectedModal({ isOpen, closeModal, maxReasonLength, onReject, - minReasonLength + minReasonLength, + selectTimesheetId }: IRejectSelectedModalProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [reason, setReason] = useState(''); diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index 9f6f9668a..0b3a6a9cc 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -52,34 +52,18 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb to: endOfMonth(new Date()), }); - const { timesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({ + const { timesheet: filterDataTimesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({ startDate: dateRange.from!, endDate: dateRange.to!, - timesheetViewMode: timesheetNavigator + timesheetViewMode: timesheetNavigator, + inputSearch: search }); React.useEffect(() => { getOrganizationProjects(); }, [getOrganizationProjects]) - const lowerCaseSearch = useMemo(() => search?.toLowerCase() ?? '', [search]); - const filterDataTimesheet = useMemo(() => { - const filteredTimesheet = - timesheet - .filter((v) => - v.tasks.some( - (task) => - task.task?.title?.toLowerCase()?.includes(lowerCaseSearch) || - task.employee?.fullName?.toLowerCase()?.includes(lowerCaseSearch) || - task.project?.name?.toLowerCase()?.includes(lowerCaseSearch) - ) - ); - return filteredTimesheet; - }, [ - timesheet, - lowerCaseSearch, - ]); const { isOpen: isManualTimeModalOpen, openModal: openManualTimeModal, diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts index 995992ea4..01fa63bc1 100644 --- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts +++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts @@ -27,6 +27,14 @@ export function useTimelogFilterOptions() { ]; return user?.role.name ? allowedRoles.includes(user.role.name as RoleNameEnum) : false; }; + const normalizeText = (text: string | undefined | null): string => { + if (!text) return ''; + return text + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(); + }; const generateTimeOptions = (interval = 15) => { const totalSlots = (24 * 60) / interval; // Total intervals in a day @@ -47,16 +55,17 @@ export function useTimelogFilterOptions() { const handleSelectRowByStatusAndDate = (logs: TimesheetLog[], isChecked: boolean) => { setSelectTimesheetId((prev) => { const logIds = logs.map((item) => item.id); - - if (isChecked) { - return [...new Set([...prev, ...logIds])]; - } else { - return prev.filter((id) => !logIds.includes(id)); + if (!isChecked) { + const allSelected = logIds.every(id => prev.includes(id)); + if (allSelected) { + return prev.filter((id) => !logIds.includes(id)); + } else { + return [...new Set([...prev, ...logIds])]; + } } + return [...new Set([...prev, ...logIds])]; }); - } - - + }; React.useEffect(() => { return () => setSelectTimesheetId([]); @@ -84,6 +93,7 @@ export function useTimelogFilterOptions() { generateTimeOptions, setPuTimesheetStatus, puTimesheetStatus, - isUserAllowedToAccess + isUserAllowedToAccess, + normalizeText }; } diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts index b96b8dc67..56cdb231a 100644 --- a/apps/web/app/hooks/features/useTimesheet.ts +++ b/apps/web/app/hooks/features/useTimesheet.ts @@ -12,6 +12,7 @@ interface TimesheetParams { startDate?: Date | string; endDate?: Date | string; timesheetViewMode?: 'ListView' | 'CalendarView' + inputSearch?: string } export interface GroupedTimesheet { @@ -88,14 +89,46 @@ const groupByMonth = createGroupingFunction(date => `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}` ); +/** + * @function useTimesheet + * + * @description + * Fetches timesheet logs based on the provided date range and filters. + * + * @param {TimesheetParams} params + * @prop {Date} startDate - Start date of the period to fetch. + * @prop {Date} endDate - End date of the period to fetch. + * @prop {string} timesheetViewMode - "ListView" or "CalendarView" + * @prop {string} inputSearch - Search string to filter the timesheet logs. + * + * @returns + * @prop {boolean} loadingTimesheet - Whether the timesheet is being fetched. + * @prop {TimesheetLog[]} timesheet - The list of timesheet logs, grouped by day. + * @prop {function} getTaskTimesheet - Callable to fetch timesheet logs. + * @prop {boolean} loadingDeleteTimesheet - Whether a timesheet is being deleted. + * @prop {function} deleteTaskTimesheet - Callable to delete timesheet logs. + * @prop {function} getStatusTimesheet - Callable to group timesheet logs by status. + * @prop {TimesheetStatus} timesheetGroupByDays - The current filter for grouping timesheet logs. + * @prop {object} statusTimesheet - Timesheet logs grouped by status. + * @prop {function} updateTimesheetStatus - Callable to update the status of timesheet logs. + * @prop {boolean} loadingUpdateTimesheetStatus - Whether timesheet logs are being updated. + * @prop {boolean} puTimesheetStatus - Whether timesheet logs are updatable. + * @prop {function} createTimesheet - Callable to create a new timesheet log. + * @prop {boolean} loadingCreateTimesheet - Whether a timesheet log is being created. + * @prop {function} updateTimesheet - Callable to update a timesheet log. + * @prop {boolean} loadingUpdateTimesheet - Whether a timesheet log is being updated. + * @prop {function} groupByDate - Callable to group timesheet logs by date. + * @prop {boolean} isManage - Whether the user is authorized to manage the timesheet. + */ export function useTimesheet({ startDate, endDate, - timesheetViewMode + timesheetViewMode, + inputSearch }: TimesheetParams) { const { user } = useAuthenticateUser(); const [timesheet, setTimesheet] = useAtom(timesheetRapportState); - const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess } = useTimelogFilterOptions(); + const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText } = useTimelogFilterOptions(); const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi); const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi); const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi) @@ -266,23 +299,53 @@ export function useTimesheet({ }, [user, queryDeleteTimesheet, setTimesheet]); + const filterDataTimesheet = useMemo(() => { + if (!timesheet || !inputSearch) { + return timesheet; + } + const searchTerms = normalizeText(inputSearch).split(/\s+/).filter(Boolean); + if (searchTerms.length === 0) { + return timesheet; + } + return timesheet.filter((task) => { + const searchableContent = { + title: normalizeText(task.task?.title), + employee: normalizeText(task.employee?.fullName), + project: normalizeText(task.project?.name) + }; + return searchTerms.every(term => + Object.values(searchableContent).some(content => + content.includes(term) + ) + ); + }); + }, [timesheet, inputSearch]); + const timesheetElementGroup = useMemo(() => { + if (!timesheet) { + return []; + } + if (timesheetViewMode === 'ListView') { - if (timesheetGroupByDays === 'Daily') { - return groupByDate(timesheet); + switch (timesheetGroupByDays) { + case 'Daily': + return groupByDate(filterDataTimesheet); + case 'Weekly': + return groupByWeek(filterDataTimesheet); + case 'Monthly': + return groupByMonth(filterDataTimesheet); + default: + return groupByDate(filterDataTimesheet); } - if (timesheetGroupByDays === 'Weekly') { - return groupByWeek(timesheet); - } - return groupByMonth(timesheet); } - return groupByDate(timesheet); + + return groupByDate(filterDataTimesheet); }, [timesheetGroupByDays, timesheetViewMode, timesheet]); useEffect(() => { getTaskTimesheet({ startDate, endDate }); - }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays]); + }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, inputSearch]); return { loadingTimesheet, @@ -292,7 +355,7 @@ export function useTimesheet({ deleteTaskTimesheet, getStatusTimesheet, timesheetGroupByDays, - statusTimesheet: getStatusTimesheet(timesheet.flat()), + statusTimesheet: getStatusTimesheet(filterDataTimesheet.flat()), updateTimesheetStatus, loadingUpdateTimesheetStatus, puTimesheetStatus, @@ -301,6 +364,7 @@ export function useTimesheet({ updateTimesheet, loadingUpdateTimesheet, groupByDate, - isManage + isManage, + normalizeText }; } 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 2c73b7deb..96ca5bde5 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -117,6 +117,7 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[], countID={selectTimesheetId.length} /> { // Pending implementation }} @@ -186,11 +187,16 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[], handleSelectRowByStatusAndDate(rows, selectTimesheetId.length === 0)} + () => handleSelectRowByStatusAndDate( + rows, + !rows.every(row => selectTimesheetId.includes(row.id)) + ) + } data={rows} status={status} onSort={handleSort} date={plan.date} + selectedIds={selectTimesheetId} /> {rows.map((task) => (
void, data: TimesheetLog[], handleSelectRowByStatusAndDate: (status: string, date: string) => void, - date?: string + date?: string, + selectedIds: string[] + }) => { const { bg, bgOpacity } = statusColor(status); @@ -536,6 +546,7 @@ const HeaderRow = ({ Employee: null, Status: null, }); + const isAllSelected = data.length > 0 && data.every(row => selectedIds.includes(row.id)); const handleSort = (key: string) => { const newOrder = sortState[key] === "ASC" ? "DESC" : "ASC"; @@ -549,6 +560,7 @@ const HeaderRow = ({ className="flex items-center text-[#71717A] font-medium border-b border-t dark:border-gray-600 space-x-4 p-1 h-[60px] w-full" > date && handleSelectRowByStatusAndDate(status, date)} className="w-5 h-5" disabled={!date} @@ -581,7 +593,7 @@ const HeaderRow = ({ currentSort={sortState["Status"]} />
-
+
Time
From eacf9312c05944ac12dbe4a10139f5efb3a888a9 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:20:15 +0200 Subject: [PATCH 2/4] [Feat]: Timesheet-Pagination-Hook-and-Component (#3485) * feat:Add custom hook for paginating grouped timesheet data * feat:Added TimesheetPagination component for managing pagination of timesheet data. * feat:Added TimesheetPagination component for managing pagination of timesheet data. --- .../[memberId]/components/TimesheetIcons.tsx | 21 +++ .../components/TimesheetPagination.tsx | 85 ++++++++++++ .../[locale]/timesheet/[memberId]/page.tsx | 47 ++++++- .../hooks/features/useTimesheetPagination.ts | 123 ++++++++++++++++++ apps/web/components/ui/pagination.tsx | 117 +++++++++++++++++ .../calendar/table-time-sheet.tsx | 2 - 6 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx create mode 100755 apps/web/app/hooks/features/useTimesheetPagination.ts create mode 100755 apps/web/components/ui/pagination.tsx diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetIcons.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetIcons.tsx index 654782aaa..427bc3625 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetIcons.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetIcons.tsx @@ -157,3 +157,24 @@ export const PlusIcon = () => + + +/** + * ArrowLeftIcon + * + * Renders an arrow pointing left using SVG. + * + * @returns {React.ReactElement} - The rendered arrow left icon component. + */ +export const ArrowLeftIcon = () => + + + + + + + + +export const ArrowRightIcon = () => + + diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx new file mode 100644 index 000000000..89d3c4720 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx @@ -0,0 +1,85 @@ +import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink } from '@components/ui/pagination' +import React from 'react' +import { MdKeyboardDoubleArrowLeft, MdKeyboardDoubleArrowRight } from 'react-icons/md'; +interface TimesheetPaginationProps { + totalPages?: number; + onPageChange?: (page: number) => void; + nextPage?: () => void; + previousPage?: () => void; + goToPage: (page: number) => void; + currentPage?: number; + getPageNumbers: () => (number | string)[]; + dates?: string[]; + totalGroups?: number + +} + +/** + * A component for paginating timesheet data. + * + * @param {TimesheetPaginationProps} props - The props for the component. + * @param {number} [props.totalPages] - The total number of pages. + * @param {(page: number) => void} [props.onPageChange] - A function to call when the page is changed. + * @param {(page: number) => void} props.goToPage - A function to call when the user navigates to a specific page. + * @param {() => void} props.nextPage - A function to call when the user navigates to the next page. + * @param {() => void} props.previousPage - A function to call when the user navigates to the previous page. + * @param {number} [props.currentPage] - The current page number. + * @param {() => (number | string)[]} props.getPageNumbers - A function to get an array of page numbers. + * + * @returns {React.ReactElement} - The component element. + */ +function TimesheetPagination({ totalPages, onPageChange, goToPage, nextPage, previousPage, currentPage, getPageNumbers, dates, totalGroups }: TimesheetPaginationProps) { + return ( + // totalPages > 1 + <> + {totalPages && totalPages > 1 && ( + + +
+ Page {currentPage} of {totalPages} ({dates?.length} items of {totalGroups}) +
+ + + + + + {getPageNumbers().map((pageNumber, index) => ( + + {pageNumber === '...' ? ( + + ) : ( + goToPage(pageNumber as number)}> + {pageNumber} + + )} + + ))} + + + + +
+ ) + } + + ) + +} + +export default TimesheetPagination diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index 0b3a6a9cc..d79febb07 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -21,9 +21,12 @@ import { differenceBetweenHours, getGreeting, secondsToTime } from '@/app/helper import { useTimesheet } from '@/app/hooks/features/useTimesheet'; import { endOfMonth, startOfMonth } from 'date-fns'; import TimesheetDetailModal from './components/TimesheetDetailModal'; +import { useTimesheetPagination } from '@/app/hooks/features/useTimesheetPagination'; +import TimesheetPagination from './components/TimesheetPagination'; type TimesheetViewMode = 'ListView' | 'CalendarView'; export type TimesheetDetailMode = 'Pending' | 'MenHours' | 'MemberWork'; +const TIMESHEET_PAGE_SIZE = 10; type ViewToggleButtonProps = { mode: TimesheetViewMode; @@ -52,13 +55,30 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb to: endOfMonth(new Date()), }); - const { timesheet: filterDataTimesheet, statusTimesheet, loadingTimesheet, isManage } = useTimesheet({ + const { timesheet: filterDataTimesheet, statusTimesheet, loadingTimesheet, isManage, timesheetGroupByDays } = useTimesheet({ startDate: dateRange.from!, endDate: dateRange.to!, timesheetViewMode: timesheetNavigator, inputSearch: search }); + const { + paginatedGroups, + currentPage, + totalPages, + goToPage, + nextPage, + previousPage, + getPageNumbers, + totalGroups, + dates + } = useTimesheetPagination({ + data: filterDataTimesheet, + pageSize: TIMESHEET_PAGE_SIZE + });; + + + React.useEffect(() => { getOrganizationProjects(); }, [getOrganizationProjects]) @@ -103,6 +123,10 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb ], [activeTeam?.name, currentLocale, t] ); + const shouldRenderPagination = + timesheetNavigator === 'ListView' || + (timesheetGroupByDays === 'Daily' && timesheetNavigator === 'CalendarView'); + return ( <> {isTimesheetDetailOpen @@ -230,16 +254,33 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb {timesheetNavigator === 'ListView' ? ( ) : ( )} + {shouldRenderPagination && ( + + )} diff --git a/apps/web/app/hooks/features/useTimesheetPagination.ts b/apps/web/app/hooks/features/useTimesheetPagination.ts new file mode 100755 index 000000000..031b52558 --- /dev/null +++ b/apps/web/app/hooks/features/useTimesheetPagination.ts @@ -0,0 +1,123 @@ +import { useState, useMemo } from 'react'; +import { TimesheetLog } from '@/app/interfaces'; + +export interface GroupedTimesheet { + date: string; + tasks: TimesheetLog[]; +} + +interface PaginationState { + currentPage: number; + totalPages: number; + totalGroups: number; + totalTasks: number; + dates: string[]; +} + +interface UseTimesheetPaginationProps { + data: GroupedTimesheet[]; + pageSize?: number; +} + +/** + * Custom hook for paginating grouped timesheet data. + * + * @param {GroupedTimesheet[]} data - An array of grouped timesheet data, each group containing a date and associated tasks. + * @param {number} [pageSize=10] - The number of groups to show per page. + * + * @returns {Object} - An object containing pagination details and functions. + * @property {GroupedTimesheet[]} paginatedGroups - The currently visible groups based on pagination. + * @property {number} currentPage - The current page number. + * @property {number} totalPages - The total number of pages available. + * @property {number} totalGroups - The total number of groups in the data. + * @property {number} totalTasks - The total number of tasks across all groups. + * @property {string[]} dates - The dates of the currently visible groups. + * @property {function} goToPage - A function to navigate to a specific page. + * @property {function} nextPage - A function to navigate to the next page. + * @property {function} previousPage - A function to navigate to the previous page. + * @property {function} getPageNumbers - A function to get an array of page numbers for pagination controls. + */ + +export function useTimesheetPagination({ + data, + pageSize = 10, +}: UseTimesheetPaginationProps) { + const [currentPage, setCurrentPage] = useState(1); + + const paginationState = useMemo(() => { + const totalGroups = data.length; + const totalPages = Math.max(1, Math.ceil(totalGroups / pageSize)); + const validCurrentPage = Math.min(currentPage, totalPages); + + const startIndex = (validCurrentPage - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, totalGroups); + const paginatedDates = data + .slice(startIndex, endIndex) + .map(group => group.date); + + const totalTasks = data.reduce((sum, group) => sum + group.tasks.length, 0); + + return { + currentPage: validCurrentPage, + totalPages, + totalGroups, + totalTasks, + dates: paginatedDates, + }; + }, [data, pageSize, currentPage]); + + const paginatedGroups = useMemo(() => { + const startIndex = (paginationState.currentPage - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, data.length); + return data.slice(startIndex, endIndex); + }, [data, pageSize, paginationState.currentPage]); + + const goToPage = (page: number) => { + setCurrentPage(Math.max(1, Math.min(page, paginationState.totalPages))); + }; + + const nextPage = () => { + if (currentPage < paginationState.totalPages) { + goToPage(currentPage + 1); + } + }; + + const previousPage = () => { + if (currentPage > 1) { + goToPage(currentPage - 1); + } + }; + + const getPageNumbers = (): (number | string)[] => { + const { currentPage, totalPages } = paginationState; + const delta = 2; + const range: (number | string)[] = []; + + for (let i = 1; i <= totalPages; i++) { + if ( + i === 1 || + i === totalPages || + (i >= currentPage - delta && i <= currentPage + delta) + ) { + range.push(i); + } else if (range[range.length - 1] !== '...') { + range.push('...'); + } + } + + return range; + }; + + return { + paginatedGroups, + currentPage: paginationState.currentPage, + totalPages: paginationState.totalPages, + totalGroups: paginationState.totalGroups, + totalTasks: paginationState.totalTasks, + dates: paginationState.dates, + goToPage, + nextPage, + previousPage, + getPageNumbers, + }; +} diff --git a/apps/web/components/ui/pagination.tsx b/apps/web/components/ui/pagination.tsx new file mode 100755 index 000000000..6c1859e4c --- /dev/null +++ b/apps/web/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from 'lib/utils'; +import { ButtonProps, buttonVariants } from "components/ui/button" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +