Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat]: Timesheet Statistics api types #3557

Merged
merged 6 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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')}`;
}

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
{stats.map((stat) => (
<Card key={stat.title} className="p-6 dark:bg-dark--theme-light">
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-500">{stat.title}</span>
<span className={`text-2xl font-semibold mt-2 ${stat.color || "text-gray-900 dark:text-white"}`}>
{stat.value}
</span>
{stat.progress && (
<div className="mt-4">
<div className="w-full h-2 bg-gray-100 rounded-full">
<div
className={`h-full rounded-full ${stat.progressColor}`}
style={{ width: `${stat.progress}%` }}
/>
</div>
</div>
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 (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
{stats.map((stat) => (
<Card key={stat.title} className="p-6 dark:bg-dark--theme-light">
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-500">{stat.title}</span>
<div className="mt-2 h-9">
{loadingTimesheetStatisticsCounts ? (
<Loader2 className="w-6 h-6 text-gray-500 animate-spin" />
) : (
<span className={`text-2xl font-semibold ${stat.color || 'text-gray-900 dark:text-white'}`}>
{stat.value}
</span>
)}
</div>
</Card>
))}
</div>
</>
{stat.showProgress && (
<div className="mt-4">
<div className="w-full h-2 bg-gray-100 rounded-full dark:bg-gray-700">
<div
className={`h-full rounded-full ${stat.progressColor} transition-all duration-300`}
style={{
width: `${loadingTimesheetStatisticsCounts ? 0 : stat.progress}%`
}}
/>
</div>
</div>
)}
</div>
</Card>
))}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function TeamStatsTable({
}

return (
<div className="space-y-4 min-h-[400px] w-full dark:bg-dark--theme-light">
<div className="min-h-[400px] w-full dark:bg-dark--theme-light">
<div className="relative rounded-md border">
<div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle">
Expand Down
19 changes: 10 additions & 9 deletions apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 (
<MainLayout
Expand All @@ -57,8 +54,12 @@ function TeamDashboard() {
onUpdateDateRange={updateDateRange}
onUpdateFilters={updateFilters}
/>
<TeamStatsGrid />
<Card className="w-full">
<TeamStatsGrid
statisticsCounts={statisticsCounts}
loadingTimesheetStatisticsCounts={loadingTimesheetStatisticsCounts}
/>

<Card className="w-full dark:bg-dark--theme-light">
<TeamStatsChart
rapportChartActivity={rapportChartActivity}
isLoading={loadingTimeLogReportDailyChart}
Expand All @@ -70,7 +71,7 @@ function TeamDashboard() {
}
>
<Container fullWidth={fullWidth} className={cn('flex flex-col gap-8 py-6 w-full')}>
<Card className="w-full">
<Card className="w-full dark:bg-dark--theme-light min-h-[400px]">
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
<TeamStatsTable
rapportDailyActivity={rapportDailyActivity}
isLoading={loadingTimeLogReportDaily}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
import { NextResponse } from 'next/server';
import { getTimeLogReportDailyChartRequest } from '@app/services/server/requests';
import { getTimesheetStatisticsCountsRequest } from '@app/services/server/requests';
import { TimeLogType } from '@/app/interfaces';

const isValidLogType = (type: string): type is TimeLogType => {
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');
const tenantId = searchParams.get('tenantId');
const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');
const timeZone = searchParams.get('timeZone');
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved
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);
Innocent-Akim marked this conversation as resolved.
Show resolved Hide resolved

// Validate required parameters
if (!startDate || !endDate || !organizationId || !tenantId) {
return NextResponse.json(
{
Expand All @@ -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 }
);
}
Expand Down
Loading