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"]}
/>
-
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">) => (
+
+)
+Pagination.displayName = "Pagination"
+
+const PaginationContent = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationContent.displayName = "PaginationContent"
+
+const PaginationItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+PaginationItem.displayName = "PaginationItem"
+
+type PaginationLinkProps = {
+ isActive?: boolean
+} & Pick &
+ React.ComponentProps<"a">
+
+const PaginationLink = ({
+ className,
+ isActive,
+ size = "icon",
+ ...props
+}: PaginationLinkProps) => (
+
+)
+PaginationLink.displayName = "PaginationLink"
+
+const PaginationPrevious = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+
+ Previous
+
+)
+PaginationPrevious.displayName = "PaginationPrevious"
+
+const PaginationNext = ({
+ className,
+ ...props
+}: React.ComponentProps) => (
+
+ Next
+
+
+)
+PaginationNext.displayName = "PaginationNext"
+
+const PaginationEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More pages
+
+)
+PaginationEllipsis.displayName = "PaginationEllipsis"
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+}
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 96ca5bde5..c47f736ec 100644
--- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
+++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
@@ -62,8 +62,6 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
const { deleteTaskTimesheet, loadingDeleteTimesheet, getStatusTimesheet, updateTimesheetStatus } = useTimesheet({});
const { timesheetGroupByDays, handleSelectRowByStatusAndDate, handleSelectRowTimesheet, selectTimesheetId, setSelectTimesheetId, isUserAllowedToAccess } = useTimelogFilterOptions();
const isManage = isUserAllowedToAccess(user);
-
-
const handleConfirm = () => {
try {
deleteTaskTimesheet({ logIds: selectTimesheetId })
From e9ff2134700ff00779dcb887b8c577f9a7cea664 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Sat, 28 Dec 2024 19:22:48 +0200
Subject: [PATCH 3/4] [Feat]: Add Selection/Deselection Functionality based on
ID (#3488)
* feat: Add selection/deselection functionality based on ID with full data handling
* fix: deepscan
---
.../components/RejectSelectedModal.tsx | 12 +++++++--
.../hooks/features/useTimelogFilterOptions.ts | 20 +++++++--------
apps/web/app/hooks/features/useTimesheet.ts | 16 ++++++------
.../calendar/table-time-sheet.tsx | 25 +++++++++++--------
4 files changed, 43 insertions(+), 30 deletions(-)
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx
index 44dc52055..6e82b567b 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/components/RejectSelectedModal.tsx
@@ -1,3 +1,4 @@
+import { useTimesheet } from '@/app/hooks/features/useTimesheet';
import { clsxm } from '@/app/utils';
import { Modal } from '@/lib/components';
import { useTranslations } from 'next-intl';
@@ -32,13 +33,20 @@ export function RejectSelectedModal({
}: IRejectSelectedModalProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [reason, setReason] = useState('');
+ const { updateTimesheetStatus, setSelectTimesheetId } = useTimesheet({});
+
const t = useTranslations();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
- await onReject(reason);
- closeModal();
+ updateTimesheetStatus({
+ status: 'DENIED',
+ ids: selectTimesheetId || [],
+ }).then(() => {
+ closeModal();
+ setSelectTimesheetId([])
+ }).catch((error) => console.error(error));
} finally {
setIsSubmitting(false);
}
diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
index 01fa63bc1..242352156 100644
--- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts
+++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
@@ -13,7 +13,7 @@ export function useTimelogFilterOptions() {
const [timesheetGroupByDays, setTimesheetGroupByDays] = useAtom(timesheetGroupByDayState);
const [puTimesheetStatus, setPuTimesheetStatus] = useAtom(timesheetUpdateStatus)
const [selectedItems, setSelectedItems] = React.useState<{ status: string; date: string }[]>([]);
- const [selectTimesheetId, setSelectTimesheetId] = React.useState([])
+ const [selectTimesheetId, setSelectTimesheetId] = React.useState([])
const employee = employeeState;
const project = projectState;
@@ -48,22 +48,20 @@ export function useTimelogFilterOptions() {
});
};
- const handleSelectRowTimesheet = (items: string) => {
+ const handleSelectRowTimesheet = (items: TimesheetLog) => {
setSelectTimesheetId((prev) => prev.includes(items) ? prev.filter((filter) => filter !== items) : [...prev, items])
}
const handleSelectRowByStatusAndDate = (logs: TimesheetLog[], isChecked: boolean) => {
- setSelectTimesheetId((prev) => {
- const logIds = logs.map((item) => item.id);
+ setSelectTimesheetId((prev: TimesheetLog[]) => {
+ const isLogIncluded = (log: TimesheetLog, list: TimesheetLog[]) =>
+ list.some((item) => item.id === log.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 prev.filter((prevLog) => !logs.some((log) => log.id === prevLog.id));
}
- return [...new Set([...prev, ...logIds])];
+ const newLogs = logs.filter((log) => !isLogIncluded(log, prev));
+ return [...prev, ...newLogs];
});
};
diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts
index 56cdb231a..d06e4db2b 100644
--- a/apps/web/app/hooks/features/useTimesheet.ts
+++ b/apps/web/app/hooks/features/useTimesheet.ts
@@ -128,7 +128,7 @@ export function useTimesheet({
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
- const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText } = useTimelogFilterOptions();
+ const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText, setSelectTimesheetId } = useTimelogFilterOptions();
const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi)
@@ -198,13 +198,14 @@ export function useTimesheet({
}
try {
const response = await queryUpdateTimesheet(timesheet);
- setTimesheet(prevTimesheet =>
- prevTimesheet.map(item =>
+ setTimesheet((prevTimesheet) => {
+ const updatedTimesheets = prevTimesheet.map((item) =>
item.id === response.data.id
- ? response.data
+ ? { ...item, ...response.data }
: item
- )
- );
+ );
+ return updatedTimesheets;
+ });
} catch (error) {
console.error('Error updating timesheet:', error);
throw error;
@@ -365,6 +366,7 @@ export function useTimesheet({
loadingUpdateTimesheet,
groupByDate,
isManage,
- normalizeText
+ normalizeText,
+ setSelectTimesheetId
};
}
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 c47f736ec..66fcee16f 100644
--- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
+++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
@@ -64,7 +64,7 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
const isManage = isUserAllowedToAccess(user);
const handleConfirm = () => {
try {
- deleteTaskTimesheet({ logIds: selectTimesheetId })
+ deleteTaskTimesheet({ logIds: selectTimesheetId?.map((select) => select.id).filter((id) => id !== undefined) })
.then(() => {
setSelectTimesheetId([]);
closeAlertConfirmation()
@@ -86,10 +86,13 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
switch (action) {
case 'Approved':
if (selectTimesheetId.length > 0) {
- await updateTimesheetStatus({
+ updateTimesheetStatus({
status: 'APPROVED',
- ids: selectTimesheetId
- })
+ ids: selectTimesheetId.map((select) =>
+ select.timesheetId)
+ .filter((timesheetId) => timesheetId !== undefined)
+ }).then(() => setSelectTimesheetId([]))
+ .catch((error) => console.error(error))
}
break;
case 'Denied':
@@ -115,7 +118,9 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
countID={selectTimesheetId.length}
/>
+ select.timesheetId)
+ .filter((timesheetId) => timesheetId !== undefined)}
onReject={() => {
// Pending implementation
}}
@@ -187,7 +192,7 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
handleSelectRowByStatusAndDate={
() => handleSelectRowByStatusAndDate(
rows,
- !rows.every(row => selectTimesheetId.includes(row.id))
+ !rows.every(row => selectTimesheetId.includes(row))
)
}
data={rows}
@@ -209,8 +214,8 @@ export function DataTableTimeSheet({ data, user }: { data?: GroupedTimesheet[],
>
handleSelectRowTimesheet(task.id)}
- checked={selectTimesheetId.includes(task.id)}
+ onCheckedChange={() => handleSelectRowTimesheet(task)}
+ checked={selectTimesheetId.includes(task)}
/>
void,
date?: string,
- selectedIds: string[]
+ selectedIds: TimesheetLog[]
}) => {
@@ -544,7 +549,7 @@ const HeaderRow = ({
Employee: null,
Status: null,
});
- const isAllSelected = data.length > 0 && data.every(row => selectedIds.includes(row.id));
+ const isAllSelected = data.length > 0 && data.every(row => selectedIds.includes(row));
const handleSort = (key: string) => {
const newOrder = sortState[key] === "ASC" ? "DESC" : "ASC";
From 92e20f8b4c36d71dfbbbeb272f0eb47b383de12e Mon Sep 17 00:00:00 2001
From: Innocent-akim
Date: Sat, 28 Dec 2024 22:34:09 +0200
Subject: [PATCH 4/4] feat: update offline screen for network interruption
---
apps/web/app/[locale]/page-component.tsx | 6 +-
apps/web/app/interfaces/hooks.ts | 1 +
apps/web/components/pages/offline/index.tsx | 24 +++-
apps/web/lib/features/timer/timer.tsx | 123 ++++++++++----------
apps/web/locales/ar.json | 6 +-
apps/web/locales/bg.json | 4 +-
apps/web/locales/de.json | 4 +-
apps/web/locales/en.json | 4 +-
apps/web/locales/es.json | 6 +-
apps/web/locales/fr.json | 4 +-
apps/web/locales/he.json | 6 +-
apps/web/locales/it.json | 6 +-
apps/web/locales/nl.json | 6 +-
apps/web/locales/pl.json | 6 +-
apps/web/locales/pt.json | 6 +-
apps/web/locales/ru.json | 4 +-
apps/web/locales/zh.json | 6 +-
17 files changed, 136 insertions(+), 86 deletions(-)
diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx
index 89a043e2e..2a1ab3151 100644
--- a/apps/web/app/[locale]/page-component.tsx
+++ b/apps/web/app/[locale]/page-component.tsx
@@ -2,7 +2,7 @@
'use client';
import React, { useEffect, useState } from 'react';
-import { useOrganizationTeams } from '@app/hooks';
+import { useOrganizationTeams, useTimerView } from '@app/hooks';
import { clsxm } from '@app/utils';
import NoTeam from '@components/pages/main/no-team';
import { withAuthentication } from 'lib/app/authenticator';
@@ -34,6 +34,8 @@ function MainPage() {
const t = useTranslations();
const [headerSize] = useState(10);
const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams();
+ const { timerStatus } = useTimerView();
+
const [fullWidth, setFullWidth] = useAtom(fullWidthState);
const [view, setView] = useAtom(headerTabs);
const path = usePathname();
@@ -56,7 +58,7 @@ function MainPage() {
}, [setFullWidth]);
if (!online) {
- return ;
+ return ;
}
return (
<>
diff --git a/apps/web/app/interfaces/hooks.ts b/apps/web/app/interfaces/hooks.ts
index 288c22120..33a4b3e69 100644
--- a/apps/web/app/interfaces/hooks.ts
+++ b/apps/web/app/interfaces/hooks.ts
@@ -110,6 +110,7 @@ export interface IDrowDownData {
export type IClassName = {
className?: string;
fullWidth?: boolean;
+ showTimerButton?: boolean,
type?: 'VERTICAL' | 'HORIZONTAL';
} & T;
diff --git a/apps/web/components/pages/offline/index.tsx b/apps/web/components/pages/offline/index.tsx
index ec2fdc015..198e689df 100644
--- a/apps/web/components/pages/offline/index.tsx
+++ b/apps/web/components/pages/offline/index.tsx
@@ -1,20 +1,36 @@
+import { Timer } from '@/lib/features';
+import { cn } from '@/lib/utils';
import SadCry from '@components/ui/svgs/sad-cry';
import { Text } from 'lib/components';
import { useTranslations } from 'next-intl';
-
-function Offline() {
+interface IPropsOffline {
+ showTimer?: boolean
+}
+function Offline({ showTimer }: IPropsOffline) {
const t = useTranslations();
-
return (
- Offline!
+ {t('pages.offline.STATUS_NETWORK')}
{t('pages.offline.HEADING_TITLE')}
+
+
+ {t('pages.offline.HEADING_SUB_DESCRIPTION')}
+
+
+ {showTimer &&
}
{t('pages.offline.HEADING_DESCRIPTION')}
diff --git a/apps/web/lib/features/timer/timer.tsx b/apps/web/lib/features/timer/timer.tsx
index 3ce63957c..6d8ab6609 100644
--- a/apps/web/lib/features/timer/timer.tsx
+++ b/apps/web/lib/features/timer/timer.tsx
@@ -18,7 +18,7 @@ import { useCallback, useMemo } from 'react';
import { AddTasksEstimationHoursModal, EnforcePlanedTaskModal, SuggestDailyPlanModal } from '../daily-plan';
import { useStartStopTimerHandler } from '@app/hooks/features/useStartStopTimerHandler';
-export function Timer({ className }: IClassName) {
+export function Timer({ className, showTimerButton = true }: IClassName) {
const t = useTranslations();
const {
@@ -86,13 +86,12 @@ export function Timer({ className }: IClassName) {
{pad(hours)}:{pad(minutes)}:{pad(seconds)}
:{pad(ms_p)}
@@ -117,68 +116,74 @@ export function Timer({ className }: IClassName) {
-
-
-
-
+
+
+
-
+ // enabled={
+ // !canRunTimer && timerStatus?.lastLog?.source !== TimerSource.TEAMS
+ // }
+ >
+
+
-
- {/**
+
+ {/**
* Track time on planned task (SOFT FLOW)
*/}
- {hasPlan && activeTeamTask && (
-
- )}
+ {hasPlan && activeTeamTask && (
+
+ )}
- {hasPlan && hasPlan.tasks && (
-
- )}
+ {hasPlan && hasPlan.tasks && (
+
+ )}
- {/**
+ {/**
* Track time on planned task (REQUIRE PLAN)
*/}
- {requirePlan && hasPlan && activeTeamTask && (
-
- )}
-
+ {requirePlan && hasPlan && activeTeamTask && (
+
+ )}
+
+ >
+ )
+ }
);
}
diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json
index 5f2c47efd..21adcb4b7 100644
--- a/apps/web/locales/ar.json
+++ b/apps/web/locales/ar.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "الذهاب إلى الصفحة الرئيسية"
},
"offline": {
- "HEADING_TITLE": "انقطاع الشبكة!",
- "HEADING_DESCRIPTION": "أنت غير متصل حاليًا، يرجى التحقق من اتصالك بالإنترنت..."
+ "HEADING_TITLE": "تم قطع الشبكة!",
+ "HEADING_SUB_DESCRIPTION": "عفوًا، فقدناك. لا تقلق، الوقت ما زال يتم تتبعه.",
+ "HEADING_DESCRIPTION": "ستتمكن من متابعة العمل في تطبيق الويب بمجرد استعادة الاتصال.",
+ "STATUS_NETWORK": "غير متصل!"
},
"error": {
"TITLE": "خطأ",
diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json
index ce51bec75..936a57d6c 100644
--- a/apps/web/locales/bg.json
+++ b/apps/web/locales/bg.json
@@ -612,7 +612,9 @@
},
"offline": {
"HEADING_TITLE": "Мрежата е прекъсната!",
- "HEADING_DESCRIPTION": "В момента сте извън линия, моля, проверете интернет връзката си..."
+ "HEADING_SUB_DESCRIPTION": "Опа, изгубихме ви. Не се притеснявайте, времето ви все още се отчита.",
+ "HEADING_DESCRIPTION": "Ще можете да продължите да работите в уеб приложението, след като връзката ви бъде възстановена.",
+ "STATUS_NETWORK": "Офлайн!"
},
"error": {
"TITLE": "грешка!",
diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json
index 09b5a8192..b7fcbcc8f 100644
--- a/apps/web/locales/de.json
+++ b/apps/web/locales/de.json
@@ -612,7 +612,9 @@
},
"offline": {
"HEADING_TITLE": "Netzwerk getrennt!",
- "HEADING_DESCRIPTION": "Sie sind derzeit offline, bitte überprüfen Sie Ihre Internetverbindung..."
+ "HEADING_SUB_DESCRIPTION": "Ups, wir haben Sie verloren. Keine Sorge, Ihre Zeit wird weiterhin erfasst.",
+ "HEADING_DESCRIPTION": "Sie können in der Web-App weiterarbeiten, sobald Ihre Verbindung wiederhergestellt ist.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Error !",
diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json
index 37c2d34eb..02baf2cca 100644
--- a/apps/web/locales/en.json
+++ b/apps/web/locales/en.json
@@ -612,7 +612,9 @@
},
"offline": {
"HEADING_TITLE": "Network Disconnected!",
- "HEADING_DESCRIPTION": "Your network is offline, please check your internet connection..."
+ "HEADING_SUB_DESCRIPTION": "Oops, we lost you. Don't worry, your time is still tracking.",
+ "HEADING_DESCRIPTION": "You will be able to continue working in the web app once your connection is restored.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Error !",
diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json
index 4e9f06126..6269b7b28 100644
--- a/apps/web/locales/es.json
+++ b/apps/web/locales/es.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "Ir a la página de inicio"
},
"offline": {
- "HEADING_TITLE": "¡Red Desconectada!",
- "HEADING_DESCRIPTION": "Actualmente estás desconectado, por favor verifica tu conexión a Internet..."
+ "HEADING_TITLE": "¡Red desconectada!",
+ "HEADING_SUB_DESCRIPTION": "Vaya, te hemos perdido. No te preocupes, tu tiempo sigue siendo registrado.",
+ "HEADING_DESCRIPTION": "Podrás continuar trabajando en la aplicación web una vez que se restablezca tu conexión.",
+ "STATUS_NETWORK": "¡Desconectado!"
},
"error": {
"TITLE": "Error !",
diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json
index 408e52567..20a2e02c6 100644
--- a/apps/web/locales/fr.json
+++ b/apps/web/locales/fr.json
@@ -612,7 +612,9 @@
},
"offline": {
"HEADING_TITLE": "Réseau déconnecté !",
- "HEADING_DESCRIPTION": "Vous êtes actuellement hors ligne, veuillez vérifier votre connexion Internet..."
+ "HEADING_SUB_DESCRIPTION": "Oups, nous vous avons perdu. Ne vous inquiétez pas, votre temps est toujours suivi.",
+ "HEADING_DESCRIPTION": "Vous pourrez continuer à travailler dans l'application web une fois votre connexion rétablie.",
+ "STATUS_NETWORK": "Hors ligne !"
},
"error": {
"TITLE": "Erreur !",
diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json
index 22fcdc8be..ea287b699 100644
--- a/apps/web/locales/he.json
+++ b/apps/web/locales/he.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "לך לדף הבית"
},
"offline": {
- "HEADING_TITLE": "רשת מנותקת!",
- "HEADING_DESCRIPTION": "אתה לא מחובר כרגע, בבקשה בדוק את חיבור האינטרנט שלך..."
+ "HEADING_TITLE": "החיבור נותק!",
+ "HEADING_SUB_DESCRIPTION": "אופס, איבדנו אותך. אל דאגה, הזמן שלך עדיין עוקב.",
+ "HEADING_DESCRIPTION": "תוכל להמשיך לעבוד באפליקציית האינטרנט לאחר שחיבורך ישוחזר.",
+ "STATUS_NETWORK": "לא מקוון!"
},
"error": {
"TITLE": "שגיאה",
diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json
index a675053bb..1b55c461d 100644
--- a/apps/web/locales/it.json
+++ b/apps/web/locales/it.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "Vai alla homepage"
},
"offline": {
- "HEADING_TITLE": "Rete Disconnessa!",
- "HEADING_DESCRIPTION": "Attualmente sei offline, controlla la tua connessione Internet..."
+ "HEADING_TITLE": "Connessione di rete interrotta!",
+ "HEADING_SUB_DESCRIPTION": "Ops, ti abbiamo perso. Non preoccuparti, il tuo tempo è ancora in tracciamento.",
+ "HEADING_DESCRIPTION": "Potrai continuare a lavorare nell'app web una volta ripristinata la connessione.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Errore!",
diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json
index 75d00076d..0fb542512 100644
--- a/apps/web/locales/nl.json
+++ b/apps/web/locales/nl.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "Naar de startpagina"
},
"offline": {
- "HEADING_TITLE": "Netwerk Verbroken!",
- "HEADING_DESCRIPTION": "U bent momenteel offline, controleer uw internetverbinding..."
+ "HEADING_TITLE": "Netwerk verbroken!",
+ "HEADING_SUB_DESCRIPTION": "Oeps, we zijn je kwijt. Maak je geen zorgen, je tijd wordt nog steeds bijgehouden.",
+ "HEADING_DESCRIPTION": "Je kunt doorgaan met werken in de webapp zodra je verbinding is hersteld.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Fout !",
diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json
index e5df9337c..477f8cf14 100644
--- a/apps/web/locales/pl.json
+++ b/apps/web/locales/pl.json
@@ -611,8 +611,10 @@
"LINK_LABEL": "Przejdź do strony głównej"
},
"offline": {
- "HEADING_TITLE": "Rozłączono sieć!",
- "HEADING_DESCRIPTION": "Jesteś obecnie offline, sprawdź swoje połączenie z internetem..."
+ "HEADING_TITLE": "Sieć rozłączona!",
+ "HEADING_SUB_DESCRIPTION": "Ups, straciliśmy cię. Nie martw się, twój czas jest nadal śledzony.",
+ "HEADING_DESCRIPTION": "Będziesz mógł kontynuować pracę w aplikacji internetowej, gdy połączenie zostanie przywrócone.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Błąd !",
diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json
index a17e5f55d..027e08e28 100644
--- a/apps/web/locales/pt.json
+++ b/apps/web/locales/pt.json
@@ -612,8 +612,10 @@
"LINK_LABEL": "Ir para a página inicial"
},
"offline": {
- "HEADING_TITLE": "Rede Desconectada!",
- "HEADING_DESCRIPTION": "Você está atualmente offline, por favor, verifique sua conexão com a Internet..."
+ "HEADING_TITLE": "Rede desconectada!",
+ "HEADING_SUB_DESCRIPTION": "Ops, perdemos você. Não se preocupe, seu tempo ainda está sendo rastreado.",
+ "HEADING_DESCRIPTION": "Você poderá continuar trabalhando no aplicativo web assim que sua conexão for restaurada.",
+ "STATUS_NETWORK": "Offline!"
},
"error": {
"TITLE": "Erro!",
diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json
index 5f406cfb5..f44561ad4 100644
--- a/apps/web/locales/ru.json
+++ b/apps/web/locales/ru.json
@@ -612,7 +612,9 @@
},
"offline": {
"HEADING_TITLE": "Сеть отключена!",
- "HEADING_DESCRIPTION": "В настоящее время вы не подключены к Интернету, проверьте ваше соединение..."
+ "HEADING_SUB_DESCRIPTION": "Ой, мы потеряли вас. Не волнуйтесь, ваше время все еще отслеживается.",
+ "HEADING_DESCRIPTION": "Вы сможете продолжить работу в веб-приложении, как только ваше соединение будет восстановлено.",
+ "STATUS_NETWORK": "Офлайн!"
},
"error": {
"TITLE": "Ошибка !",
diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json
index f5c69dbf5..8281b7660 100644
--- a/apps/web/locales/zh.json
+++ b/apps/web/locales/zh.json
@@ -587,8 +587,10 @@
"TRY_AGAIN": "再试一次"
},
"offline": {
- "HEADING_TITLE": "网络断开连接!",
- "HEADING_DESCRIPTION": "您目前处于离线状态,请检查您的互联网连接..."
+ "HEADING_TITLE": "网络断开!",
+ "HEADING_SUB_DESCRIPTION": "糟糕,我们失去了您的连接。别担心,您的时间仍在跟踪中。",
+ "HEADING_DESCRIPTION": "连接恢复后,您将可以继续在网页应用中工作。",
+ "STATUS_NETWORK": "离线!"
},
"invite": {
"HEADING_TITLE": "邀请成员加入您的团队",