diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-grid.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-grid.tsx index d24a93b64..dd5c6ffd9 100644 --- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-grid.tsx +++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/components/team-stats-grid.tsx @@ -1,70 +1,117 @@ -"use client"; +'use client'; -import { Card } from "@/components/ui/card"; +import { secondsToTime } from '@/app/helpers'; +import { ITimesheetStatisticsData } from '@/app/interfaces'; +import { Card } from '@/components/ui/card'; +import { Loader2 } from 'lucide-react'; +import { useMemo } from 'react'; -const stats = [ - { - title: "Members worked", - value: "17", - type: "number" - }, - { - title: "Tracked", - value: "47:23", - type: "time", - color: "text-blue-500", - progress: 70, - progressColor: "bg-blue-500" - }, - { - title: "Manual", - value: "18:33", - type: "time", - color: "text-red-500", - progress: 30, - progressColor: "bg-red-500" - }, - { - title: "Idle", - value: "05:10", - type: "time", - color: "text-yellow-500", - progress: 10, - progressColor: "bg-yellow-500" - }, - { - title: "Total Hours", - value: "70:66", - type: "time" - } -]; +function formatPercentage(value: number | undefined): number { + if (!value) return 0; + return Math.round(value); +} -export function TeamStatsGrid() { - return ( - <> +function formatTime(hours: number, minutes: number, seconds: number): string { + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; +} -
- {stats.map((stat) => ( - -
- {stat.title} - - {stat.value} - - {stat.progress && ( -
-
-
-
-
+interface StatItem { + title: string; + value: string; + type: 'number' | 'time'; + color?: string; + progress?: number; + progressColor?: string; + showProgress: boolean; +} + +export function TeamStatsGrid({ + statisticsCounts, + loadingTimesheetStatisticsCounts +}: { + statisticsCounts: ITimesheetStatisticsData | null; + loadingTimesheetStatisticsCounts: boolean; +}) { + const { h: hours, m: minutes, s: seconds } = secondsToTime(statisticsCounts?.weekDuration || 0); + const timeValue = formatTime(hours, minutes, seconds); + const progress = formatPercentage(statisticsCounts?.weekActivities); + + const stats: StatItem[] = useMemo( + () => [ + { + title: 'Members worked', + value: statisticsCounts?.employeesCount?.toString() || '0', + type: 'number', + showProgress: false + }, + { + title: 'Tracked', + value: timeValue, + type: 'time', + color: 'text-blue-500', + progress, + progressColor: 'bg-blue-500', + showProgress: true + }, + { + title: 'Manual', + value: timeValue, + type: 'time', + color: 'text-red-500', + progress, + progressColor: 'bg-red-500', + showProgress: true + }, + { + title: 'Idle', + value: timeValue, + type: 'time', + color: 'text-yellow-500', + progress, + progressColor: 'bg-yellow-500', + showProgress: true + }, + { + title: 'Total Hours', + value: timeValue, + type: 'time', + color: 'text-green-500', + showProgress: false + } + ], + [timeValue, progress, statisticsCounts?.employeesCount] + ); + + return ( +
+ {stats.map((stat) => ( + +
+ {stat.title} +
+ {loadingTimesheetStatisticsCounts ? ( + + ) : ( + + {stat.value} + )}
- - ))} -
- + {stat.showProgress && ( +
+
+
+
+
+ )} +
+
+ ))} +
); } 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 a4c84573b..076ac9fc0 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 @@ -72,7 +72,7 @@ export function TeamStatsTable({ } return ( -
+
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 39b616a1d..467474126 100644 --- a/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx +++ b/apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { useParams, useRouter } from 'next/navigation'; -import { useTranslations } from 'next-intl'; import { Card } from '@/components/ui/card'; import { ArrowLeftIcon } from 'lucide-react'; import { TeamStatsChart } from './components/team-stats-chart'; @@ -16,24 +15,22 @@ import { cn } from '@/lib/utils'; import { useAtomValue } from 'jotai'; import { fullWidthState } from '@app/stores/fullWidth'; import { withAuthentication } from '@/lib/app/authenticator'; -import { useReportActivity } from '@app/hooks/features/useReportActivity'; +import { useReportActivity } from '@/app/hooks/features/useReportActivity'; function TeamDashboard() { const { activeTeam, isTrackingEnabled } = useOrganizationTeams(); - const { rapportChartActivity, updateDateRange, updateFilters, loadingTimeLogReportDailyChart, rapportDailyActivity, loadingTimeLogReportDaily } = useReportActivity(); + const { rapportChartActivity, updateDateRange, updateFilters, loadingTimeLogReportDailyChart, rapportDailyActivity, loadingTimeLogReportDaily, statisticsCounts,loadingTimesheetStatisticsCounts} = useReportActivity(); const router = useRouter(); - const t = useTranslations(); const fullWidth = useAtomValue(fullWidthState); const paramsUrl = useParams<{ locale: string }>(); const currentLocale = paramsUrl?.locale; 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, t] + [activeTeam?.name, currentLocale] ); return ( - - + + + - + { + return ['TRACKED', 'MANUAL', 'IDLE'].includes(type as TimeLogType); +}; export async function GET(req: Request) { const res = new NextResponse(); const { searchParams } = new URL(req.url); + + // Get required parameters from the URL const activityLevelStart = searchParams.get('activityLevel[start]'); const activityLevelEnd = searchParams.get('activityLevel[end]'); const organizationId = searchParams.get('organizationId'); @@ -12,7 +19,13 @@ export async function GET(req: Request) { const startDate = searchParams.get('startDate'); const endDate = searchParams.get('endDate'); const timeZone = searchParams.get('timeZone'); - const groupBy = searchParams.get('groupBy'); + + // Get log types from URL and validate them + const logTypes = Array.from({ length: 10 }, (_, i) => searchParams.get(`logType[${i}]`)) + .filter((logType): logType is string => logType !== null) + .filter(isValidLogType); + + // Validate required parameters if (!startDate || !endDate || !organizationId || !tenantId) { return NextResponse.json( { @@ -22,28 +35,39 @@ export async function GET(req: Request) { ); } + if (logTypes.length === 0) { + return NextResponse.json( + { + error: 'At least one valid logType is required (TRACKED, MANUAL, or IDLE)' + }, + { status: 400 } + ); + } + + // Authenticate the request const { $res, user, access_token } = await authenticatedGuard(req, res); if (!user) return $res('Unauthorized'); try { - const { data } = await getTimeLogReportDailyChartRequest({ + // Make the request to get statistics counts + const { data } = await getTimesheetStatisticsCountsRequest({ activityLevel: { start: parseInt(activityLevelStart || '0'), end: parseInt(activityLevelEnd || '100') }, + logType: logTypes, organizationId, tenantId, startDate, endDate, - timeZone: timeZone || 'Etc/UTC', - groupBy: groupBy || 'date' + timeZone: timeZone || 'Etc/UTC' }, access_token); return NextResponse.json(data); } catch (error) { - console.error('Error fetching daily chart data:', error); + console.error('Error fetching timesheet statistics counts:', error); return NextResponse.json( - { error: 'Failed to fetch daily chart data' }, + { error: 'Failed to fetch timesheet statistics counts' }, { status: 500 } ); } diff --git a/apps/web/app/api/timesheet copy/time-log/report/daily/route.ts b/apps/web/app/api/timesheet/time-log/report/daily/route.ts similarity index 100% rename from apps/web/app/api/timesheet copy/time-log/report/daily/route.ts rename to apps/web/app/api/timesheet/time-log/report/daily/route.ts diff --git a/apps/web/app/hooks/features/useReportActivity.ts b/apps/web/app/hooks/features/useReportActivity.ts index 69b239fa7..9da2183b5 100644 --- a/apps/web/app/hooks/features/useReportActivity.ts +++ b/apps/web/app/hooks/features/useReportActivity.ts @@ -1,27 +1,31 @@ import { useCallback, useEffect, useState, useMemo } from 'react'; -import { ITimeLogReportDailyChartProps } from '@/app/interfaces/timer/ITimerLog'; -import { getTimeLogReportDaily, getTimeLogReportDailyChart } 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 } from '@/app/stores'; - -type UseReportActivityProps = Omit; - -const getDefaultDates = () => { - const endDate = new Date(); - const startDate = new Date(); - startDate.setDate(startDate.getDate() - 7); - - return { - startDate: startDate.toISOString().split('T')[0], - endDate: endDate.toISOString().split('T')[0] +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; +} -const defaultProps: UseReportActivityProps = { - activityLevel: { start: 0, end: 100 }, - ...getDefaultDates(), +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 }; @@ -30,8 +34,11 @@ 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 @@ -40,31 +47,47 @@ export function useReportActivity() { return null; } - return (customProps?: Partial): ITimeLogReportDailyChartProps => ({ - ...defaultProps, - ...currentFilters, - ...(customProps || {}), - organizationId: user.employee.organizationId, - tenantId: user.tenantId ?? '' - }); + 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, - setData: (data: T[]) => void, + queryFn: typeof queryTimeLogReportDailyChart | typeof queryTimeLogReportDaily | typeof queryTimesheetStatisticsCounts, + setData: ((data: T[]) => void) | null, customProps?: Partial ) => { if (!user || !getMergedProps) { - setData([]); + if (setData) { + setData([]); + } return; } try { const mergedProps = getMergedProps(customProps); - const { data } = await queryFn(mergedProps); + const response = await queryFn(mergedProps); - setData(data as T[]); + if (setData && Array.isArray(response.data)) { + setData(response.data as T[]); + } if (customProps) { setCurrentFilters(prev => ({ @@ -74,7 +97,9 @@ export function useReportActivity() { } } catch (err) { console.error('Failed to fetch report:', err); - setData([]); + if (setData) { + setData([]); + } } }, [user, getMergedProps]); @@ -86,6 +111,28 @@ export function useReportActivity() { 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], @@ -94,33 +141,39 @@ export function useReportActivity() { Promise.all([ fetchReportActivity(newProps), - fetchDailyReport(newProps) + fetchDailyReport(newProps), + fetchStatisticsCounts(newProps) ]).catch(console.error); - }, [fetchReportActivity, fetchDailyReport]); + }, [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]); const updateFilters = useCallback((newFilters: Partial) => { Promise.all([ fetchReportActivity(newFilters), - fetchDailyReport(newFilters) + fetchDailyReport(newFilters), + fetchStatisticsCounts(newFilters) ]).catch(console.error); - }, [fetchReportActivity, fetchDailyReport]); + }, [fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]); useEffect(() => { if (user) { Promise.all([ fetchReportActivity(), - fetchDailyReport() + fetchDailyReport(), + fetchStatisticsCounts() ]).catch(console.error); } - }, [user, fetchReportActivity, fetchDailyReport]); + }, [user, fetchReportActivity, fetchDailyReport, fetchStatisticsCounts]); return { loadingTimeLogReportDailyChart, loadingTimeLogReportDaily, + loadingTimesheetStatisticsCounts, rapportChartActivity, rapportDailyActivity, + statisticsCounts, updateDateRange, updateFilters, - currentFilters + currentFilters, + setStatisticsCounts, }; } diff --git a/apps/web/app/interfaces/timer/ITimerLog.ts b/apps/web/app/interfaces/timer/ITimerLog.ts index 68143d969..451d71497 100644 --- a/apps/web/app/interfaces/timer/ITimerLog.ts +++ b/apps/web/app/interfaces/timer/ITimerLog.ts @@ -1,271 +1,282 @@ -import { ITeamTask, TimesheetStatus } from "../ITask"; -import { TimeLogType, TimerSource } from "../ITimer"; +import { ITeamTask, TimesheetStatus } from '../ITask'; +import { TimeLogType, TimerSource } from '../ITimer'; interface BaseEntity { - id: string; - isActive: boolean; - isArchived: boolean; - tenantId: string; - organizationId: string; - createdAt: string; - updatedAt: string; - deletedAt: string | null; - archivedAt: string | null; + id: string; + isActive: boolean; + isArchived: boolean; + tenantId: string; + organizationId: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + archivedAt: string | null; } interface ImageEntity { - imageUrl: string | null; - image: string | null; + imageUrl: string | null; + image: string | null; } interface User extends BaseEntity { - firstName: string; - lastName: string; - name: string; - imageUrl: string | null; - image: string | null; + firstName: string; + lastName: string; + name: string; + imageUrl: string | null; + image: string | null; } interface Employee extends BaseEntity { - isOnline: boolean; - isAway: boolean; - user: User; - fullName: string; + isOnline: boolean; + isAway: boolean; + user: User; + fullName: string; } interface TaskStatus extends BaseEntity { - name: string; - value: string; - description: string; - order: number; - icon: string; - color: string; - isSystem: boolean; - isCollapsed: boolean; - isDefault: boolean; - isTodo: boolean; - isInProgress: boolean; - isDone: boolean; - projectId: string | null; - organizationTeamId: string | null; - fullIconUrl: string; + name: string; + value: string; + description: string; + order: number; + icon: string; + color: string; + isSystem: boolean; + isCollapsed: boolean; + isDefault: boolean; + isTodo: boolean; + isInProgress: boolean; + isDone: boolean; + projectId: string | null; + organizationTeamId: string | null; + fullIconUrl: string; } interface Task extends ITeamTask { - taskStatus: TaskStatus | null, - number: number; - description: string; - startDate: string | null; + taskStatus: TaskStatus | null; + number: number; + description: string; + startDate: string | null; } interface Timesheet extends BaseEntity { - duration: number; - keyboard: number; - mouse: number; - overall: number; - startedAt: string; - stoppedAt: string; - approvedAt: string | null; - submittedAt: string | null; - lockedAt: string | null; - editedAt: string | null; - isBilled: boolean; - status: TimesheetStatus; - employeeId: string; - approvedById: string | null; - isEdited: boolean; + duration: number; + keyboard: number; + mouse: number; + overall: number; + startedAt: string; + stoppedAt: string; + approvedAt: string | null; + submittedAt: string | null; + lockedAt: string | null; + editedAt: string | null; + isBilled: boolean; + status: TimesheetStatus; + employeeId: string; + approvedById: string | null; + isEdited: boolean; } interface Project extends BaseEntity, ImageEntity { - name: string; - membersCount: number; + name: string; + membersCount: number; } interface OrganizationContact extends BaseEntity, ImageEntity { - name: string; + name: string; } export interface TimesheetLog extends BaseEntity { - startedAt: string | Date; - stoppedAt: string | Date; - editedAt: string | null; - logType: TimeLogType.MANUAL; - source: TimerSource.BROWSER; - description: string; - reason: string | null; - isBillable: boolean; - isRunning: boolean; - version: string | null; - employeeId: string; - timesheetId: string; - projectId: string; - taskId: string; - organizationContactId: string; - organizationTeamId: string | null; - project: Project; - task: Task; - organizationContact: OrganizationContact; - employee: Employee; - timesheet: Timesheet, - duration: number; - isEdited: boolean; + startedAt: string | Date; + stoppedAt: string | Date; + editedAt: string | null; + logType: TimeLogType.MANUAL; + source: TimerSource.BROWSER; + description: string; + reason: string | null; + isBillable: boolean; + isRunning: boolean; + version: string | null; + employeeId: string; + timesheetId: string; + projectId: string; + taskId: string; + organizationContactId: string; + organizationTeamId: string | null; + project: Project; + task: Task; + organizationContact: OrganizationContact; + employee: Employee; + timesheet: Timesheet; + duration: number; + isEdited: boolean; } export interface UpdateTimesheetStatus extends BaseEntity { - duration: number; - keyboard: number; - mouse: number; - overall: number; - startedAt: string | Date; - stoppedAt: string | Date; - approvedAt: string | null; - submittedAt: string | null; - lockedAt: string | null; - editedAt: string | null; - isBilled: boolean; - status: - | "DRAFT" - | "PENDING" - | "IN REVIEW" - | "DENIED" - | "APPROVED"; - employeeId: string; - approvedById: string | null; - employee: Employee; - isEdited: boolean; + duration: number; + keyboard: number; + mouse: number; + overall: number; + startedAt: string | Date; + stoppedAt: string | Date; + approvedAt: string | null; + submittedAt: string | null; + lockedAt: string | null; + editedAt: string | null; + isBilled: boolean; + status: 'DRAFT' | 'PENDING' | 'IN REVIEW' | 'DENIED' | 'APPROVED'; + employeeId: string; + approvedById: string | null; + employee: Employee; + isEdited: boolean; } -export interface UpdateTimesheet extends Pick< - Partial, - | 'id' - | 'reason' - | 'organizationContactId' - | 'description' - | 'organizationTeamId' - | 'projectId' - | 'taskId' - | 'employeeId' - | 'organizationId' - | 'tenantId' - | 'logType' - | 'source' ->, - Pick< - TimesheetLog, - | 'startedAt' - | 'stoppedAt' - > { - isBillable: boolean; +export interface UpdateTimesheet + extends Pick< + Partial, + | 'id' + | 'reason' + | 'organizationContactId' + | 'description' + | 'organizationTeamId' + | 'projectId' + | 'taskId' + | 'employeeId' + | 'organizationId' + | 'tenantId' + | 'logType' + | 'source' + >, + Pick { + isBillable: boolean; } export interface ITimerValue { - TRACKED: number; - MANUAL: number; - IDLE: number; - RESUMED: number; + TRACKED: number; + MANUAL: number; + IDLE: number; + RESUMED: number; } export interface ITimerDailyLog { - date: string; - value: ITimerValue; + date: string; + value: ITimerValue; } export interface ITimeLogReportDailyChartProps { - activityLevel: { - start: number; - end: number; - }; - start: number; - end: number; - organizationId: string; - tenantId: string; - startDate: string; - endDate: string; - timeZone?: string; - projectIds?: string[], - employeeIds?: string[], - logType?: string[], - teamIds?: string[], - groupBy?: string; + activityLevel: { + start: number; + end: number; + }; + start: number; + end: number; + organizationId: string; + tenantId: string; + startDate: string; + endDate: string; + timeZone?: string; + projectIds?: string[]; + employeeIds?: string[]; + logType?: TimeLogType[]; + teamIds?: string[]; + groupBy?: string; } export interface IOrganizationContact { - id: string; - name: string; - imageUrl: string; - image: string | null; + id: string; + name: string; + imageUrl: string; + image: string | null; } export interface ITimerProject { - id: string; - name: string; - imageUrl: string; - membersCount: number; - organizationContact: IOrganizationContact; - image: string | null; + id: string; + name: string; + imageUrl: string; + membersCount: number; + organizationContact: IOrganizationContact; + image: string | null; } export interface ITimerUser { - id: string; - firstName: string; - lastName: string; - imageUrl: string; - image: string | null; - name: string; + id: string; + firstName: string; + lastName: string; + imageUrl: string; + image: string | null; + name: string; } export interface ITimerEmployee { - id: string; - isOnline: boolean; - isAway: boolean; - userId: string; - user: ITimerUser; - fullName: string; + id: string; + isOnline: boolean; + isAway: boolean; + userId: string; + user: ITimerUser; + fullName: string; } export interface ITimerTask { - id: string; - isActive: boolean; - isArchived: boolean; - tenantId: string; - organizationId: string; - number: number; - prefix: string; - title: string; - description: string; - status: string; - priority: string | null; - size: string | null; - issueType: string | null; - estimate: string | null; - dueDate: string; - startDate: string | null; - resolvedAt: string | null; - version: string | null; - taskStatus: string | null; - taskNumber: string; + id: string; + isActive: boolean; + isArchived: boolean; + tenantId: string; + organizationId: string; + number: number; + prefix: string; + title: string; + description: string; + status: string; + priority: string | null; + size: string | null; + issueType: string | null; + estimate: string | null; + dueDate: string; + startDate: string | null; + resolvedAt: string | null; + version: string | null; + taskStatus: string | null; + taskNumber: string; } export interface ITimerTaskLog { - task: ITimerTask; - description: string; - duration: number; - client: IOrganizationContact; + task: ITimerTask; + description: string; + duration: number; + client: IOrganizationContact; } export interface ITimerEmployeeLog { - employee: ITimerEmployee; - sum: number; - tasks: ITimerTaskLog[]; - activity: number; + employee: ITimerEmployee; + sum: number; + tasks: ITimerTaskLog[]; + activity: number; } export interface ITimerProjectLog { - project: ITimerProject; - employeeLogs: ITimerEmployeeLog[]; + project: ITimerProject; + employeeLogs: ITimerEmployeeLog[]; } export interface ITimerLogGrouped { - date: string; - logs: ITimerProjectLog[]; + date: string; + logs: ITimerProjectLog[]; +} + +/** + * Interface for timesheet statistics counts. + */ +export interface ITimesheetStatisticsCounts { + tracked: ITimesheetStatisticsData | null; + idle: ITimesheetStatisticsData | null; + manual: ITimesheetStatisticsData | null; + combined: ITimesheetStatisticsData; +} + +export interface ITimesheetStatisticsData { + employeesCount: number; + projectsCount: number; + todayActivities: number; + todayDuration: number; + weekActivities: number; + weekDuration: number; } 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 0b624a7d4..cb28d39cd 100644 --- a/apps/web/app/services/client/api/timer/timer-log.ts +++ b/apps/web/app/services/client/api/timer/timer-log.ts @@ -1,6 +1,7 @@ -import { TimesheetLog, ITimerStatus, IUpdateTimesheetStatus, UpdateTimesheetStatus, UpdateTimesheet, ITimerDailyLog, ITimeLogReportDailyChartProps, ITimerLogGrouped } from '@app/interfaces'; +import { TimesheetLog, ITimerStatus, IUpdateTimesheetStatus, UpdateTimesheetStatus, UpdateTimesheet, ITimerDailyLog, ITimeLogReportDailyChartProps, ITimerLogGrouped, TimeLogType, ITimesheetStatisticsData } from '@app/interfaces'; import { get, deleteApi, put, post } from '../../axios'; import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers'; +import qs from 'qs'; export async function getTimerLogs( tenantId: string, @@ -224,6 +225,7 @@ interface ITimeLogReportDailyProps { employeeIds?: string[]; taskIds?: string[]; teamIds?: string[]; + logType?: TimeLogType[]; activityLevel?: { start: number; end: number; @@ -274,3 +276,76 @@ export function getTimeLogReportDaily({ return get(`/timesheet/time-log/report/daily?${queryString}`, { tenantId }); } + +/** + * Format duration in seconds to human readable format (HH:mm:ss) + */ +export function formatDuration(seconds: number): string { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + return [ + hours.toString().padStart(2, '0'), + minutes.toString().padStart(2, '0'), + remainingSeconds.toString().padStart(2, '0') + ].join(':'); +} + +/** + * Format activity percentage with 2 decimal places + */ +export function formatActivity(activity: number): string { + return `${activity.toFixed(2)}%`; +} + +/** + * Get timesheet statistics counts + * @param params Request parameters including activity levels, log types, and date range + * @returns Promise with statistics counts data + * @example + * const { data } = await getTimesheetStatisticsCounts({ + * activityLevel: { start: 0, end: 100 }, + * logType: ['TRACKED'], + * organizationId: '...', + * tenantId: '...', + * startDate: '2024-11-30 13:00:00', + * endDate: '2024-12-31 12:59:59', + * timeZone: 'Australia/Lord_Howe' + * }); + * + * console.log({ + * employees: data.employeesCount, + * projects: data.projectsCount, + * weekActivity: formatActivity(data.weekActivities), // "49.93%" + * weekDuration: formatDuration(data.weekDuration), // "106:21:19" + * todayActivity: formatActivity(data.todayActivities), + * todayDuration: formatDuration(data.todayDuration) + * }); + */ +export async function getTimesheetStatisticsCounts({ + activityLevel, + logType, + organizationId, + tenantId, + startDate, + endDate, + timeZone = 'Etc/UTC' +}: ITimeLogReportDailyProps): Promise<{ data: ITimesheetStatisticsData }> { + const queryString = qs.stringify( + { + activityLevel, + logType, + organizationId, + startDate, + endDate, + timeZone + }, + { + arrayFormat: 'indices', + encode: true, + strictNullHandling: true + } + ); + return get(`/timesheet/statistics/counts?${queryString}`, { tenantId }); +} diff --git a/apps/web/app/services/server/requests/timesheet.ts b/apps/web/app/services/server/requests/timesheet.ts index e1a852902..dd2208dc5 100644 --- a/apps/web/app/services/server/requests/timesheet.ts +++ b/apps/web/app/services/server/requests/timesheet.ts @@ -1,7 +1,7 @@ -import { ITasksTimesheet } from '@app/interfaces/ITimer'; +import { ITasksTimesheet, TimeLogType } from '@app/interfaces/ITimer'; import { serverFetch } from '../fetch'; import qs from 'qs'; -import { ITimerDailyLog, ITimerLogGrouped, TimesheetLog, UpdateTimesheet, UpdateTimesheetStatus } from '@/app/interfaces/timer/ITimerLog'; +import { ITimerDailyLog, ITimerLogGrouped, ITimesheetStatisticsCounts, TimesheetLog, UpdateTimesheet, UpdateTimesheetStatus } from '@/app/interfaces/timer/ITimerLog'; import { IUpdateTimesheetStatus } from '@/app/interfaces'; export type TTasksTimesheetStatisticsParams = { @@ -157,6 +157,45 @@ export interface ITimeLogRequestParams { }; } +// export type LogType = 'TRACKED' | 'MANUAL' | 'IDLE'; + +export interface ITimesheetStatisticsCountsProps { + activityLevel: { + start: number; + end: number; + }; + logType: TimeLogType[]; + organizationId: string; + tenantId: string; + startDate: string; + endDate: string; + timeZone?: string; +} + + +/** + * Fetches timesheet statistics counts from the API + * @param params - Parameters for the statistics request + * @param bearer_token - Authentication token + * @returns Promise with the statistics counts data + */ +export async function getTimesheetStatisticsCountsRequest( + { tenantId, ...params }: ITimesheetStatisticsCountsProps, + bearer_token: string +): Promise<{ data: ITimesheetStatisticsCounts }> { + const queries = qs.stringify(params, { + arrayFormat: 'indices', + encode: true, + strictNullHandling: true + }); + + return serverFetch({ + path: `/timesheet/statistics/counts?${queries}`, + method: 'GET', + bearer_token, + tenantId + }); +} function buildTimeLogParams(params: ITimeLogRequestParams): URLSearchParams { const baseParams = new URLSearchParams({ diff --git a/apps/web/app/stores/time-logs.ts b/apps/web/app/stores/time-logs.ts index 891af142d..90e83be24 100644 --- a/apps/web/app/stores/time-logs.ts +++ b/apps/web/app/stores/time-logs.ts @@ -1,6 +1,6 @@ import { ITimerLogsDailyReport } from '@app/interfaces/timer/ITimerLogs'; import { atom } from 'jotai'; -import { IProject, ITeamTask, ITimerDailyLog, ITimerLogGrouped, OT_Member, TimesheetFilterByDays, TimesheetLog, UpdateTimesheetStatus } from '../interfaces'; +import { IProject, ITeamTask, ITimerDailyLog, ITimerLogGrouped, ITimesheetStatisticsData, OT_Member, TimesheetFilterByDays, TimesheetLog, UpdateTimesheetStatus } from '../interfaces'; interface IFilterOption { value: string; @@ -24,3 +24,4 @@ export const timesheetUpdateState = atom(null) export const selectTimesheetIdState = atom([]) export const timeLogsRapportChartState = atom([]); export const timeLogsRapportDailyState = atom([]) +export const timesheetStatisticsCountsState = atom(null) diff --git a/apps/web/components/pages/task/ParentTask.tsx b/apps/web/components/pages/task/ParentTask.tsx index 1851a0d5a..af9ee8538 100644 --- a/apps/web/components/pages/task/ParentTask.tsx +++ b/apps/web/components/pages/task/ParentTask.tsx @@ -38,12 +38,12 @@ function CreateParentTask({ modal, task }: { modal: IHookModal; task: ITeamTask
{loading && ( -
+
)} -
+
{t('common.ADD_PARENT')}