-
{site.hour}
-
- {site.apps?.map((item, i) => (
-
- ))}
+
+
+ {headers.map((header, i) => (
+
+ {header.title}
+
+ ))}
+
+
+ {sites?.map((site, i) => (
+
+
+ {site?.apps?.map((item, i) => (
+
+
+
+ ))}
+
+ ))}
+
+ {visitedSites?.length < 1 && !loading && (
+
+
{t('timer.NO_VISITED_SITE_MESSAGE')}
- ))}
-
- {visitedSites.length < 1 && !loading && (
-
-
{t('timer.THERE_IS_NO_APPS_VISITED')}
-
- )}
- {loading && visitedSites.length < 1 && (
- <>
-
-
- >
- )}
+ )}
+ {loading && visitedSites?.length < 1 && (
+ <>
+
+
+ >
+ )}
+
);
-});
+}
diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json
index 083f63108..588ed1887 100644
--- a/apps/web/locales/ar.json
+++ b/apps/web/locales/ar.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "ابدأ العمل",
"TIMER_RUNNING": "المؤقت قيد التشغيل بالفعل",
"WARNING_PLAN_ESTIMATION": "رجى تصحيح ساعات العمل المخطط لها أو إعادة تقدير المهمة (المهام)"
- }
+ },
+ "VISITED_SITES": "المواقع التي تمت زيارتها",
+ "NO_VISITED_SITE_MESSAGE": "لا يوجد مواقع تمت زيارتها"
},
"task": {
"TITLE": "المهمة",
diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json
index a95458b6b..09887212c 100644
--- a/apps/web/locales/bg.json
+++ b/apps/web/locales/bg.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "Започнете работа",
"TIMER_RUNNING": "Таймерът вече работи",
"WARNING_PLAN_ESTIMATION": "Моля, коригирайте планираните работни часове или преоценете задачата(ите) "
- }
+ },
+ "VISITED_SITES": "Посетени сайтове",
+ "NO_VISITED_SITE_MESSAGE": "Няма посетени сайтове"
},
"task": {
"TITLE": "Задача",
diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json
index af51d0af4..f0f81761c 100644
--- a/apps/web/locales/de.json
+++ b/apps/web/locales/de.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "Mit der Arbeit beginnen",
"TIMER_RUNNING": "Der Timer läuft bereits",
"WARNING_PLAN_ESTIMATION": "Bitte korrigieren Sie die geplanten Arbeitsstunden oder schätzen Sie die Aufgabe(n) neu ein."
- }
+ },
+ "VISITED_SITES": "Besuchte Seiten",
+ "NO_VISITED_SITE_MESSAGE": "Es gibt keine besuchten Seiten"
},
"task": {
"TITLE": "Aufgabe",
diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json
index 8a0a00bfc..443fb814c 100644
--- a/apps/web/locales/en.json
+++ b/apps/web/locales/en.json
@@ -713,7 +713,9 @@
"START_WORKING_BUTTON": "Start working",
"TIMER_RUNNING": "The timer is already running",
"WARNING_PLAN_ESTIMATION": "Please correct planned work hours or re-estimate task(s)"
- }
+ },
+ "VISITED_SITES": "Visited sites",
+ "NO_VISITED_SITE_MESSAGE": "There are no visited sites"
},
"task": {
"TITLE": "Task",
diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json
index 6db4e51af..c6f4149c2 100644
--- a/apps/web/locales/es.json
+++ b/apps/web/locales/es.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "Empezar a trabajar",
"TIMER_RUNNING": "El temporizador ya está en ejecución",
"WARNING_PLAN_ESTIMATION": "Por favor, corrija las horas de trabajo previstas o reevalúe la(s) tarea(s)"
- }
+ },
+ "VISITED_SITES": "Sitios visitados",
+ "NO_VISITED_SITE_MESSAGE": "No hay sitios visitados"
},
"task": {
"TITLE": "Tarea",
diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json
index 010a70a74..6a7489529 100644
--- a/apps/web/locales/fr.json
+++ b/apps/web/locales/fr.json
@@ -712,7 +712,9 @@
"ACTIVE": "Actif",
"INACTIVE": "Inactif",
"ARCHIVED": "Archivé",
- "NOT_ARCHIVED": "Non archivé"
+ "NOT_ARCHIVED": "Non archivé",
+ "VISITED_SITES": "Sites visités",
+ "NO_VISITED_SITE_MESSAGE": "Il n'y a pas de sites visités"
},
"task": {
"TITLE": "Tâche",
diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json
index 0585c26a2..66b50a7a8 100644
--- a/apps/web/locales/he.json
+++ b/apps/web/locales/he.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "התחל לעבוד",
"TIMER_RUNNING": "הטיימר כבר פועל",
"WARNING_PLAN_ESTIMATION": "נא לתקן את שעות העבודה המתוכננות או להעריך מחדש את המשימות"
- }
+ },
+ "VISITED_SITES": "אתרים שביקרת בהם",
+ "NO_VISITED_SITE_MESSAGE": "אין אתרים שביקרת בהם"
},
"task": {
"TITLE": "משימה",
diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json
index bc5cfffae..a3705a141 100644
--- a/apps/web/locales/it.json
+++ b/apps/web/locales/it.json
@@ -712,7 +712,9 @@
"ACTIVE": "Attivo",
"INACTIVE": "Inattivo",
"ARCHIVED": "Archiviato",
- "NOT_ARCHIVED": "Non archiviato"
+ "NOT_ARCHIVED": "Non archiviato",
+ "VISITED_SITES": "Siti visitati",
+ "NO_VISITED_SITE_MESSAGE": "Non ci sono siti visitati"
},
"task": {
"TITLE": "Compito",
diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json
index 74f3f8ef3..bf1e458ed 100644
--- a/apps/web/locales/nl.json
+++ b/apps/web/locales/nl.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "Begin met werken",
"TIMER_RUNNING": "De timer loopt al",
"WARNING_PLAN_ESTIMATION": "Corrigeer de geplande werkuren of schat de taak(en) opnieuw in"
- }
+ },
+ "VISITED_SITES": "Bezochte sites",
+ "NO_VISITED_SITE_MESSAGE": "Er zijn geen bezochte sites"
},
"task": {
"TITLE": "Taak",
diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json
index ac6b4c0bb..7e4031e89 100644
--- a/apps/web/locales/pl.json
+++ b/apps/web/locales/pl.json
@@ -712,7 +712,9 @@
"ACTIVE": "Aktywny",
"INACTIVE": "Nieaktywny",
"ARCHIVED": "Zarchiwizowany",
- "NOT_ARCHIVED": "Niezarchiwizowany"
+ "NOT_ARCHIVED": "Niezarchiwizowany",
+ "VISITED_SITES": "Odwiedzone strony",
+ "NO_VISITED_SITE_MESSAGE": "Brak odwiedzonych stron"
},
"task": {
"TITLE": "Zadanie",
diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json
index 18ece4aa3..d90b2329e 100644
--- a/apps/web/locales/pt.json
+++ b/apps/web/locales/pt.json
@@ -713,7 +713,9 @@
"ACTIVE": "Ativo",
"INACTIVE": "Inativo",
"ARCHIVED": "Arquivado",
- "NOT_ARCHIVED": "Não arquivado"
+ "NOT_ARCHIVED": "Não arquivado",
+ "VISITED_SITES": "Sites visitados",
+ "NO_VISITED_SITE_MESSAGE": "Não há sites visitados"
},
"task": {
"TITLE": "Tarefa",
diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json
index 3f3ce5ef0..8b0d4e13b 100644
--- a/apps/web/locales/ru.json
+++ b/apps/web/locales/ru.json
@@ -712,7 +712,9 @@
"ACTIVE": "Активно",
"INACTIVE": "Неактивно",
"ARCHIVED": "Архивировано",
- "NOT_ARCHIVED": "Не архивировано"
+ "NOT_ARCHIVED": "Не архивировано",
+ "VISITED_SITES": "Посещенные сайты",
+ "NO_VISITED_SITE_MESSAGE": "Нет посещенных сайтов"
},
"task": {
"TITLE": "Задача",
diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json
index 1b02827d3..67c371f61 100644
--- a/apps/web/locales/zh.json
+++ b/apps/web/locales/zh.json
@@ -712,7 +712,9 @@
"START_WORKING_BUTTON": "开始工作",
"TIMER_RUNNING": "计时器已经在运行",
"WARNING_PLAN_ESTIMATION": "请更正计划工时或重新估算任务工时"
- }
+ },
+ "VISITED_SITES": "访问过的网站",
+ "NO_VISITED_SITE_MESSAGE": "没有访问过的网站"
},
"task": {
"TITLE": "任务",
From 5726bf3ba2bb42345707ffa46b8f03773387e345 Mon Sep 17 00:00:00 2001
From: sv7000
Date: Tue, 4 Feb 2025 11:29:46 +0530
Subject: [PATCH 02/11] Correctly positioning search bar
---
apps/web/lib/features/task/task-filters.tsx | 2 +-
apps/web/lib/features/user-profile-tasks.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx
index 30c3f2b0e..d9035c94a 100644
--- a/apps/web/lib/features/task/task-filters.tsx
+++ b/apps/web/lib/features/task/task-filters.tsx
@@ -544,7 +544,7 @@ export function TaskNameFilter({
};
return (
-
+
+
{tabFiltered.tab === 'worked' &&
(profile.member?.employee?.isTrackingTime || (profile.isAuthUser && timerStatus?.running)) &&
otherTasks.length > 0 && (
From 27d75b6a726f56fadb040ccc6ab3ba674ef16dd1 Mon Sep 17 00:00:00 2001
From: sv7000
Date: Tue, 4 Feb 2025 20:50:33 +0530
Subject: [PATCH 03/11] Label Button Light Theme Border Correct
---
apps/web/lib/features/auth-user-task-input.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/lib/features/auth-user-task-input.tsx b/apps/web/lib/features/auth-user-task-input.tsx
index 49a412208..e440ffda1 100644
--- a/apps/web/lib/features/auth-user-task-input.tsx
+++ b/apps/web/lib/features/auth-user-task-input.tsx
@@ -53,7 +53,7 @@ export function AuthUserTaskInput({ className }: IClassName) {
task={activeTeamTask}
className="lg:max-w-[170px] grow text-xs"
forDetails={false}
- taskStatusClassName="dark:bg-[#1B1D22] text-xs dark:border dark:border-[#fff]"
+ taskStatusClassName="dark:bg-[#1B1D22] text-xs border dark:border-[#fff]"
/>
{activeTeamTask && (
Date: Wed, 5 Feb 2025 15:38:18 +0530
Subject: [PATCH 04/11] Correct confirm dropdown visiblility
---
apps/web/lib/components/dropdown.tsx | 2 +-
apps/web/lib/features/task/task-input.tsx | 2 +-
apps/web/lib/features/task/task-item.tsx | 6 +++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/web/lib/components/dropdown.tsx b/apps/web/lib/components/dropdown.tsx
index fe93d301d..956fdb249 100644
--- a/apps/web/lib/components/dropdown.tsx
+++ b/apps/web/lib/components/dropdown.tsx
@@ -182,7 +182,7 @@ export function ConfirmDropdown({
-
+
{confirmText}
diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx
index cc8955162..13f1b2c05 100644
--- a/apps/web/lib/features/task/task-input.tsx
+++ b/apps/web/lib/features/task/task-input.tsx
@@ -675,7 +675,7 @@ function TaskCard({
task={task}
selected={active}
onClick={onItemClick}
- className="cursor-pointer"
+ className="cursor-pointer overflow-y-auto"
/>
{!last && }
diff --git a/apps/web/lib/features/task/task-item.tsx b/apps/web/lib/features/task/task-item.tsx
index 850e2234b..6dca32a2c 100644
--- a/apps/web/lib/features/task/task-item.tsx
+++ b/apps/web/lib/features/task/task-item.tsx
@@ -33,7 +33,7 @@ export function TaskItem({ task, selected, onClick, className }: Props) {
return (
onClick && task && task.status !== 'closed' && onClick(task)}
>
e.stopPropagation()}>
{task?.status !== 'closed' && (
- handleChange('closed')} confirmText={'Confirm'}>
- {updateLoading ? : }
+ handleChange('closed')} confirmText={'Confirm'} className='fixed z-50'>
+ {updateLoading ? : }
)}
From b7d70860cc92795e64760c3f00d026e21a63aca3 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Thu, 6 Feb 2025 23:56:32 +0200
Subject: [PATCH 05/11] [Feat]: Add pagination size dropdown (#3593)
* feat: add pagination size control to team stats table
- Replace custom pagination select with PaginationDropdown component
- Improve layout and styling of pagination controls
- Move pagination size control next to entries count
- Update text color to #111827 for better visibility
* feat: make pagination options dynamic based on data size
* fix: dropdown
---
.../[teamId]/components/team-stats-table.tsx | 39 ++++++++++++-------
apps/web/lib/settings/page-dropdown.tsx | 30 ++++++++++++--
2 files changed, 52 insertions(+), 17 deletions(-)
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
index 076ac9fc0..984583aff 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
@@ -3,6 +3,7 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { PaginationDropdown } from '@/lib/settings/page-dropdown';
import { format } from 'date-fns';
import { ITimerLogGrouped } from '@/app/interfaces';
import { Spinner } from '@/components/ui/loaders/spinner';
@@ -34,8 +35,6 @@ const formatPercentage = (value: number) => {
return `${Math.round(value)}%`;
};
-const ITEMS_PER_PAGE = 10;
-
export function TeamStatsTable({
rapportDailyActivity,
isLoading
@@ -44,9 +43,10 @@ export function TeamStatsTable({
isLoading?: boolean;
}) {
const [currentPage, setCurrentPage] = useState(1);
- const totalPages = rapportDailyActivity ? Math.ceil(rapportDailyActivity.length / ITEMS_PER_PAGE) : 0;
- const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
- const endIndex = startIndex + ITEMS_PER_PAGE;
+ const [pageSize, setPageSize] = useState(10);
+ const totalPages = rapportDailyActivity ? Math.ceil(rapportDailyActivity.length / pageSize) : 0;
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
const { openModal, closeModal, isOpen } = useModal();
const paginatedData = rapportDailyActivity?.slice(startIndex, endIndex);
@@ -68,7 +68,11 @@ export function TeamStatsTable({
}
if (!rapportDailyActivity?.length) {
- return
No data available
;
+ return (
+
+ No data available
+
+ );
}
return (
@@ -94,9 +98,7 @@ export function TeamStatsTable({
{paginatedData?.map((dayData) => (
-
+
{format(new Date(dayData.date), 'EEEE dd MMM yyyy')}
@@ -188,11 +190,7 @@ export function TeamStatsTable({
-
-
- Showing {startIndex + 1} to {Math.min(endIndex, rapportDailyActivity.length)} of{' '}
- {rapportDailyActivity.length} entries
-
+
@@ -220,6 +218,19 @@ export function TeamStatsTable({
+
+
{
+ setPageSize(value);
+ setCurrentPage(1);
+ }}
+ total={rapportDailyActivity?.length}
+ />
+
+ Showing {startIndex + 1} to {Math.min(endIndex, rapportDailyActivity?.length || 0)} of{' '}
+ {rapportDailyActivity?.length || 0} entries
+
+
);
diff --git a/apps/web/lib/settings/page-dropdown.tsx b/apps/web/lib/settings/page-dropdown.tsx
index 2ec58e82e..0899827b5 100644
--- a/apps/web/lib/settings/page-dropdown.tsx
+++ b/apps/web/lib/settings/page-dropdown.tsx
@@ -10,11 +10,29 @@ import { ChevronDownIcon } from 'lucide-react';
export const PaginationDropdown = ({
setValue,
- active
+ active,
+ total
}: {
setValue: Dispatch>;
active?: IPagination | null;
+ total?: number;
}) => {
+ const calculatePaginationOptions = useCallback((total = 0) => {
+ const baseOptions = [10, 20, 30,40, 50];
+
+ if (total > 50) {
+ const nextOption = Math.ceil(total / 10) * 10;
+ if (!baseOptions.includes(nextOption)) {
+ baseOptions.push(nextOption);
+ }
+ }
+ baseOptions.sort((a, b) => a - b);
+
+ return baseOptions.map(size => ({
+ title: size.toString()
+ }));
+ }, []);
+
const [paginationList, setPagination] = useState([
{
title: '10'
@@ -33,6 +51,12 @@ export const PaginationDropdown = ({
}
]);
+ useEffect(() => {
+ if (total) {
+ setPagination(calculatePaginationOptions(total));
+ }
+ }, [total, calculatePaginationOptions]);
+
const items: PaginationItems[] = useMemo(() => mappaginationItems(paginationList), [paginationList]);
const [open, setOpen] = useState(false);
const [paginationItem, setPaginationItem] = useState();
@@ -72,9 +96,9 @@ export const PaginationDropdown = ({
onClick={() => setOpen(!open)}
className={clsxm(
'input-border',
- 'w-full flex justify-between rounded-xl px-3 py-2 text-sm items-center',
+ 'flex justify-between items-center px-3 py-2 w-full text-sm rounded-xl',
'font-normal outline-none',
- 'py-0 font-medium h-[45px] w-[145px] z-10 outline-none dark:bg-dark--theme-light'
+ 'z-10 py-0 font-medium outline-none h-[45px] w-[145px] dark:bg-dark--theme-light'
)}
>
{paginationItem?.selectedLabel || (paginationItem?.Label && )} {' '}
From dc2dbe200aec49260afa155b6b5c2706dfbbc650 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Thu, 6 Feb 2025 23:58:13 +0200
Subject: [PATCH 06/11] [Feat]: Date picker settings init (#3586)
* feat: implement date picker settings initialization
- Replace settings button with Button component
- Add click handler to initialize current month
- Set date range to current month on settings click
- Improve button styling and accessibility
* feat: disable today and future dates in date picker
- Use subDays to include today in disabled range
- Only allow selection of past dates
* feat: improve future dates disabling in calendar
- Use startOfDay to properly disable all future dates
- Replace static disabled range with dynamic date comparison
- Ensure consistent behavior across timezones
* feat: reduce date picker interface size
- Reduce calendar to single month view
- Make buttons and spacing more compact
- Decrease text size and button heights
- Add shadow for better visual hierarchy
- Optimize layout for smaller screens
* feat: set current month as default date range
- Initialize date range with current month instead of last month
- Set calendar view to current month by default
- Remove unnecessary lastMonth calculation
---
.../[teamId]/components/date-range-picker.tsx | 81 +++++++++++--------
1 file changed, 49 insertions(+), 32 deletions(-)
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
index 1ff0fb4cf..842649d82 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
-import { ChevronDown } from 'lucide-react';
+import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
format,
@@ -17,11 +17,13 @@ import {
subMonths,
isSameMonth,
isSameYear,
- isEqual
+ isEqual,
+ startOfDay
} from 'date-fns';
import { DateRange } from 'react-day-picker';
import { useTranslations } from 'next-intl';
import { SettingsIcon } from './team-icon';
+import { CalendarIcon } from '@radix-ui/react-icons';
interface DateRangePickerProps {
className?: string;
@@ -30,12 +32,15 @@ interface DateRangePickerProps {
export function DateRangePicker({ className, onDateRangeChange }: DateRangePickerProps) {
const t = useTranslations();
- const [dateRange, setDateRange] = React.useState({
- from: new Date(),
- to: new Date()
+ const [dateRange, setDateRange] = React.useState(() => {
+ const today = new Date();
+ return {
+ from: startOfMonth(today),
+ to: endOfMonth(today)
+ };
});
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
- const [currentMonth, setCurrentMonth] = React.useState(new Date());
+ const [currentMonth, setCurrentMonth] = React.useState(() => new Date());
const handleDateRangeChange = (range: DateRange | undefined) => {
try {
@@ -166,36 +171,61 @@ export function DateRangePicker({ className, onDateRangeChange }: DateRangePicke
className
)}
>
+
{dateRange ? formatDateRange(dateRange) : t('common.SELECT')}
- {
- /* Add handler */
+ const today = new Date();
+ setCurrentMonth(today);
+ handleDateRangeChange({
+ from: startOfMonth(today),
+ to: endOfMonth(today)
+ });
+ setIsPopoverOpen(true);
}}
- title="Open settings"
- aria-label="Open settings"
- className="flex items-center justify-center gap-2 px-2 py-1 w-[36px] h-[36px] bg-white dark:bg-dark--theme-light border-l border-l-[#E4E4E7] dark:border-l-[#2D2D2D] rounded-r-md"
+ title="Open date settings"
+ aria-label="Open date settings"
+ size="icon"
+ className="flex items-center justify-center w-[36px] h-[36px] bg-white dark:bg-dark--theme-light border-l border-l-[#E4E4E7] dark:border-l-[#2D2D2D] rounded-r-md hover:bg-gray-50 dark:hover:bg-gray-800"
>
-
+
e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => e.stopPropagation()}
- className="p-0 w-auto dark:bg-dark--theme-light dark:border-[#2D2D2D] border border-[#E4E4E7] rounded-md"
+ className="p-0 w-auto dark:bg-dark--theme-light dark:border-[#2D2D2D] border border-[#E4E4E7] rounded-md shadow-lg"
align="center"
>
-
-
+
+
+ date >= startOfDay(new Date())}
+ />
+
+
{predefinedRanges.map((range) => (
))}
-
-
-
-
+
{
handleDateRangeChange(undefined);
setIsPopoverOpen(false);
@@ -234,6 +250,7 @@ export function DateRangePicker({ className, onDateRangeChange }: DateRangePicke
{t('common.CLEAR')}
{
setIsPopoverOpen(false);
}}
From 87fcf360708116aa5813152c09b1214b7c27ba04 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Fri, 7 Feb 2025 00:07:56 +0200
Subject: [PATCH 07/11] [Feat]: Improve timesheet and Activity reporting
(#3590)
* feat: improve timesheet and activity reporting
- Enhance date handling in useTimesheet hook
- Fix dependency array in useTimelogFilterOptions
- Update team dashboard components for better activity tracking
- Improve chart visualization and filtering options
- Update timer log interfaces
- Update yarn dependencies
Components affected:
- activity-modal
- team-dashboard-filter
- team-stats-chart
- useReportActivity
- useTimelogFilterOptions
- useTimesheet
- ITimerLog interface
* Refactor button styles for better maintainability and consistency.
---
apps/server-web/release/app/yarn.lock | 203 ++++
.../[teamId]/components/activity-modal.tsx | 10 +-
.../components/team-dashboard-filter.tsx | 13 +-
.../[teamId]/components/team-stats-chart.tsx | 45 +-
.../app/hooks/features/useReportActivity.ts | 393 ++++---
.../hooks/features/useTimelogFilterOptions.ts | 2 +-
apps/web/app/hooks/features/useTimesheet.ts | 955 +++++++++---------
apps/web/app/interfaces/timer/ITimerLog.ts | 2 +
yarn.lock | 30 +
9 files changed, 993 insertions(+), 660 deletions(-)
diff --git a/apps/server-web/release/app/yarn.lock b/apps/server-web/release/app/yarn.lock
index fb57ccd13..362404977 100644
--- a/apps/server-web/release/app/yarn.lock
+++ b/apps/server-web/release/app/yarn.lock
@@ -2,3 +2,206 @@
# yarn lockfile v1
+"@emnapi/runtime@^1.2.0":
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60"
+ integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==
+ dependencies:
+ tslib "^2.4.0"
+
+"@img/sharp-darwin-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
+ integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
+ optionalDependencies:
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+
+"@img/sharp-darwin-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
+ integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==
+ optionalDependencies:
+ "@img/sharp-libvips-darwin-x64" "1.0.4"
+
+"@img/sharp-libvips-darwin-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
+ integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
+
+"@img/sharp-libvips-darwin-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
+ integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==
+
+"@img/sharp-libvips-linux-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
+ integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==
+
+"@img/sharp-libvips-linux-arm@1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
+ integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==
+
+"@img/sharp-libvips-linux-s390x@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
+ integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==
+
+"@img/sharp-libvips-linux-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
+ integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==
+
+"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
+ integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==
+
+"@img/sharp-libvips-linuxmusl-x64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
+ integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==
+
+"@img/sharp-linux-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
+ integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-arm64" "1.0.4"
+
+"@img/sharp-linux-arm@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
+ integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-arm" "1.0.5"
+
+"@img/sharp-linux-s390x@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
+ integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-s390x" "1.0.4"
+
+"@img/sharp-linux-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
+ integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==
+ optionalDependencies:
+ "@img/sharp-libvips-linux-x64" "1.0.4"
+
+"@img/sharp-linuxmusl-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
+ integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==
+ optionalDependencies:
+ "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+
+"@img/sharp-linuxmusl-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
+ integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==
+ optionalDependencies:
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+
+"@img/sharp-wasm32@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
+ integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==
+ dependencies:
+ "@emnapi/runtime" "^1.2.0"
+
+"@img/sharp-win32-ia32@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
+ integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==
+
+"@img/sharp-win32-x64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342"
+ integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@^1.0.0, color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+ integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
+ integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
+ dependencies:
+ color-convert "^2.0.1"
+ color-string "^1.9.0"
+
+detect-libc@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
+ integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
+
+is-arrayish@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+ integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+semver@^7.6.3:
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+ integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
+sharp@^0.33.4:
+ version "0.33.5"
+ resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e"
+ integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==
+ dependencies:
+ color "^4.2.3"
+ detect-libc "^2.0.3"
+ semver "^7.6.3"
+ optionalDependencies:
+ "@img/sharp-darwin-arm64" "0.33.5"
+ "@img/sharp-darwin-x64" "0.33.5"
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+ "@img/sharp-libvips-darwin-x64" "1.0.4"
+ "@img/sharp-libvips-linux-arm" "1.0.5"
+ "@img/sharp-libvips-linux-arm64" "1.0.4"
+ "@img/sharp-libvips-linux-s390x" "1.0.4"
+ "@img/sharp-libvips-linux-x64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+ "@img/sharp-linux-arm" "0.33.5"
+ "@img/sharp-linux-arm64" "0.33.5"
+ "@img/sharp-linux-s390x" "0.33.5"
+ "@img/sharp-linux-x64" "0.33.5"
+ "@img/sharp-linuxmusl-arm64" "0.33.5"
+ "@img/sharp-linuxmusl-x64" "0.33.5"
+ "@img/sharp-wasm32" "0.33.5"
+ "@img/sharp-win32-ia32" "0.33.5"
+ "@img/sharp-win32-x64" "0.33.5"
+
+simple-swizzle@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+ integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+ dependencies:
+ is-arrayish "^0.3.1"
+
+tslib@^2.4.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
index 86c97485f..db7ba8699 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
@@ -28,7 +28,7 @@ const Circle = ({ color, dashArray, dashOffset = '0' }: CircleProps) => (
strokeWidth="20"
strokeDasharray={`${dashArray} 251.327`}
strokeDashoffset={dashOffset}
- className="transition-all duration-1000"
+ className="transition-all duration-1000 ease-in-out"
/>
);
@@ -43,12 +43,12 @@ const LegendItem = ({
time: string;
percentage: number;
}) => (
-
+
-
+
{time} ({percentage}%)
@@ -104,7 +104,7 @@ export const ActivityModal = ({ employeeLog, isOpen, closeModal }: ActivityModal
imageTitle={employeeLog.employee.fullName}
className="relative"
/>
-
+
{employeeLog.employee.fullName}
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-dashboard-filter.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-dashboard-filter.tsx
index 495e2f2ff..961e4cdb9 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-dashboard-filter.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-dashboard-filter.tsx
@@ -47,7 +47,7 @@ export const TeamDashboardFilter = React.memo(function TeamDashboardFilter() {
)}
-
+
@@ -61,13 +61,12 @@ export const TeamDashboardFilter = React.memo(function TeamDashboardFilter() {
className={cn(
'text-primary/10',
allteamsState.length > 0 && 'text-primary dark:text-primary-light'
- )}
- >
- {t('common.CLEAR')}
+ )}>
+ {t('common.CLEAR')} ({allteamsState.length})
team?.name ?? ''}
@@ -87,10 +86,12 @@ export const TeamDashboardFilter = React.memo(function TeamDashboardFilter() {
alluserState.length > 0 && 'text-primary dark:text-primary-light'
)}
>
- {t('common.CLEAR')}
+ {t('common.CLEAR')} ({alluserState.length})
{
const members = team.members ?? [];
return members.filter((member) => member && member.employee);
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-chart.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-chart.tsx
index 4834529d0..5f8033b5d 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-chart.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-chart.tsx
@@ -48,7 +48,7 @@ export function TeamStatsChart({ rapportChartActivity, isLoading }: TeamStatsCha
idle: true,
resumed: true
});
- const [groupBy,setGroupBy] = useState('daily');
+ const [groupBy, setGroupBy] = useState('daily');
const toggleLine = (line: keyof typeof visibleLines) => {
setVisibleLines((prev) => ({
@@ -57,6 +57,25 @@ export function TeamStatsChart({ rapportChartActivity, isLoading }: TeamStatsCha
}));
};
+ const baseButtonClass = 'gap-[10px] px-3 py-1.5 w-[87px] h-[25px] rounded-[16px] text-xs font-normal';
+ const buttonColors = {
+ tracked: {
+ base: 'bg-[#0088FE]',
+ hover: 'hover:bg-[#0088FE]/80'
+ },
+ manual: {
+ base: 'bg-[#DC2626]',
+ hover: 'hover:bg-[#DC2626]/80'
+ },
+ idle: {
+ base: 'bg-[#EAB308]',
+ hover: 'hover:bg-[#EAB308]/80'
+ },
+ resumed: {
+ base: 'bg-[#22C55E]',
+ hover: 'hover:bg-[#22C55E]/80'
+ }
+ };
const groupData = (data: ITimerDailyLog[], grouping: GroupBy) => {
if (grouping === 'daily') return data;
@@ -239,39 +258,31 @@ export function TeamStatsChart({ rapportChartActivity, isLoading }: TeamStatsCha
toggleLine('tracked')}
>
-
Tracked
toggleLine('manual')}
>
-
Manual
toggleLine('idle')}
>
-
Idle
toggleLine('resumed')}
>
-
Resumed
diff --git a/apps/web/app/hooks/features/useReportActivity.ts b/apps/web/app/hooks/features/useReportActivity.ts
index 9da2183b5..0f48c31ac 100644
--- a/apps/web/app/hooks/features/useReportActivity.ts
+++ b/apps/web/app/hooks/features/useReportActivity.ts
@@ -1,179 +1,238 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
-import { ITimeLogReportDailyChartProps} from '@/app/interfaces/timer/ITimerLog';
-import { getTimeLogReportDaily, getTimeLogReportDailyChart, getTimesheetStatisticsCounts } from '@/app/services/client/api/timer/timer-log';
+import { ITimeLogReportDailyChartProps } from '@/app/interfaces/timer/ITimerLog';
+import {
+ getTimeLogReportDaily,
+ getTimeLogReportDailyChart,
+ getTimesheetStatisticsCounts
+} from '@/app/services/client/api/timer/timer-log';
import { useAuthenticateUser } from './useAuthenticateUser';
import { useQuery } from '../useQuery';
import { useAtom } from 'jotai';
import { timeLogsRapportChartState, timeLogsRapportDailyState, timesheetStatisticsCountsState } from '@/app/stores';
import { TimeLogType } from '@/app/interfaces';
-
-export interface UseReportActivityProps extends Omit {
- logType?: TimeLogType[];
- activityLevel: {
- start: number;
- end: number;
- };
- start?: number;
- end?: number;
+import { useTimelogFilterOptions } from './useTimelogFilterOptions';
+
+export interface UseReportActivityProps
+ extends Omit {
+ logType?: TimeLogType[];
+ activityLevel: {
+ start: number;
+ end: number;
+ };
+ start?: number;
+ end?: number;
+ projectIds?: string[];
+ employeeIds?: string[];
+ teamIds?: string[];
}
-const defaultProps: Required> = {
- startDate: new Date().toISOString().split('T')[0],
- endDate: new Date().toISOString().split('T')[0],
- groupBy: 'date',
- activityLevel: {
- start: 0,
- end: 100
- },
- logType: [TimeLogType.TRACKED],
- start: 0,
- end: 100
+const defaultProps: Required<
+ Pick<
+ UseReportActivityProps,
+ | 'startDate'
+ | 'endDate'
+ | 'groupBy'
+ | 'activityLevel'
+ | 'logType'
+ | 'start'
+ | 'end'
+ | 'employeeIds'
+ | 'projectIds'
+ | 'teamIds'
+ >
+> = {
+ startDate: new Date().toISOString().split('T')[0],
+ endDate: new Date().toISOString().split('T')[0],
+ groupBy: 'date',
+ activityLevel: {
+ start: 0,
+ end: 100
+ },
+ logType: [TimeLogType.TRACKED],
+ start: 0,
+ end: 100,
+ employeeIds: [],
+ projectIds: [],
+ teamIds: []
};
export function useReportActivity() {
- const { user } = useAuthenticateUser();
- const [rapportChartActivity, setRapportChartActivity] = useAtom(timeLogsRapportChartState);
- const [rapportDailyActivity, setRapportDailyActivity] = useAtom(timeLogsRapportDailyState);
- const [statisticsCounts, setStatisticsCounts] = useAtom(timesheetStatisticsCountsState);
- const { loading: loadingTimeLogReportDailyChart, queryCall: queryTimeLogReportDailyChart } = useQuery(getTimeLogReportDailyChart);
- const { loading: loadingTimeLogReportDaily, queryCall: queryTimeLogReportDaily } = useQuery(getTimeLogReportDaily);
- const { loading: loadingTimesheetStatisticsCounts, queryCall: queryTimesheetStatisticsCounts } = useQuery(getTimesheetStatisticsCounts);
-
- const [currentFilters, setCurrentFilters] = useState>(defaultProps);
-
- // Memoize the merged props to avoid recalculation
- const getMergedProps = useMemo(() => {
- if (!user?.employee.organizationId) {
- return null;
- }
-
- return (customProps?: Partial) => {
- const merged = {
- ...defaultProps,
- ...currentFilters,
- ...(customProps || {}),
- organizationId: user.employee.organizationId,
- tenantId: user.tenantId ?? '',
- logType: (customProps?.logType || currentFilters.logType || defaultProps.logType) as TimeLogType[],
- startDate: (customProps?.startDate || currentFilters.startDate || defaultProps.startDate) as string,
- endDate: (customProps?.endDate || currentFilters.endDate || defaultProps.endDate) as string,
- activityLevel: {
- start: customProps?.activityLevel?.start ?? currentFilters.activityLevel?.start ?? defaultProps.activityLevel.start,
- end: customProps?.activityLevel?.end ?? currentFilters.activityLevel?.end ?? defaultProps.activityLevel.end
- },
- start: customProps?.start ?? currentFilters.start ?? defaultProps.start,
- end: customProps?.end ?? currentFilters.end ?? defaultProps.end
- };
- return merged as Required;
- };
- }, [user?.employee.organizationId, user?.tenantId, currentFilters]);
-
- // Generic fetch function to reduce code duplication
- const fetchReport = useCallback(async (
- queryFn: typeof queryTimeLogReportDailyChart | typeof queryTimeLogReportDaily | typeof queryTimesheetStatisticsCounts,
- setData: ((data: T[]) => void) | null,
- customProps?: Partial
- ) => {
- if (!user || !getMergedProps) {
- if (setData) {
- setData([]);
- }
- return;
- }
-
- try {
- const mergedProps = getMergedProps(customProps);
- const response = await queryFn(mergedProps);
-
- if (setData && Array.isArray(response.data)) {
- setData(response.data as T[]);
- }
-
- if (customProps) {
- setCurrentFilters(prev => ({
- ...prev,
- ...customProps
- }));
- }
- } catch (err) {
- console.error('Failed to fetch report:', err);
- if (setData) {
- setData([]);
- }
- }
- }, [user, getMergedProps]);
-
- const fetchReportActivity = useCallback((customProps?: Partial) =>
- fetchReport(queryTimeLogReportDailyChart, setRapportChartActivity, customProps),
- [fetchReport, queryTimeLogReportDailyChart, setRapportChartActivity]);
-
- const fetchDailyReport = useCallback((customProps?: Partial) =>
- fetchReport(queryTimeLogReportDaily, setRapportDailyActivity, customProps),
- [fetchReport, queryTimeLogReportDaily, setRapportDailyActivity]);
-
-
-
- const fetchStatisticsCounts = useCallback(async (customProps?: Partial) => {
- if (!user || !getMergedProps) {
- return;
- }
- try {
- const mergedProps = getMergedProps(customProps);
- const response = await queryTimesheetStatisticsCounts({...mergedProps, logType:[TimeLogType.TRACKED]});
- setStatisticsCounts(response.data);
- if (customProps) {
- setCurrentFilters(prev => ({
- ...prev,
- ...customProps
- }));
- }
- } catch (error) {
- console.error('Error fetching statistics:', error);
- setStatisticsCounts(null);
- }
- }, [user, getMergedProps, queryTimesheetStatisticsCounts, setStatisticsCounts]);
-
- const updateDateRange = useCallback((startDate: Date, endDate: Date) => {
- const newProps = {
- startDate: startDate.toISOString().split('T')[0],
- endDate: endDate.toISOString().split('T')[0]
- };
-
- Promise.all([
- fetchReportActivity(newProps),
- fetchDailyReport(newProps),
- fetchStatisticsCounts(newProps)
- ]).catch(console.error);
- }, [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]);
-
- const updateFilters = useCallback((newFilters: Partial) => {
- Promise.all([
- fetchReportActivity(newFilters),
- fetchDailyReport(newFilters),
- fetchStatisticsCounts(newFilters)
- ]).catch(console.error);
- }, [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]);
-
- useEffect(() => {
- if (user) {
- Promise.all([
- fetchReportActivity(),
- fetchDailyReport(),
- fetchStatisticsCounts()
- ]).catch(console.error);
- }
- }, [user, fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]);
-
- return {
- loadingTimeLogReportDailyChart,
- loadingTimeLogReportDaily,
- loadingTimesheetStatisticsCounts,
- rapportChartActivity,
- rapportDailyActivity,
- statisticsCounts,
- updateDateRange,
- updateFilters,
- currentFilters,
- setStatisticsCounts,
- };
+ const { user } = useAuthenticateUser();
+ const [rapportChartActivity, setRapportChartActivity] = useAtom(timeLogsRapportChartState);
+ const [rapportDailyActivity, setRapportDailyActivity] = useAtom(timeLogsRapportDailyState);
+ const [statisticsCounts, setStatisticsCounts] = useAtom(timesheetStatisticsCountsState);
+ const { allteamsState, alluserState } = useTimelogFilterOptions();
+
+ const { loading: loadingTimeLogReportDailyChart, queryCall: queryTimeLogReportDailyChart } =
+ useQuery(getTimeLogReportDailyChart);
+ const { loading: loadingTimeLogReportDaily, queryCall: queryTimeLogReportDaily } = useQuery(getTimeLogReportDaily);
+ const { loading: loadingTimesheetStatisticsCounts, queryCall: queryTimesheetStatisticsCounts } =
+ useQuery(getTimesheetStatisticsCounts);
+
+ const [currentFilters, setCurrentFilters] = useState>(defaultProps);
+
+ // Memoize the merged props to avoid recalculation
+ const getMergedProps = useMemo(() => {
+ if (!user?.employee.organizationId) {
+ return null;
+ }
+
+ return (customProps?: Partial) => {
+ const merged = {
+ ...defaultProps,
+ ...currentFilters,
+ ...(customProps || {}),
+ organizationId: user.employee.organizationId,
+ teamId: customProps?.teamId || currentFilters.teamId,
+ userId: customProps?.userId || currentFilters.userId,
+ tenantId: user.tenantId ?? '',
+ logType: (customProps?.logType || currentFilters.logType || defaultProps.logType) as TimeLogType[],
+ startDate: (customProps?.startDate || currentFilters.startDate || defaultProps.startDate) as string,
+ endDate: (customProps?.endDate || currentFilters.endDate || defaultProps.endDate) as string,
+ projectIds: (customProps?.projectIds ||
+ currentFilters.projectIds ||
+ defaultProps.projectIds) as string[],
+ employeeIds: alluserState?.map(({ employee: { id } }) => id).filter(Boolean),
+ teamIds:allteamsState?.map(({ id }) => id).filter(Boolean),
+ activityLevel: {
+ start:
+ customProps?.activityLevel?.start ??
+ currentFilters.activityLevel?.start ??
+ defaultProps.activityLevel.start,
+ end:
+ customProps?.activityLevel?.end ??
+ currentFilters.activityLevel?.end ??
+ defaultProps.activityLevel.end
+ },
+ start: customProps?.start ?? currentFilters.start ?? defaultProps.start,
+ end: customProps?.end ?? currentFilters.end ?? defaultProps.end
+ };
+ return merged as Required;
+ };
+ }, [user?.employee.organizationId, user?.tenantId, currentFilters, alluserState, allteamsState]);
+
+ // Generic fetch function to reduce code duplication
+ const fetchReport = useCallback(
+ async (
+ queryFn:
+ | typeof queryTimeLogReportDailyChart
+ | typeof queryTimeLogReportDaily
+ | typeof queryTimesheetStatisticsCounts,
+ setData: ((data: T[]) => void) | null,
+ customProps?: Partial
+ ) => {
+ if (!user || !getMergedProps) {
+ if (setData) {
+ setData([]);
+ }
+ return;
+ }
+
+ try {
+ const mergedProps = getMergedProps(customProps);
+ const response = await queryFn(mergedProps);
+
+ if (setData && Array.isArray(response.data)) {
+ setData(response.data as T[]);
+ }
+
+ if (customProps) {
+ setCurrentFilters((prev) => ({
+ ...prev,
+ ...customProps
+ }));
+ }
+ } catch (err) {
+ console.error('Failed to fetch report:', err);
+ if (setData) {
+ setData([]);
+ }
+ }
+ },
+ [user, getMergedProps]
+ );
+
+ const fetchReportActivity = useCallback(
+ (customProps?: Partial) =>
+ fetchReport(queryTimeLogReportDailyChart, setRapportChartActivity, customProps),
+ [fetchReport, queryTimeLogReportDailyChart, setRapportChartActivity]
+ );
+
+ const fetchDailyReport = useCallback(
+ (customProps?: Partial) =>
+ fetchReport(queryTimeLogReportDaily, setRapportDailyActivity, customProps),
+ [fetchReport, queryTimeLogReportDaily, setRapportDailyActivity]
+ );
+
+ const fetchStatisticsCounts = useCallback(
+ async (customProps?: Partial) => {
+ if (!user || !getMergedProps) {
+ return;
+ }
+ try {
+ const mergedProps = getMergedProps(customProps);
+ const response = await queryTimesheetStatisticsCounts({
+ ...mergedProps,
+ logType: [TimeLogType.TRACKED]
+ });
+ setStatisticsCounts(response.data);
+ if (customProps) {
+ setCurrentFilters((prev) => ({
+ ...prev,
+ ...customProps
+ }));
+ }
+ } catch (error) {
+ console.error('Error fetching statistics:', error);
+ setStatisticsCounts(null);
+ }
+ },
+ [user, getMergedProps, queryTimesheetStatisticsCounts, setStatisticsCounts]
+ );
+
+ const updateDateRange = useCallback(
+ (startDate: Date, endDate: Date) => {
+ const newProps = {
+ startDate: startDate.toISOString().split('T')[0],
+ endDate: endDate.toISOString().split('T')[0]
+ };
+
+ Promise.all([
+ fetchReportActivity(newProps),
+ fetchDailyReport(newProps),
+ fetchStatisticsCounts(newProps)
+ ]).catch(console.error);
+ },
+ [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]
+ );
+
+ const updateFilters = useCallback(
+ (newFilters: Partial) => {
+ Promise.all([
+ fetchReportActivity(newFilters),
+ fetchDailyReport(newFilters),
+ fetchStatisticsCounts(newFilters)
+ ]).catch(console.error);
+ },
+ [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]
+ );
+
+ useEffect(() => {
+ if (user) {
+ Promise.all([fetchReportActivity(), fetchDailyReport(), fetchStatisticsCounts()]).catch(console.error);
+ }
+ }, [user, fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]);
+
+ return {
+ loadingTimeLogReportDailyChart,
+ loadingTimeLogReportDaily,
+ loadingTimesheetStatisticsCounts,
+ rapportChartActivity,
+ rapportDailyActivity,
+ statisticsCounts,
+ updateDateRange,
+ updateFilters,
+ currentFilters,
+ setStatisticsCounts
+ };
}
diff --git a/apps/web/app/hooks/features/useTimelogFilterOptions.ts b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
index b93b3c2b0..54097af5b 100644
--- a/apps/web/app/hooks/features/useTimelogFilterOptions.ts
+++ b/apps/web/app/hooks/features/useTimelogFilterOptions.ts
@@ -71,7 +71,7 @@ export function useTimelogFilterOptions() {
React.useEffect(() => {
return () => setSelectTimesheetId([]);
- }, []);
+ }, [setSelectTimesheetId]);
return {
statusState,
diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts
index c8a38cfe6..e36101eff 100644
--- a/apps/web/app/hooks/features/useTimesheet.ts
+++ b/apps/web/app/hooks/features/useTimesheet.ts
@@ -2,148 +2,163 @@ import { useAuthenticateUser } from './useAuthenticateUser';
import { useAtom } from 'jotai';
import { timesheetRapportState } from '@/app/stores/time-logs';
import { useQuery } from '../useQuery';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { deleteTaskTimesheetLogsApi, getTaskTimesheetLogsApi, updateStatusTimesheetFromApi, createTimesheetFromApi, updateTimesheetFromAPi } from '@/app/services/client/api/timer/timer-log';
+import { useCallback, useEffect, useMemo } from 'react';
+import {
+ deleteTaskTimesheetLogsApi,
+ getTaskTimesheetLogsApi,
+ updateStatusTimesheetFromApi,
+ createTimesheetFromApi,
+ updateTimesheetFromAPi
+} from '@/app/services/client/api/timer/timer-log';
import moment from 'moment';
import { ID, TimesheetLog, TimesheetStatus, UpdateTimesheet } from '@/app/interfaces';
import { useTimelogFilterOptions } from './useTimelogFilterOptions';
import axios from 'axios';
interface TimesheetParams {
- startDate?: Date | string;
- endDate?: Date | string;
- timesheetViewMode?: 'ListView' | 'CalendarView'
- inputSearch?: string
+ startDate?: Date | string;
+ endDate?: Date | string;
+ timesheetViewMode?: 'ListView' | 'CalendarView';
+ inputSearch?: string;
}
export interface GroupedTimesheet {
- date: string;
- tasks: TimesheetLog[];
+ date: string;
+ tasks: TimesheetLog[];
}
-
const groupByDate = (items: TimesheetLog[]): GroupedTimesheet[] => {
- if (!items?.length) return [];
-
- // First, group by timesheetId
- const groupedByTimesheet = items.reduce((acc, item) => {
- if (!item?.timesheet?.id || !item?.timesheet.createdAt) {
- console.warn('Skipping item with missing timesheet or createdAt:', item);
- return acc;
- }
- const timesheetId = item.timesheet.id;
- if (!acc[timesheetId]) {
- acc[timesheetId] = [];
- }
- acc[timesheetId].push(item);
- return acc;
- }, {} as Record);
-
- // Then, for each timesheet group, group by date and merge all results
- const result: GroupedTimesheet[] = [];
- Object.values(groupedByTimesheet).forEach(timesheetLogs => {
- const groupedByDate = timesheetLogs.reduce((acc, item) => {
- try {
- const date = new Date(item.timesheet.createdAt).toISOString().split('T')[0];
- if (!acc[date]) acc[date] = [];
- acc[date].push(item);
- } catch (error) {
- console.error(
- `Failed to process date for timesheet ${item.timesheet.id}:`,
- { createdAt: item.timesheet.createdAt, error }
- );
- }
- return acc;
- }, {} as Record);
-
- // Convert grouped dates to array format and add to results
- Object.entries(groupedByDate).forEach(([date, tasks]) => {
- result.push({ date, tasks });
- });
- });
-
- // Sort by date in descending order
- return result.sort((a, b) => b.date.localeCompare(a.date));
+ if (!items?.length) return [];
+
+ // First, group by timesheetId
+ const groupedByTimesheet = items.reduce(
+ (acc, item) => {
+ if (!item?.timesheet?.id || !item?.timesheet.createdAt) {
+ console.warn('Skipping item with missing timesheet or createdAt:', item);
+ return acc;
+ }
+ const timesheetId = item.timesheet.id;
+ if (!acc[timesheetId]) {
+ acc[timesheetId] = [];
+ }
+ acc[timesheetId].push(item);
+ return acc;
+ },
+ {} as Record
+ );
+
+ // Then, for each timesheet group, group by date and merge all results
+ const result: GroupedTimesheet[] = [];
+ Object.values(groupedByTimesheet).forEach((timesheetLogs) => {
+ const groupedByDate = timesheetLogs.reduce(
+ (acc, item) => {
+ try {
+ const date = new Date(item.timesheet.createdAt).toISOString().split('T')[0];
+ if (!acc[date]) acc[date] = [];
+ acc[date].push(item);
+ } catch (error) {
+ console.error(`Failed to process date for timesheet ${item.timesheet.id}:`, {
+ createdAt: item.timesheet.createdAt,
+ error
+ });
+ }
+ return acc;
+ },
+ {} as Record
+ );
+
+ // Convert grouped dates to array format and add to results
+ Object.entries(groupedByDate).forEach(([date, tasks]) => {
+ result.push({ date, tasks });
+ });
+ });
+
+ // Sort by date in descending order
+ return result.sort((a, b) => b.date.localeCompare(a.date));
};
const getWeekYearKey = (date: Date): string => {
- try {
- const year = date.getFullYear();
- const startOfYear = new Date(year, 0, 1);
- const days = Math.floor((date.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000));
- const week = Math.ceil((days + startOfYear.getDay() + 1) / 7);
- return `${year}-W${week.toString().padStart(2, '0')}`;
- } catch (error) {
- console.error('Error in getWeekYearKey:', error);
- return '';
- }
+ try {
+ const year = date.getFullYear();
+ const startOfYear = new Date(year, 0, 1);
+ const days = Math.floor((date.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000));
+ const week = Math.ceil((days + startOfYear.getDay() + 1) / 7);
+ return `${year}-W${week.toString().padStart(2, '0')}`;
+ } catch (error) {
+ console.error('Error in getWeekYearKey:', error);
+ return '';
+ }
};
const getMonthKey = (date: Date): string => {
- try {
- const year = date.getFullYear();
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
- return `${year}-${month}`;
- } catch (error) {
- console.error('Error in getMonthKey:', error);
- return '';
- }
+ try {
+ const year = date.getFullYear();
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ return `${year}-${month}`;
+ } catch (error) {
+ console.error('Error in getMonthKey:', error);
+ return '';
+ }
};
type GroupingKeyFunction = (date: Date) => string;
-const createGroupingFunction = (getKey: GroupingKeyFunction) => (items: TimesheetLog[]): GroupedTimesheet[] => {
- if (!items?.length) return [];
- const groupedByDate = items.reduce((acc, item) => {
- if (!item?.timesheet?.createdAt) {
- console.warn('Skipping item with missing createdAt:', item);
- return acc;
- }
-
- try {
- const date = new Date(item.timesheet.createdAt);
- if (isNaN(date.getTime())) {
- console.warn('Invalid date:', item.timesheet.createdAt);
- return acc;
- }
-
- const key = getKey(date);
- if (!acc[key]) {
- acc[key] = {
- date: key,
- timesheets: {}
- };
- }
-
- const timesheetId = item.timesheet.id;
- if (!acc[key].timesheets[timesheetId]) {
- acc[key].timesheets[timesheetId] = [];
- }
- acc[key].timesheets[timesheetId].push(item);
-
- } catch (error) {
- console.warn('Error processing date:', error);
- }
- return acc;
- }, {} as Record }>);
-
- return Object.values(groupedByDate)
- .map(({ date, timesheets }) => ({
- date,
- tasks: Object.values(timesheets)
- .flat()
- .sort((a, b) => {
- if (a.timesheet.id !== b.timesheet.id) {
- return a.timesheet.id.localeCompare(b.timesheet.id);
- }
- const dateA = new Date(a.timesheet.createdAt);
- const dateB = new Date(b.timesheet.createdAt);
- return dateB.getTime() - dateA.getTime();
- })
- }))
- .sort((a, b) => b.date.localeCompare(a.date));
-};
-
-const groupByWeek = createGroupingFunction(date => getWeekYearKey(date));
+const createGroupingFunction =
+ (getKey: GroupingKeyFunction) =>
+ (items: TimesheetLog[]): GroupedTimesheet[] => {
+ if (!items?.length) return [];
+ const groupedByDate = items.reduce(
+ (acc, item) => {
+ if (!item?.timesheet?.createdAt) {
+ console.warn('Skipping item with missing createdAt:', item);
+ return acc;
+ }
+
+ try {
+ const date = new Date(item.timesheet.createdAt);
+ if (isNaN(date.getTime())) {
+ console.warn('Invalid date:', item.timesheet.createdAt);
+ return acc;
+ }
+
+ const key = getKey(date);
+ if (!acc[key]) {
+ acc[key] = {
+ date: key,
+ timesheets: {}
+ };
+ }
+
+ const timesheetId = item.timesheet.id;
+ if (!acc[key].timesheets[timesheetId]) {
+ acc[key].timesheets[timesheetId] = [];
+ }
+ acc[key].timesheets[timesheetId].push(item);
+ } catch (error) {
+ console.warn('Error processing date:', error);
+ }
+ return acc;
+ },
+ {} as Record }>
+ );
+
+ return Object.values(groupedByDate)
+ .map(({ date, timesheets }) => ({
+ date,
+ tasks: Object.values(timesheets)
+ .flat()
+ .sort((a, b) => {
+ if (a.timesheet.id !== b.timesheet.id) {
+ return a.timesheet.id.localeCompare(b.timesheet.id);
+ }
+ const dateA = new Date(a.timesheet.createdAt);
+ const dateB = new Date(b.timesheet.createdAt);
+ return dateB.getTime() - dateA.getTime();
+ })
+ }))
+ .sort((a, b) => b.date.localeCompare(a.date));
+ };
+
+const groupByWeek = createGroupingFunction((date) => getWeekYearKey(date));
const groupByMonth = createGroupingFunction(getMonthKey);
/**
@@ -177,347 +192,359 @@ const groupByMonth = createGroupingFunction(getMonthKey);
* @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 = moment().startOf('month').toDate(),
- endDate = moment().endOf('month').toDate(),
- timesheetViewMode,
- inputSearch
-}: TimesheetParams) {
- const { user } = useAuthenticateUser();
- const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
- const { employee, project, task, statusState, timesheetGroupByDays, puTimesheetStatus, isUserAllowedToAccess, normalizeText, setSelectTimesheetId, selectTimesheetId, handleSelectRowByStatusAndDate, handleSelectRowTimesheet } = useTimelogFilterOptions();
- const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
- const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
- const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } = useQuery(updateStatusTimesheetFromApi)
- const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi);
- const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi);
- const isManage = user && isUserAllowedToAccess(user);
-
- const [createTimesheetResponse, setCreateTimesheetResponse] = useState(null);
-
- /**
- * Memoized date range with fallback to defaults
- * Ensures all dates are converted to Date objects
- */
- const currentDateRange = useMemo(() => {
- const defaultStart = moment().startOf('month').toDate();
- const defaultEnd = moment().endOf('month').toDate();
-
- return {
- startDate: startDate ? moment(startDate).toDate() : defaultStart,
- endDate: endDate ? moment(endDate).toDate() : defaultEnd
- };
- }, [startDate, endDate]);
-
- /**
- * Format date to YYYY-MM-DD ensuring valid date input
- * @param date - Input date (optional)
- * @param defaultDate - Default date if input is undefined
- */
- const formatDate = useCallback((date: Date | string | undefined, defaultDate: Date): string => {
- try {
- return moment(date || defaultDate).format('YYYY-MM-DD');
- } catch (error) {
- console.warn('Invalid date provided, using default date');
- return moment(defaultDate).format('YYYY-MM-DD');
- }
- }, []);
-
- const getTaskTimesheet = useCallback(
- ({ startDate, endDate }: TimesheetParams) => {
- if (!user) return;
-
- const from = formatDate(startDate, currentDateRange.startDate);
- const to = formatDate(endDate, currentDateRange.endDate);
- queryTimesheet({
- startDate: from,
- endDate: to,
- organizationId: user.employee?.organizationId,
- tenantId: user.tenantId ?? '',
- timeZone: user.timeZone?.split('(')[0].trim() || 'UTC',
- employeeIds: isManage
- ? employee?.map(({ employee: { id } }) => id).filter(Boolean)
- : [user.employee.id],
- projectIds: project?.map((project) => project.id).filter((id) => id !== undefined),
- taskIds: task?.map((task) => task.id).filter((id) => id !== undefined),
- status: statusState?.map((status) => status.value).filter((value) => value !== undefined)
- }).then((response) => {
- setTimesheet(response.data);
- }).catch((error) => {
- console.error('Error fetching timesheet:', error);
- });
- },
- [user, formatDate, currentDateRange.startDate, currentDateRange.endDate, queryTimesheet, isManage, employee, project, task, statusState, setTimesheet]
- );
-
- const createTimesheet = useCallback(
- async ({ ...timesheetParams }: UpdateTimesheet) => {
- if (!user) {
- throw new Error("User not authenticated");
- }
- try {
- const response = await queryCreateTimesheet(timesheetParams);
- setCreateTimesheetResponse(response.data);
- return response.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- console.error('Axios Error:', {
- status: error.response?.status,
- statusText: error.response?.statusText,
- data: error.response?.data
- });
- throw new Error(`Request failed: ${error.message}`);
- }
- console.error('Error:', error instanceof Error ? error.message : error);
- throw error;
- }
- },
- [queryCreateTimesheet, user]
- );
-
- const updateTimesheet = useCallback(
- async (timesheet: UpdateTimesheet) => {
- if (!user) {
- console.warn("User not authenticated!");
- return;
- }
- try {
- const response = await queryUpdateTimesheet(timesheet);
- if (response?.data?.id) {
- setTimesheet((prevTimesheet) =>
- prevTimesheet.map((item) =>
- item.id === response.data.id
- ? { ...item, ...response.data }
- : item
- )
- );
- } else {
- console.warn(
- "Unexpected structure of the response. No update performed.",
- response
- );
- }
- } catch (error) {
- console.error("Error updating the timesheet:", error);
- throw error;
- }
- },
- [queryUpdateTimesheet, setTimesheet, user]
- );
-
-
-
- const updateTimesheetStatus = useCallback(
- async ({ status, ids }: { status: TimesheetStatus; ids: ID[] | ID }) => {
- if (!user) return;
- const idsArray = Array.isArray(ids) ? ids : [ids];
- try {
- const response = await queryUpdateTimesheetStatus({ ids: idsArray, status });
- const responseMap = new Map(response.data.map(item => [item.id, item]));
- setTimesheet(prevTimesheet =>
- prevTimesheet.map(item => {
- const updatedItem = responseMap.get(item.timesheet.id);
- if (updatedItem) {
- return {
- ...item,
- timesheet: {
- ...item.timesheet,
- status: updatedItem.status
- }
- };
- }
- return item;
- })
- );
- console.log('Timesheet status updated successfully!');
- } catch (error) {
- console.error('Error updating timesheet status:', error);
- }
- },
- [queryUpdateTimesheetStatus, setTimesheet, user]
- );
-
- const getStatusTimesheet = (items: TimesheetLog[] = []) => {
- const STATUS_MAP: Record = {
- PENDING: [],
- APPROVED: [],
- DENIED: [],
- DRAFT: [],
- 'IN REVIEW': []
- };
-
- return items.reduce((acc, item) => {
- const status = item.timesheet.status;
- if (isTimesheetStatus(status)) {
- acc[status].push(item);
- } else {
- console.warn(`Invalid timesheet status: ${status}`);
- }
- return acc;
- }, STATUS_MAP);
- }
-
- // Type guard
- function isTimesheetStatus(status: unknown): status is TimesheetStatus {
- const timesheetStatusValues: TimesheetStatus[] = [
- "DRAFT",
- "PENDING",
- "IN REVIEW",
- "DENIED",
- "APPROVED"
- ];
- return Object.values(timesheetStatusValues).includes(status as TimesheetStatus);
- }
-
-
- const deleteTaskTimesheet = useCallback(async ({ logIds }: { logIds: string[] }) => {
- if (!user) {
- throw new Error('User not authenticated');
- }
- if (!logIds.length) {
- throw new Error('No timesheet IDs provided for deletion');
- }
- try {
- await queryDeleteTimesheet({
- organizationId: user.employee.organizationId,
- tenantId: user.tenantId ?? "",
- logIds
- });
- setTimesheet(prevTimesheet =>
- prevTimesheet.filter(item => !logIds.includes(item.id))
- );
-
- } catch (error) {
- console.error('Failed to delete timesheets:', error);
- throw error;
- }
- }, [user, queryDeleteTimesheet, setTimesheet]);
-
- const groupedByTimesheetIds = ({ rows }: { rows: TimesheetLog[] }): Record => {
- if (!rows) {
- return {};
- }
- return rows.reduce((acc, row) => {
- if (!row) {
- return acc;
- }
- const timesheetId = row.timesheetId ?? 'unassigned';
- if (!acc[timesheetId]) {
- acc[timesheetId] = [];
- }
- acc[timesheetId].push(row);
- return acc;
- }, {} as Record);
- }
-
-
- 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, normalizeText]);
-
- const reGroupByDate = (groupedTimesheets: GroupedTimesheet[]): GroupedTimesheet[] => {
- return groupedTimesheets.reduce((acc, { date, tasks }) => {
- const existingGroup = acc.find(group => group.date === date);
- if (existingGroup) {
- existingGroup.tasks = existingGroup.tasks.concat(tasks);
- } else {
- acc.push({ date, tasks });
- }
- return acc;
- }, [] as GroupedTimesheet[]);
- };
-
- const timesheetElementGroup = useMemo(() => {
- if (!timesheet) {
- return [];
- }
-
- if (timesheetViewMode === 'ListView') {
- const groupedTimesheets = groupByDate(filterDataTimesheet);
- const reGroupedByDate = reGroupByDate(groupedTimesheets);
- switch (timesheetGroupByDays) {
- case 'Daily':
- return reGroupedByDate;
- case 'Weekly':
- return groupByWeek(filterDataTimesheet);
- case 'Monthly':
- return groupByMonth(filterDataTimesheet);
- default:
- return reGroupedByDate;
- }
- }
- return reGroupByDate(groupByDate(filterDataTimesheet));
- }, [timesheet, timesheetViewMode, filterDataTimesheet, timesheetGroupByDays]);
-
- const rowsToObject = (rows: TimesheetLog[]): Record => {
- return rows.reduce((acc, row) => {
- acc[row.timesheet.id] = { task: row, status: row.timesheet.status }
- return acc;
- }, {} as Record);
- };
-
-
- /**
- * Combined effect for fetching timesheet data
- * Handles both initial load and updates after timesheet creation
- */
- useEffect(() => {
- if (createTimesheetResponse) {
- getTaskTimesheet(currentDateRange);
- setCreateTimesheetResponse(null);
- } else {
- getTaskTimesheet(currentDateRange);
- }
- }, [
- getTaskTimesheet,
- currentDateRange,
- createTimesheetResponse,
- timesheetGroupByDays,
- inputSearch
- ]);
-
- return {
- loadingTimesheet,
- timesheet: timesheetElementGroup,
- getTaskTimesheet,
- loadingDeleteTimesheet,
- deleteTaskTimesheet,
- getStatusTimesheet,
- timesheetGroupByDays,
- statusTimesheet: getStatusTimesheet(filterDataTimesheet.flat()),
- updateTimesheetStatus,
- loadingUpdateTimesheetStatus,
- puTimesheetStatus,
- createTimesheet,
- loadingCreateTimesheet,
- updateTimesheet,
- loadingUpdateTimesheet,
- groupByDate,
- isManage,
- normalizeText,
- setSelectTimesheetId,
- selectTimesheetId,
- handleSelectRowByStatusAndDate,
- handleSelectRowTimesheet,
- groupedByTimesheetIds,
- rowsToObject
- };
+export function useTimesheet({ startDate, endDate, timesheetViewMode, inputSearch }: TimesheetParams) {
+ const { user } = useAuthenticateUser();
+ const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
+ const {
+ employee,
+ project,
+ task,
+ statusState,
+ timesheetGroupByDays,
+ puTimesheetStatus,
+ isUserAllowedToAccess,
+ normalizeText,
+ setSelectTimesheetId,
+ selectTimesheetId,
+ handleSelectRowByStatusAndDate,
+ handleSelectRowTimesheet
+ } = useTimelogFilterOptions();
+ const { loading: loadingTimesheet, queryCall: queryTimesheet } = useQuery(getTaskTimesheetLogsApi);
+ const { loading: loadingDeleteTimesheet, queryCall: queryDeleteTimesheet } = useQuery(deleteTaskTimesheetLogsApi);
+ const { loading: loadingUpdateTimesheetStatus, queryCall: queryUpdateTimesheetStatus } =
+ useQuery(updateStatusTimesheetFromApi);
+ const { loading: loadingCreateTimesheet, queryCall: queryCreateTimesheet } = useQuery(createTimesheetFromApi);
+ const { loading: loadingUpdateTimesheet, queryCall: queryUpdateTimesheet } = useQuery(updateTimesheetFromAPi);
+ const isManage = user && isUserAllowedToAccess(user);
+
+ /**
+ * Memoized date range with fallback to defaults
+ * Ensures all dates are converted to Date objects
+ */
+ const currentDateRange = useMemo(() => {
+ const now = moment();
+ const defaultStart = now.clone().startOf('month').toDate();
+ const defaultEnd = now.clone().endOf('month').toDate();
+
+ const parseDate = (date: Date | string | undefined, defaultValue: Date) => {
+ if (!date) return defaultValue;
+ const parsed = moment(date);
+ return parsed.isValid() ? parsed.toDate() : defaultValue;
+ };
+
+ return {
+ startDate: parseDate(startDate, defaultStart),
+ endDate: parseDate(endDate, defaultEnd)
+ };
+ }, [startDate, endDate]);
+
+ /**
+ * Format date to YYYY-MM-DD ensuring valid date input
+ * @param date - Input date (optional)
+ * @returns Formatted date string
+ */
+ const formatDate = useCallback((date: Date | string | undefined): string => {
+ if (!date) {
+ return moment().format('YYYY-MM-DD');
+ }
+
+ if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
+ return date;
+ }
+
+ try {
+ const parsedDate = moment(date);
+ if (!parsedDate.isValid()) {
+ console.warn('Invalid date provided, using current date');
+ return moment().format('YYYY-MM-DD');
+ }
+ return parsedDate.format('YYYY-MM-DD');
+ } catch (error) {
+ console.warn('Error parsing date:', error);
+ return moment().format('YYYY-MM-DD');
+ }
+ }, []);
+
+ const getTaskTimesheet = useCallback(
+ ({ startDate, endDate }: TimesheetParams) => {
+ if (!user) return;
+
+ const from = moment(startDate).format('YYYY-MM-DD');
+ const to = moment(endDate).format('YYYY-MM-DD');
+
+ queryTimesheet({
+ startDate: from,
+ endDate: to,
+ organizationId: user.employee?.organizationId,
+ tenantId: user.tenantId ?? '',
+ timeZone: user.timeZone?.split('(')[0].trim() || 'UTC',
+ employeeIds: isManage
+ ? employee?.map(({ employee: { id } }) => id).filter(Boolean)
+ : [user.employee.id],
+ projectIds: project?.map((project) => project.id).filter((id) => id !== undefined),
+ taskIds: task?.map((task) => task.id).filter((id) => id !== undefined),
+ status: statusState?.map((status) => status.value).filter((value) => value !== undefined)
+ })
+ .then((response) => {
+ setTimesheet(response.data);
+ })
+ .catch((error) => {
+ console.error('Error fetching timesheet:', error);
+ });
+ },
+ [user, queryTimesheet, isManage, employee, project, task, statusState, setTimesheet]
+ );
+
+ useEffect(() => {
+ if (startDate || endDate) {
+ getTaskTimesheet({ startDate, endDate });
+ }
+ }, [startDate, endDate, getTaskTimesheet]);
+
+ const createTimesheet = useCallback(
+ async ({ ...timesheetParams }: UpdateTimesheet) => {
+ if (!user) {
+ throw new Error('User not authenticated');
+ }
+ try {
+ const response = queryCreateTimesheet(timesheetParams).then((res) => {
+ return res.data;
+ });
+ return response;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ console.error('Axios Error:', {
+ status: error.response?.status,
+ statusText: error.response?.statusText,
+ data: error.response?.data
+ });
+ throw new Error(`Request failed: ${error.message}`);
+ }
+ console.error('Error:', error instanceof Error ? error.message : error);
+ throw error;
+ }
+ },
+ [queryCreateTimesheet, user]
+ );
+
+ const updateTimesheet = useCallback(
+ async (timesheet: UpdateTimesheet) => {
+ if (!user) {
+ console.warn('User not authenticated!');
+ return;
+ }
+ try {
+ const response = await queryUpdateTimesheet(timesheet);
+ if (response?.data?.id) {
+ setTimesheet((prevTimesheet) =>
+ prevTimesheet.map((item) =>
+ item.id === response.data.id ? { ...item, ...response.data } : item
+ )
+ );
+ } else {
+ console.warn('Unexpected structure of the response. No update performed.', response);
+ }
+ } catch (error) {
+ console.error('Error updating the timesheet:', error);
+ throw error;
+ }
+ },
+ [queryUpdateTimesheet, setTimesheet, user]
+ );
+
+ const updateTimesheetStatus = useCallback(
+ async ({ status, ids }: { status: TimesheetStatus; ids: ID[] | ID }) => {
+ if (!user) return;
+ const idsArray = Array.isArray(ids) ? ids : [ids];
+ try {
+ const response = await queryUpdateTimesheetStatus({ ids: idsArray, status });
+ const responseMap = new Map(response.data.map((item) => [item.id, item]));
+ setTimesheet((prevTimesheet) =>
+ prevTimesheet.map((item) => {
+ const updatedItem = responseMap.get(item.timesheet.id);
+ if (updatedItem) {
+ return {
+ ...item,
+ timesheet: {
+ ...item.timesheet,
+ status: updatedItem.status
+ }
+ };
+ }
+ return item;
+ })
+ );
+ console.log('Timesheet status updated successfully!');
+ } catch (error) {
+ console.error('Error updating timesheet status:', error);
+ }
+ },
+ [queryUpdateTimesheetStatus, setTimesheet, user]
+ );
+
+ const getStatusTimesheet = (items: TimesheetLog[] = []) => {
+ const STATUS_MAP: Record = {
+ PENDING: [],
+ APPROVED: [],
+ DENIED: [],
+ DRAFT: [],
+ 'IN REVIEW': []
+ };
+
+ return items.reduce((acc, item) => {
+ const status = item.timesheet.status;
+ if (isTimesheetStatus(status)) {
+ acc[status].push(item);
+ } else {
+ console.warn(`Invalid timesheet status: ${status}`);
+ }
+ return acc;
+ }, STATUS_MAP);
+ };
+
+ // Type guard
+ function isTimesheetStatus(status: unknown): status is TimesheetStatus {
+ const timesheetStatusValues: TimesheetStatus[] = ['DRAFT', 'PENDING', 'IN REVIEW', 'DENIED', 'APPROVED'];
+ return Object.values(timesheetStatusValues).includes(status as TimesheetStatus);
+ }
+
+ const deleteTaskTimesheet = useCallback(
+ async ({ logIds }: { logIds: string[] }) => {
+ if (!user) {
+ throw new Error('User not authenticated');
+ }
+ if (!logIds.length) {
+ throw new Error('No timesheet IDs provided for deletion');
+ }
+ try {
+ await queryDeleteTimesheet({
+ organizationId: user.employee.organizationId,
+ tenantId: user.tenantId ?? '',
+ logIds
+ });
+ setTimesheet((prevTimesheet) => prevTimesheet.filter((item) => !logIds.includes(item.id)));
+ } catch (error) {
+ console.error('Failed to delete timesheets:', error);
+ throw error;
+ }
+ },
+ [user, queryDeleteTimesheet, setTimesheet]
+ );
+
+ const groupedByTimesheetIds = ({ rows }: { rows: TimesheetLog[] }): Record => {
+ if (!rows) {
+ return {};
+ }
+ return rows.reduce(
+ (acc, row) => {
+ if (!row) {
+ return acc;
+ }
+ const timesheetId = row.timesheetId ?? 'unassigned';
+ if (!acc[timesheetId]) {
+ acc[timesheetId] = [];
+ }
+ acc[timesheetId].push(row);
+ return acc;
+ },
+ {} as Record
+ );
+ };
+
+ 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, normalizeText]);
+
+ const reGroupByDate = (groupedTimesheets: GroupedTimesheet[]): GroupedTimesheet[] => {
+ return groupedTimesheets.reduce((acc, { date, tasks }) => {
+ const existingGroup = acc.find((group) => group.date === date);
+ if (existingGroup) {
+ existingGroup.tasks = existingGroup.tasks.concat(tasks);
+ } else {
+ acc.push({ date, tasks });
+ }
+ return acc;
+ }, [] as GroupedTimesheet[]);
+ };
+
+ const timesheetElementGroup = useMemo(() => {
+ if (!timesheet) {
+ return [];
+ }
+
+ if (timesheetViewMode === 'ListView') {
+ const groupedTimesheets = groupByDate(filterDataTimesheet);
+ const reGroupedByDate = reGroupByDate(groupedTimesheets);
+ switch (timesheetGroupByDays) {
+ case 'Daily':
+ return reGroupedByDate;
+ case 'Weekly':
+ return groupByWeek(filterDataTimesheet);
+ case 'Monthly':
+ return groupByMonth(filterDataTimesheet);
+ default:
+ return reGroupedByDate;
+ }
+ }
+ return reGroupByDate(groupByDate(filterDataTimesheet));
+ }, [timesheet, timesheetViewMode, filterDataTimesheet, timesheetGroupByDays]);
+
+ const rowsToObject = (rows: TimesheetLog[]): Record => {
+ return rows.reduce(
+ (acc, row) => {
+ acc[row.timesheet.id] = { task: row, status: row.timesheet.status };
+ return acc;
+ },
+ {} as Record
+ );
+ };
+
+ useEffect(() => {
+ getTaskTimesheet({ startDate, endDate });
+ }, [getTaskTimesheet, startDate, endDate, timesheetGroupByDays, inputSearch]);
+
+ return {
+ loadingTimesheet,
+ timesheet: timesheetElementGroup,
+ getTaskTimesheet,
+ loadingDeleteTimesheet,
+ deleteTaskTimesheet,
+ getStatusTimesheet,
+ timesheetGroupByDays,
+ statusTimesheet: getStatusTimesheet(filterDataTimesheet.flat()),
+ updateTimesheetStatus,
+ loadingUpdateTimesheetStatus,
+ puTimesheetStatus,
+ createTimesheet,
+ loadingCreateTimesheet,
+ updateTimesheet,
+ loadingUpdateTimesheet,
+ groupByDate,
+ isManage,
+ normalizeText,
+ setSelectTimesheetId,
+ selectTimesheetId,
+ handleSelectRowByStatusAndDate,
+ handleSelectRowTimesheet,
+ groupedByTimesheetIds,
+ rowsToObject,
+ formatDate,
+ currentDateRange
+ };
}
diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts
index 451d71497..858f38298 100644
--- a/apps/web/app/interfaces/timer/ITimerLog.ts
+++ b/apps/web/app/interfaces/timer/ITimerLog.ts
@@ -179,6 +179,8 @@ export interface ITimeLogReportDailyChartProps {
logType?: TimeLogType[];
teamIds?: string[];
groupBy?: string;
+ teamId?: string;
+ userId?: string;
}
export interface IOrganizationContact {
diff --git a/yarn.lock b/yarn.lock
index 7187db4b6..5a33a032e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1207,6 +1207,13 @@
dependencies:
regenerator-runtime "^0.14.0"
+"@babel/runtime@^7.17.9":
+ version "7.26.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341"
+ integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/runtime@^7.20.7":
version "7.24.6"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz"
@@ -2760,6 +2767,19 @@
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
+"@hcaptcha/loader@^1.2.1":
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@hcaptcha/loader/-/loader-1.2.4.tgz#541714395a82e27ec0f0e8bd80ef1a0bea141cc3"
+ integrity sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw==
+
+"@hcaptcha/react-hcaptcha@^1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.11.1.tgz#cc0dd0b180b3458ca7b0f8cf1446f09afca2ec55"
+ integrity sha512-g6TwatNIzBtOR3RM4mxzvTUQGs5T9HMN+4fcNGHn7wUVThvmazThUs0vImI836bSkGpJS8n0rOYvv1UZ47q8Vw==
+ dependencies:
+ "@babel/runtime" "^7.17.9"
+ "@hcaptcha/loader" "^1.2.1"
+
"@headlessui/react@^1.7.7":
version "1.7.17"
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz"
@@ -12399,6 +12419,11 @@ csstype@^3.0.2:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+custom-electron-titlebar@^4.2.8:
+ version "4.2.8"
+ resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-4.2.8.tgz#8b0d024cf372b10c9758cc512ca55498b69616da"
+ integrity sha512-JEFiOKJdSZtMh90FO90FeEqCc463ZZhuh78JxILvO9Nc0H8I9MfKekgCf6jZH6xccd3/tm1OcYOoUJi3JbXR/w==
+
cypress-file-upload@^5.0.8:
version "5.0.8"
resolved "https://registry.npmjs.org/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz"
@@ -23045,6 +23070,11 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
+react-turnstile@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/react-turnstile/-/react-turnstile-1.1.4.tgz#0c23b2f4b55f83b929407ae9bfbd211fbe5df362"
+ integrity sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ==
+
react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
From 9451b4c7b372d6b327a64c05992abbba6200a705 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Fri, 7 Feb 2025 08:34:12 +0200
Subject: [PATCH 08/11] [Feat]: Add rows per page (#3595)
* feat: improve TimesheetPagination component UI
* feat: improve TimesheetPagination component UI
---
.../components/TimesheetPagination.tsx | 170 +++++++++++-------
.../[locale]/timesheet/[memberId]/page.tsx | 15 +-
2 files changed, 117 insertions(+), 68 deletions(-)
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx
index 89d3c4720..5b6947993 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetPagination.tsx
@@ -1,17 +1,26 @@
-import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink } from '@components/ui/pagination'
-import React from 'react'
+import React from 'react';
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink
+} from '@components/ui/pagination';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@components/ui/select';
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
-
+ totalPages?: number;
+ onPageChange?: (page: number) => void;
+ nextPage?: () => void;
+ previousPage?: () => void;
+ goToPage: (page: number) => void;
+ currentPage?: number;
+ getPageNumbers: () => (number | string)[];
+ dates?: string[];
+ totalGroups?: number;
+ pageSize?: number;
+ pageSizeOptions?: number[];
+ onPageSizeChange?: (size: number) => void;
}
/**
@@ -28,58 +37,89 @@ interface TimesheetPaginationProps {
*
* @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}
-
- )}
-
- ))}
-
-
-
-
-
-
-
- )
- }
- >
- )
-
+function TimesheetPagination({
+ totalPages,
+ goToPage,
+ nextPage,
+ previousPage,
+ currentPage,
+ getPageNumbers,
+ dates,
+ totalGroups,
+ pageSize = 10,
+ pageSizeOptions = [10, 20, 30, 50],
+ onPageSizeChange
+}: TimesheetPaginationProps) {
+ return (
+ // totalPages > 1
+ <>
+ {totalPages && totalPages > 1 && (
+
+
+
+ {dates?.length || 0} of {totalGroups || 0} row(s) selected
+
+
+ Rows per page
+ onPageSizeChange?.(Number(value))}
+ >
+
+
+
+
+ {pageSizeOptions.map((size) => (
+
+ {size}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {getPageNumbers().map((pageNumber, index) => (
+
+ {pageNumber === '...' ? (
+
+ ) : (
+ goToPage(pageNumber as number)}
+ >
+ {pageNumber}
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+ )}
+ >
+ );
}
-export default TimesheetPagination
+export default TimesheetPagination;
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
index 4edb11365..3aa815d26 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
@@ -34,8 +34,6 @@ import { useTimesheetViewData } from '@/app/hooks/features/useTimesheetViewData'
type TimesheetViewMode = 'ListView' | 'CalendarView';
export type TimesheetDetailMode = 'Pending' | 'MenHours' | 'MemberWork';
-const TIMESHEET_PAGE_SIZE = 10;
-
type ViewToggleButtonProps = {
mode: TimesheetViewMode;
active: boolean;
@@ -47,6 +45,14 @@ type ViewToggleButtonProps = {
const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memberId: string } }) {
const t = useTranslations();
const { user } = useAuthenticateUser();
+ const [pageSize, setPageSize] = useState(10);
+
+ const getPageSizeOptions = (total: number) => {
+ if (total <= 10) return [10];
+ if (total <= 20) return [10, 20];
+ if (total <= 30) return [10, 20, 30];
+ return [10, 20, 30, 50];
+ };
const { getOrganizationProjects } = useOrganizationProjects();
const { isTrackingEnabled, activeTeam } = useOrganizationTeams();
@@ -105,7 +111,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
dates
} = useTimesheetPagination({
data: filterDataTimesheet,
- pageSize: TIMESHEET_PAGE_SIZE
+ pageSize
});
const viewData = useTimesheetViewData({
timesheetNavigator,
@@ -330,6 +336,9 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
previousPage={previousPage}
dates={dates}
totalGroups={totalGroups}
+ pageSize={pageSize}
+ onPageSizeChange={setPageSize}
+ pageSizeOptions={getPageSizeOptions(totalGroups || 0)}
/>
)}
From 1c673505e09c1602b3704c08d757462af8c0e0f8 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Fri, 7 Feb 2025 08:34:48 +0200
Subject: [PATCH 09/11] [Feat]: Task date picker improvements (#3596)
* feat: enhance task date picker with data indicators
- Add visual indicators for dates with available data
- Improve date picker type safety with IDailyPlan interface
- Optimize date filtering and validation logic
- Add dot indicators to highlight dates with tasks
- Ensure proper date range selection behavior
* feat: enhance task date picker with data indicators
- Add visual indicators for dates with available data
- Improve date picker type safety with IDailyPlan interface
- Optimize date filtering and validation logic
- Add dot indicators to highlight dates with tasks
- Ensure proper date range selection behavior
* fix: deepscan
---
apps/web/app/hooks/useDateRange.ts | 10 +++---
apps/web/app/stores/daily-plan.ts | 9 +++++
.../web/lib/features/task/task-date-range.tsx | 33 ++++++++++++-----
apps/web/lib/features/task/task-filters.tsx | 2 +-
apps/web/lib/features/user-profile-plans.tsx | 36 +++++++++----------
5 files changed, 58 insertions(+), 32 deletions(-)
diff --git a/apps/web/app/hooks/useDateRange.ts b/apps/web/app/hooks/useDateRange.ts
index 3c7b75723..60050a275 100644
--- a/apps/web/app/hooks/useDateRange.ts
+++ b/apps/web/app/hooks/useDateRange.ts
@@ -2,23 +2,23 @@ import {
dateRangeAllPlanState,
dateRangeFuturePlanState,
dateRangePastPlanState,
- getFirstAndLastDateState
+ getPlanState
} from '@app/stores';
import { useAtom, useAtomValue } from 'jotai';
export const useDateRange = (tab: string | any) => {
- const itemsDate = useAtomValue(getFirstAndLastDateState);
+ const itemsPlans = useAtomValue(getPlanState);
const [dateFuture, setDateFuture] = useAtom(dateRangeFuturePlanState);
const [dateAllPlan, setDateAllPlan] = useAtom(dateRangeAllPlanState);
const [datePastPlan, setDatePastPlan] = useAtom(dateRangePastPlanState);
switch (tab) {
case 'Future Tasks':
- return { date: dateFuture, setDate: setDateFuture, data: itemsDate };
+ return { date: dateFuture, setDate: setDateFuture, data: itemsPlans };
case 'Past Tasks':
- return { date: datePastPlan, setDate: setDatePastPlan, data: itemsDate };
+ return { date: datePastPlan, setDate: setDatePastPlan, data: itemsPlans };
case 'All Tasks':
default:
- return { date: dateAllPlan, setDate: setDateAllPlan, data: itemsDate };
+ return { date: dateAllPlan, setDate: setDateAllPlan, data: itemsPlans };
}
};
diff --git a/apps/web/app/stores/daily-plan.ts b/apps/web/app/stores/daily-plan.ts
index d7c86a8d2..d973bd9e0 100644
--- a/apps/web/app/stores/daily-plan.ts
+++ b/apps/web/app/stores/daily-plan.ts
@@ -123,3 +123,12 @@ export const getFirstAndLastDateState = atom((get) => {
to: new Date(sortedData[sortedData.length - 1]?.date)
};
});
+
+
+export const getPlanState = atom((get) => {
+ const itemsData = get(dataDailyPlanState);
+ if (!itemsData?.length) return { data: [] };
+ return {
+ data: itemsData
+ };
+});
diff --git a/apps/web/lib/features/task/task-date-range.tsx b/apps/web/lib/features/task/task-date-range.tsx
index 096735849..abdf79a9b 100644
--- a/apps/web/lib/features/task/task-date-range.tsx
+++ b/apps/web/lib/features/task/task-date-range.tsx
@@ -13,6 +13,7 @@ import { DateRange } from 'react-day-picker';
import { SetStateAction } from 'jotai';
import moment from 'moment';
import { SetAtom } from 'types';
+import { IDailyPlan } from '@/app/interfaces';
interface ITaskDatePickerWithRange {
className?: string;
@@ -20,7 +21,7 @@ interface ITaskDatePickerWithRange {
onSelect?: SetAtom<[SetStateAction
], void>;
label?: string;
- data?: any;
+ data?: IDailyPlan[];
}
export function TaskDatePickerWithRange({
className,
@@ -29,11 +30,14 @@ export function TaskDatePickerWithRange({
label,
data
}: ITaskDatePickerWithRange) {
- const isDateDisabled = (dateToCheck: any) => {
- const { from, to }: any = data;
- const fromDate = new Date(moment(from)?.format('YYYY-MM-DD'));
- const toDate = new Date(moment(to)?.format('YYYY-MM-DD'));
- return dateToCheck < fromDate || dateToCheck > toDate;
+ const isDateDisabled = (dateToCheck: Date) => {
+ if (!data || !Array.isArray(data)) return true;
+
+ const checkDate = moment(dateToCheck).format('YYYY-MM-DD');
+ return !data.some(item => {
+ const itemDate = moment(item.date).format('YYYY-MM-DD');
+ return itemDate === checkDate;
+ });
};
const handleDateSelect = (newDate: DateRange | undefined) => {
if (onSelect) {
@@ -52,7 +56,7 @@ export function TaskDatePickerWithRange({
!date && 'text-muted-foreground'
)}
>
-
+
{date?.from ? (
date.to ? (
<>
@@ -67,7 +71,7 @@ export function TaskDatePickerWithRange({
)}
-
+
{
+ if (!data || !Array.isArray(data)) return false;
+ const checkDate = moment(date).format('YYYY-MM-DD');
+ return data.some(item => {
+ const itemDate = moment(item.date).format('YYYY-MM-DD');
+ return itemDate === checkDate;
+ });
+ }
+ }}
+ modifiersClassNames={{
+ hasData: 'relative before:absolute before:content-[""] before:w-1 before:h-1 before:bg-primary before:rounded-full before:bottom-1 before:left-1/2 before:-translate-x-1/2'
+ }}
/>
diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx
index d9035c94a..ff980728a 100644
--- a/apps/web/lib/features/task/task-filters.tsx
+++ b/apps/web/lib/features/task/task-filters.tsx
@@ -477,7 +477,7 @@ export function TaskStatusFilter({ hook, employeeId }: { hook: I_TaskFilter; emp
{hook.tab === 'dailyplan' && }
{['Future Tasks', 'Past Tasks', 'All Tasks'].includes(dailyPlanTab) && (
setDate(range)}
label="Planned date"
diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx
index b0471f39a..b2105e185 100644
--- a/apps/web/lib/features/user-profile-plans.tsx
+++ b/apps/web/lib/features/user-profile-plans.tsx
@@ -148,11 +148,11 @@ export function UserProfilePlans(props: IUserProfilePlansProps) {
<>
{profileDailyPlans?.items?.length > 0 ? (
-
+
-
+
{Object.keys(tabsScreens).map((filter, i) => (
-
+
{i !== 0 &&
}
))}
-
+
{currentTab === 'Today Tasks' && todayPlan[0] && (
<>
{canSeeActivity ? (
@@ -238,10 +238,10 @@ export function UserProfilePlans(props: IUserProfilePlansProps) {
}
}}
variant="destructive"
- className="flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:bg-red-400"
+ className="flex justify-center items-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:bg-red-400"
>
{deleteDailyPlanLoading && (
-
+
)}
{t('common.DELETE')}
@@ -364,8 +364,8 @@ function AllPlans({
className="dark:border-slate-600 !border-none"
>
-
-
+
+
{formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length})
@@ -518,7 +518,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
>
{/* Planned Time */}
-
+
{!editTime && !updateDailyPlanLoading ? (
<>
@@ -538,13 +538,13 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
min={0}
type="number"
className={clsxm(
- 'outline-none p-0 bg-transparent border-b text-center max-w-[54px] text-xs font-medium'
+ 'p-0 text-xs font-medium text-center bg-transparent border-b outline-none max-w-[54px]'
)}
onChange={(e) => setTime(parseFloat(e.target.value))}
/>
{updateDailyPlanLoading ? (
-
+
) : (
-
+
{t('dailyPlan.ESTIMATED_TIME')} :
{formatIntegerToHour(estimatedTime / 3600)}
@@ -571,7 +571,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
{/* Total worked time for the plan */}
{planMode !== 'Future Tasks' && (
-
+
{t('dailyPlan.TOTAL_TIME_WORKED')} :
{formatIntegerToHour(totalWorkTime / 3600)}
@@ -582,15 +582,15 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
{/* Completed tasks */}
{planMode !== 'Future Tasks' && (
-
+
{t('dailyPlan.COMPLETED_TASKS')} :
{`${completedTasks}/${totalTasks}`}
-
+
{t('dailyPlan.READY')}:
{readyTasks}
-
+
{t('dailyPlan.LEFT')}:
{totalTasks - completedTasks - readyTasks}
@@ -602,7 +602,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
{/* Completion progress */}
{planMode !== 'Future Tasks' && (
-
+
{t('dailyPlan.COMPLETION')}:
{completionPercent}%
@@ -613,7 +613,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil
{/* Future tasks total plan */}
{planMode === 'Future Tasks' && (
-
+
{t('dailyPlan.PLANNED_TASKS')}:
{totalTasks}
From 59ab50287319d9f803659c3fe0521d49801b9fd7 Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Fri, 7 Feb 2025 08:36:12 +0200
Subject: [PATCH 10/11] refactor: optimize date range picker component (#3592)
* refactor: optimize date range picker component
- Extract PredefinedRanges into a separate component for better maintainability
- Implement createRangeHelper factory function to reduce code duplication
- Add memoization for predefined ranges to prevent unnecessary recalculations
- Improve type safety with DateRangeGetter type
- Standardize date comparison using startOfDay
- Enhance UI with consistent button styling and layout
Performance improvements:
- Reduce redundant date calculations
- Prevent unnecessary re-renders with useMemo
- Centralize date range creation logic
Technical improvements:
- Better type safety
- More maintainable code structure
- Consistent date handling
* deepscan
* deepscan
* deepscan
* fix: memoize createRange function to prevent unnecessary re-renders in date-range-picker
* deepscan
* fix: conflit
---
.cspell.json | 1 +
.../[teamId]/components/date-range-picker.tsx | 235 +++++++++---------
.../components/TimesheetDetailModal.tsx | 28 +--
3 files changed, 139 insertions(+), 125 deletions(-)
diff --git a/.cspell.json b/.cspell.json
index eb482a503..a650d982e 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -110,6 +110,7 @@
"deserunt",
"digitaloceanspaces",
"dimesions",
+ "Ipredefine",
"discrepenancy",
"dolor",
"dolore",
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
index 842649d82..25ed7cbc0 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/date-range-picker.tsx
@@ -23,6 +23,7 @@ import {
import { DateRange } from 'react-day-picker';
import { useTranslations } from 'next-intl';
import { SettingsIcon } from './team-icon';
+import { TranslationHooks } from 'next-intl';
import { CalendarIcon } from '@radix-ui/react-icons';
interface DateRangePickerProps {
@@ -51,100 +52,6 @@ export function DateRangePicker({ className, onDateRangeChange }: DateRangePicke
}
};
- const predefinedRanges = [
- {
- label: t('common.TODAY'),
- action: () => {
- const today = new Date();
- handleDateRangeChange({ from: today, to: today });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const today = new Date();
- return isEqual(range.from, today) && isEqual(range.to, today);
- }
- },
- {
- label: t('common.YESTERDAY'),
- action: () => {
- const yesterday = subDays(new Date(), 1);
- handleDateRangeChange({ from: yesterday, to: yesterday });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const yesterday = subDays(new Date(), 1);
- return isEqual(range.from, yesterday) && isEqual(range.to, yesterday);
- }
- },
- {
- label: t('common.THIS_WEEK'),
- action: () => {
- const today = new Date();
- handleDateRangeChange({
- from: startOfWeek(today, { weekStartsOn: 1 }),
- to: endOfWeek(today, { weekStartsOn: 1 })
- });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const today = new Date();
- const weekStart = startOfWeek(today, { weekStartsOn: 1 });
- const weekEnd = endOfWeek(today, { weekStartsOn: 1 });
- return isEqual(range.from, weekStart) && isEqual(range.to, weekEnd);
- }
- },
- {
- label: t('common.LAST_WEEK'),
- action: () => {
- const lastWeek = subWeeks(new Date(), 1);
- handleDateRangeChange({
- from: startOfWeek(lastWeek, { weekStartsOn: 1 }),
- to: endOfWeek(lastWeek, { weekStartsOn: 1 })
- });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const lastWeek = subWeeks(new Date(), 1);
- const weekStart = startOfWeek(lastWeek, { weekStartsOn: 1 });
- const weekEnd = endOfWeek(lastWeek, { weekStartsOn: 1 });
- return isEqual(range.from, weekStart) && isEqual(range.to, weekEnd);
- }
- },
- {
- label: t('common.THIS_MONTH'),
- action: () => {
- const today = new Date();
- handleDateRangeChange({
- from: startOfMonth(today),
- to: endOfMonth(today)
- });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const today = new Date();
- const monthStart = startOfMonth(today);
- const monthEnd = endOfMonth(today);
- return isEqual(range.from, monthStart) && isEqual(range.to, monthEnd);
- }
- },
- {
- label: t('common.LAST_MONTH'),
- action: () => {
- const lastMonth = subMonths(new Date(), 1);
- handleDateRangeChange({
- from: startOfMonth(lastMonth),
- to: endOfMonth(lastMonth)
- });
- },
- isSelected: (range: DateRange | undefined) => {
- if (!range?.from || !range?.to) return false;
- const lastMonth = subMonths(new Date(), 1);
- const monthStart = startOfMonth(lastMonth);
- const monthEnd = endOfMonth(lastMonth);
- return isEqual(range.from, monthStart) && isEqual(range.to, monthEnd);
- }
- }
- ];
const formatDateRange = (range: DateRange) => {
if (!range.from) return 'Select date range';
@@ -219,23 +126,8 @@ export function DateRangePicker({ className, onDateRangeChange }: DateRangePicke
disabled={(date) => date >= startOfDay(new Date())}
/>
-
- {predefinedRanges.map((range) => (
-
{
- range.action();
- }}
- >
- {range.label}
-
- ))}
+
@@ -262,3 +154,124 @@ export function DateRangePicker({ className, onDateRangeChange }: DateRangePicke
);
}
+interface PredefinedRangeProps {
+ handleDateRangeChange: (range: DateRange | undefined) => void;
+ t: TranslationHooks;
+ dateRange: DateRange | undefined;
+}
+
+type DateRangeGetter = () => { from: Date; to: Date };
+
+const createRangeHelper = (handleDateRangeChange: (range: DateRange | undefined) => void) => {
+ return (getRange: DateRangeGetter) => {
+ const range = getRange();
+ return {
+ action: () => {
+ const newRange = {
+ from: new Date(range.from),
+ to: new Date(range.to)
+ };
+ handleDateRangeChange(newRange);
+ },
+ isSelected: (currentRange: DateRange | undefined) => {
+ if (!currentRange?.from || !currentRange?.to) return false;
+ return isEqual(
+ startOfDay(currentRange.from),
+ startOfDay(range.from)
+ ) && isEqual(
+ startOfDay(currentRange.to),
+ startOfDay(range.to)
+ );
+ }
+ };
+ };
+};
+
+const PredefinedRanges = ({ handleDateRangeChange, t, dateRange }: PredefinedRangeProps) => {
+ const weekOptions = { weekStartsOn: 1 as const };
+
+ const createRange = React.useMemo(
+ () => createRangeHelper(handleDateRangeChange),
+ [handleDateRangeChange]
+ );
+
+ const predefinedRanges = React.useMemo(
+ () => [
+ {
+ label: t('common.TODAY'),
+ ...createRange(() => {
+ const today = new Date();
+ return { from: today, to: today };
+ })
+ },
+ {
+ label: t('common.YESTERDAY'),
+ ...createRange(() => {
+ const yesterday = subDays(new Date(), 1);
+ return { from: yesterday, to: yesterday };
+ })
+ },
+ {
+ label: t('common.THIS_WEEK'),
+ ...createRange(() => {
+ const today = new Date();
+ return {
+ from: startOfWeek(today, weekOptions),
+ to: endOfWeek(today, weekOptions)
+ };
+ })
+ },
+ {
+ label: t('common.LAST_WEEK'),
+ ...createRange(() => {
+ const lastWeek = subWeeks(new Date(), 1);
+ return {
+ from: startOfWeek(lastWeek, weekOptions),
+ to: endOfWeek(lastWeek, weekOptions)
+ };
+ })
+ },
+ {
+ label: t('common.THIS_MONTH'),
+ ...createRange(() => {
+ const today = new Date();
+ return {
+ from: startOfMonth(today),
+ to: endOfMonth(today)
+ };
+ })
+ },
+ {
+ label: t('common.LAST_MONTH'),
+ ...createRange(() => {
+ const lastMonth = subMonths(new Date(), 1);
+ return {
+ from: startOfMonth(lastMonth),
+ to: endOfMonth(lastMonth)
+ };
+ })
+ }
+ ],
+ [createRange, t]
+ );
+
+ return (
+
+ {predefinedRanges.map((range) => (
+ {
+ range.action();
+ }}
+ >
+ {range.label}
+
+ ))}
+
+ );
+};
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx
index 33b3eeb05..f4f6c7e75 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetDetailModal.tsx
@@ -38,7 +38,7 @@ function TimesheetDetailModal({ closeModal, isOpen, timesheet, timesheetDetailMo
showCloseIcon
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded w-full md:w-40 md:min-w-[35rem]"
titleClass="font-bold flex justify-start w-full text-xl">
-
+
{(() => {
switch (timesheetDetailMode) {
@@ -89,13 +89,13 @@ const MembersWorkedCard = ({ element, t }: { element: TimesheetLog[], t: Transla
-
-
+
+
{timesheet.element[0].employee.fullName}
-
+
-
+
{status === 'DENIED' ? 'REJECTED' : status}
@@ -150,7 +150,7 @@ const MembersWorkedCard = ({ element, t }: { element: TimesheetLog[], t: Transla
borderBottomColor: statusColor(status).bg
}}
className={cn(
- 'flex items-center border-b border-b-gray-200 dark:border-b-gray-600 space-x-4 p-1 h-[60px]'
+ 'flex items-center p-1 space-x-4 border-b border-b-gray-200 dark:border-b-gray-600 h-[60px]'
)} >
-
+
{items.project?.imageUrl &&
}
{items.project?.name}
@@ -218,11 +218,11 @@ const MenHoursCard = ({ element, t }: MenHoursCardProps) => {
style={{ backgroundColor: statusColor(timesheet.element[0].timesheet.status).bgOpacity }}
type="button"
className={cn(
- 'flex flex-row-reverse justify-end items-center w-full h-[50px] rounded-sm gap-x-2 hover:no-underline px-2',
+ 'flex flex-row-reverse gap-x-2 justify-end items-center px-2 w-full rounded-sm h-[50px] hover:no-underline',
)}>
-
+
{timesheet.element[0].timesheet.status === 'DENIED' ? 'REJECTED' : timesheet.element[0].timesheet.status}
@@ -249,10 +249,10 @@ const MenHoursCard = ({ element, t }: MenHoursCardProps) => {
'flex flex-row-reverse justify-end items-center w-full h-[50px] rounded-sm gap-x-2 hover:no-underline px-2',
statusColor(status).text
)}>
-
+
-
+
{status === 'DENIED' ? 'REJECTED' : status}
@@ -278,7 +278,7 @@ const MenHoursCard = ({ element, t }: MenHoursCardProps) => {
borderBottomColor: statusColor(status).bg
}}
className={cn(
- 'flex items-center border-b border-b-gray-200 dark:border-b-gray-600 space-x-4 p-1 h-[60px]'
+ 'flex items-center p-1 space-x-4 border-b border-b-gray-200 dark:border-b-gray-600 h-[60px]'
)} >
{
taskNumberClassName="text-sm"
/>
-
+
{items.project?.imageUrl &&
}
{items.project?.name}
From e71a27cf1caa06e892a15a6dce7510ef9bbaebfb Mon Sep 17 00:00:00 2001
From: AKILIMAILI CIZUNGU Innocent
<51681130+Innocent-Akim@users.noreply.github.com>
Date: Fri, 7 Feb 2025 08:40:54 +0200
Subject: [PATCH 11/11] [Feat]: Improve activity modal UI (#3585)
* feat: improve activity modal and chart button UI
- Fix type error in ActivityModal component
- Improve modal styling and responsiveness
- Replace custom button with Button component
- Add accessibility attributes to chart button
- Optimize modal rendering with null check
* feat: improve activity modal and chart button UI
- Fix type error in ActivityModal component
- Improve modal styling and responsiveness
- Replace custom button with Button component
- Add accessibility attributes to chart button
- Optimize modal rendering with null check
* fix: conflit
---
.../[teamId]/components/activity-modal.tsx | 6 +-
.../[teamId]/components/team-stats-table.tsx | 301 ++++++++++--------
.../team-dashboard/[teamId]/page.tsx | 15 +-
3 files changed, 172 insertions(+), 150 deletions(-)
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
index db7ba8699..f07325619 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx
@@ -92,19 +92,19 @@ export const ActivityModal = ({ employeeLog, isOpen, closeModal }: ActivityModal
-
+
{employeeLog.employee.fullName}
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
index 984583aff..c1766c0d6 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-table.tsx
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { PaginationDropdown } from '@/lib/settings/page-dropdown';
import { format } from 'date-fns';
-import { ITimerLogGrouped } from '@/app/interfaces';
+import { ITimerEmployeeLog, ITimerLogGrouped } from '@/app/interfaces';
import { Spinner } from '@/components/ui/loaders/spinner';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { Fragment, useState } from 'react';
@@ -42,6 +42,9 @@ export function TeamStatsTable({
rapportDailyActivity?: ITimerLogGrouped[];
isLoading?: boolean;
}) {
+
+
+ const [employeeLog, setEmployeeLog] = useState
(undefined);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const totalPages = rapportDailyActivity ? Math.ceil(rapportDailyActivity.length / pageSize) : 0;
@@ -61,7 +64,7 @@ export function TeamStatsTable({
if (isLoading) {
return (
-
+
);
@@ -76,149 +79,163 @@ export function TeamStatsTable({
}
return (
-
-
-
-
-
-
-
-
- Member
- Total Time
- Tracked
- Manually Added
- Active Time
- Idle Time
- Unknown Activity
- Activity Level
-
-
-
-
- {paginatedData?.map((dayData) => (
-
-
-
- {format(new Date(dayData.date), 'EEEE dd MMM yyyy')}
-
-
- {dayData.logs?.map(
- (projectLog) =>
- projectLog.employeeLogs?.map((employeeLog) => (
-
-
-
-
-
-
- {employeeLog.employee?.user?.name?.[0] ||
- 'U'}
-
-
-
- {employeeLog.employee?.user?.name ||
- 'Unknown User'}
-
-
-
-
- {formatDuration(employeeLog.sum || 0)}
-
-
- {formatPercentage(employeeLog.activity)}
-
-
- {formatPercentage(0)}
-
-
- {formatPercentage(100)}
-
-
- {formatPercentage(0)}
-
-
- {formatPercentage(0)}
-
-
-
-
-
+ <>
+ {employeeLog && (
+
+ )}
+
+
+
+
+
+
+
+
+ Member
+ Total Time
+ Tracked
+ Manually Added
+ Active Time
+ Idle Time
+ Unknown Activity
+ Activity Level
+
+
+
+
+ {paginatedData?.map((dayData) => (
+
+
+
+ {format(new Date(dayData.date), 'EEEE dd MMM yyyy')}
+
+
+ {dayData.logs?.map(
+ (projectLog) =>
+ projectLog.employeeLogs?.map((employeeLog) => (
+
+
+
+
+
+
+ {employeeLog.employee?.user
+ ?.name?.[0] || 'U'}
+
+
+
+ {employeeLog.employee?.user?.name ||
+ 'Unknown User'}
+
-
- {(employeeLog.activity || 0).toFixed(1)}%
-
-
-
-
- <>
-
-
-
-
- >
-
-
- )) || []
- ) || []}
-
- ))}
-
-
+
+
+ {formatDuration(employeeLog.sum || 0)}
+
+
+ {formatPercentage(employeeLog.activity)}
+
+
+ {formatPercentage(0)}
+
+
+ {formatPercentage(100)}
+
+
+ {formatPercentage(0)}
+
+
+ {formatPercentage(0)}
+
+
+
+
+
+ {(employeeLog.activity || 0).toFixed(1)}%
+
+
+
+
+ <>
+ {
+ setEmployeeLog(employeeLog);
+ openModal();
+ }}
+ aria-label={`View activity chart for ${employeeLog.employee?.user?.name || 'employee'}`}
+ title="View activity chart">
+
+
+ >
+
+
+ )) || []
+ ) || []}
+
+ ))}
+
+
+
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
-
goToPage(page)}
- className={currentPage === page ? 'bg-primary text-primary-foreground' : ''}
- >
- {page}
-
- ))}
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
+ goToPage(page)}
+ className={currentPage === page ? 'bg-primary text-primary-foreground' : ''}
+ >
+ {page}
+
+ ))}
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
{
setPageSize(value);
@@ -231,7 +248,9 @@ export function TeamStatsTable({
{rapportDailyActivity?.length || 0} entries
+
+
-
+ >
);
}
diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx
index 467474126..c8ae6308b 100644
--- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx
+++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx
@@ -16,9 +16,11 @@ import { useAtomValue } from 'jotai';
import { fullWidthState } from '@app/stores/fullWidth';
import { withAuthentication } from '@/lib/app/authenticator';
import { useReportActivity } from '@/app/hooks/features/useReportActivity';
+import { useTranslations } from 'next-intl';
function TeamDashboard() {
const { activeTeam, isTrackingEnabled } = useOrganizationTeams();
+ const t=useTranslations();
const { rapportChartActivity, updateDateRange, updateFilters, loadingTimeLogReportDailyChart, rapportDailyActivity, loadingTimeLogReportDaily, statisticsCounts,loadingTimesheetStatisticsCounts} = useReportActivity();
const router = useRouter();
const fullWidth = useAtomValue(fullWidthState);
@@ -27,11 +29,13 @@ function TeamDashboard() {
const breadcrumbPath = useMemo(
() => [
+ { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' },
{ title: activeTeam?.name || '', href: '/' },
{ title: 'Team-Dashboard', href: `/${currentLocale}/dashboard/team-dashboard` }
],
- [activeTeam?.name, currentLocale]
+ [activeTeam?.name, currentLocale,t]
);
+
return (
-