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 new file mode 100644 index 000000000..86c97485f --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/activity-modal.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { Modal, Avatar } from '@/lib/components'; +import { ITimerEmployeeLog } from '@/app/interfaces/timer/ITimerLog'; +import { useState, useMemo } from 'react'; + +interface ActivityModalProps { + employeeLog: ITimerEmployeeLog; + isOpen: boolean; + closeModal: () => void; +} + +type TabType = 'tracked' | 'activity'; + +interface CircleProps { + color: string; + dashArray: string; + dashOffset?: string; +} + +const Circle = ({ color, dashArray, dashOffset = '0' }: CircleProps) => ( + +); + +const LegendItem = ({ + color, + label, + time, + percentage +}: { + color: string; + label: string; + time: string; + percentage: number; +}) => ( +
+
+
+ {label} +
+ + {time} ({percentage}%) + +
+); + +const formatTime = (minutes: number): string => { + const hours = Math.floor(minutes / 60); + const mins = Math.floor(minutes % 60); + return `${hours}h ${mins.toString().padStart(2, '0')}min`; +}; + +export const ActivityModal = ({ employeeLog, isOpen, closeModal }: ActivityModalProps) => { + const [activeTab, setActiveTab] = useState('activity'); + + const timeCalculations = useMemo(() => { + const totalTime = employeeLog.sum || 0; + return { + totalTime, + activeTime: totalTime * 0.57, + idleTime: totalTime * 0.13, + unknownTime: totalTime * 0.3, + trackedTime: totalTime * 0.75, + manualTime: totalTime * 0.25 + }; + }, [employeeLog.sum]); + + const { activeTime, idleTime, unknownTime, trackedTime, manualTime } = timeCalculations; + + const formattedTimes = useMemo( + () => ({ + active: formatTime(activeTime), + idle: formatTime(idleTime), + unknown: formatTime(unknownTime), + tracked: formatTime(trackedTime), + manual: formatTime(manualTime) + }), + [activeTime, idleTime, unknownTime, trackedTime, manualTime] + ); + + return ( + +
+
+
+ +

+ {employeeLog.employee.fullName} +

+
+
+ +
+ {(['tracked', 'activity'] as const).map((tab) => ( + + ))} +
+ +
+ {activeTab === 'tracked' ? ( +
+
+ + + + +
+ +
+ + +
+
+ ) : ( +
+
+ + + + + +
+ +
+ + + +
+
+ )} +
+
+
+ ); +}; diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-icon.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-icon.tsx new file mode 100644 index 000000000..a658310e9 --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-icon.tsx @@ -0,0 +1,30 @@ +export const ChartIcon = () => { + return ( + + + + + + ); +}; 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 0d4609fe7..964957e14 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 @@ -7,7 +7,10 @@ import { format } from 'date-fns'; import { ITimerLogGrouped } from '@/app/interfaces'; import { Spinner } from '@/components/ui/loaders/spinner'; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; -import { useState } from 'react'; +import { Fragment, useState } from 'react'; +import { ChartIcon } from './team-icon'; +import { ActivityModal } from './activity-modal'; +import { useModal } from '@/app/hooks'; const getProgressColor = (activityLevel: number) => { if (isNaN(activityLevel) || activityLevel < 0) return 'bg-gray-300'; @@ -33,12 +36,18 @@ const formatPercentage = (value: number) => { const ITEMS_PER_PAGE = 10; -export function TeamStatsTable({ rapportDailyActivity, isLoading }: { rapportDailyActivity?: ITimerLogGrouped[], isLoading?: boolean }) { +export function TeamStatsTable({ + rapportDailyActivity, + isLoading +}: { + rapportDailyActivity?: ITimerLogGrouped[]; + 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 { openModal, closeModal, isOpen } = useModal(); const paginatedData = rapportDailyActivity?.slice(startIndex, endIndex); const goToPage = (page: number) => { @@ -59,103 +68,139 @@ export function TeamStatsTable({ rapportDailyActivity, isLoading }: { rapportDai } if (!rapportDailyActivity?.length) { - return ( -
- No data available -
- ); + return
No data available
; } 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.activity || 0).toFixed(1)}% -
- - - )) || [] - )) || []} -
- ))} - -
+
+
+
+
+ + + + 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.activity || 0).toFixed(1)}% + +
+ + + <> + + + + + + )) || [] + ) || []} + + ))} + +
+
+
+
-
-
-

- Showing {startIndex + 1} to {Math.min(endIndex, rapportDailyActivity.length)} of {rapportDailyActivity.length} entries -

+
+
+ Showing {startIndex + 1} to {Math.min(endIndex, rapportDailyActivity.length)} of{' '} + {rapportDailyActivity.length} entries
- - -
+
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
- -
diff --git a/apps/web/app/services/client/api/timer/timer-log.ts b/apps/web/app/services/client/api/timer/timer-log.ts index 086304761..0b624a7d4 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -16,6 +16,27 @@ export async function getTimerLogs( // todayStart, todayEnd; +interface ITaskTimesheetParams { + organizationId: string; + tenantId: string; + startDate: string | Date; + endDate: string | Date; + timeZone?: string; + projectIds?: string[]; + employeeIds?: string[]; + taskIds?: string[]; + status?: string[]; +} + +const TIMESHEET_RELATIONS = [ + 'project', + 'task', + 'organizationContact', + 'employee.user', + 'task.taskStatus', + 'timesheet' +] as const; + export async function getTaskTimesheetLogsApi({ organizationId, tenantId, @@ -26,26 +47,18 @@ export async function getTaskTimesheetLogsApi({ employeeIds = [], taskIds = [], status = [] -}: { - organizationId: string, - tenantId: string, - startDate: string | Date, - endDate: string | Date, - timeZone?: string, - projectIds?: string[], - employeeIds?: string[], - taskIds?: string[], - status?: string[] -}) { - +}: ITaskTimesheetParams) { if (!organizationId || !tenantId || !startDate || !endDate) { throw new Error('Required parameters missing: organizationId, tenantId, startDate, and endDate are required'); } + const start = typeof startDate === 'string' ? new Date(startDate).toISOString() : startDate.toISOString(); const end = typeof endDate === 'string' ? new Date(endDate).toISOString() : endDate.toISOString(); + if (isNaN(new Date(start).getTime()) || isNaN(new Date(end).getTime())) { throw new Error('Invalid date format provided'); } + const params = new URLSearchParams({ 'activityLevel[start]': '0', 'activityLevel[end]': '100', @@ -53,34 +66,25 @@ export async function getTaskTimesheetLogsApi({ tenantId, startDate: start, endDate: end, - timeZone: timeZone || '', - 'relations[0]': 'project', - 'relations[1]': 'task', - 'relations[2]': 'organizationContact', - 'relations[3]': 'employee.user', - 'relations[4]': 'task.taskStatus', - 'relations[5]': 'timesheet' - + timeZone: timeZone || getDefaultTimezone() }); - projectIds.forEach((id, index) => { - params.append(`projectIds[${index}]`, id); + TIMESHEET_RELATIONS.forEach((relation, index) => { + params.append(`relations[${index}]`, relation); }); - employeeIds.forEach((id, index) => { - params.append(`employeeIds[${index}]`, id); - }); - - taskIds.forEach((id, index) => { - params.append(`taskIds[${index}]`, id) - }); + const addArrayParam = (key: string, values: string[]) => { + values.forEach((value, index) => { + if (value) params.append(`${key}[${index}]`, value); + }); + }; - status.forEach((name, index) => { - params.append(`status[${index}]`, name); - }) + addArrayParam('projectIds', projectIds); + addArrayParam('employeeIds', employeeIds); + addArrayParam('taskIds', taskIds); + addArrayParam('status', status); - const endpoint = `/timesheet/time-log?${params.toString()}`; - return get(endpoint, { tenantId }); + return get(`/timesheet/time-log?${params.toString()}`, { tenantId }); }