Skip to content

Commit

Permalink
[Feat]: Report Activity Data productivity (#3613)
Browse files Browse the repository at this point in the history
* feat: report activity data productivity

* fix: Condition  is always false

* fix:Enhance error handling in fetchReport function

* fix: improve null checks and error handling in useReportActivity hook

- Simplify mergedProps and setData validation
- Remove redundant setData([]) initialization
- Add consistent mergedProps check in fetchStatisticsCounts
- Optimize dependency array in useMemo
- Improve type safety with null coalescing for tenantId

* fix: improve null checks and error handling in useReportActivity hook

- Simplify mergedProps and setData validation
- Remove redundant setData([]) initialization
- Add consistent mergedProps check in fetchStatisticsCounts
- Optimize dependency array in useMemo
- Improve type safety with null coalescing for tenantId
  • Loading branch information
Innocent-Akim authored Feb 19, 2025
1 parent ecb6aec commit 72e8642
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 176 deletions.
60 changes: 31 additions & 29 deletions apps/web/app/[locale]/dashboard/app-url/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { useOrganizationTeams } from '@app/hooks/features/useOrganizationTeams';
import { useAtomValue } from 'jotai';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { ArrowLeftIcon } from '@radix-ui/react-icons';
import { useRouter } from 'next/navigation';
import { Breadcrumb, Card, Container } from '@/lib/components';
import { Breadcrumb, Container } from '@/lib/components';
import { DashboardHeader } from '../../team-dashboard/[teamId]/components/dashboard-header';
import { useReportActivity } from '@/app/hooks/features/useReportActivity';
import { ProductivityStats } from '../components/ProductivityStats';
import { ProductivityChart } from '../components/ProductivityChart';
import { ProductivityHeader } from '../components/ProductivityHeader';
import { ProductivityTable } from '../components/ProductivityTable';
import { Card } from '@components/ui/card';

interface ProductivityData {
date: string;
Expand All @@ -26,34 +27,36 @@ interface ProductivityData {
}

function AppUrls() {
// const { rapportDailyActivity } = useReportActivity();
const { isTrackingEnabled } = useOrganizationTeams();
const { updateDateRange, updateFilters, isManage } = useReportActivity();

const [groupBy, setGroupBy] = useState<string>('date');

const router = useRouter();
const t = useTranslations();
const router = useRouter();
const fullWidth = useAtomValue(fullWidthState);
const paramsUrl = useParams<{ locale: string }>();
const currentLocale = paramsUrl?.locale;
const { isTrackingEnabled } = useOrganizationTeams();

const {
activityReport,
loadingActivityReport,
handleGroupByChange,
updateDateRange,
updateFilters,
isManage
} = useReportActivity({ types: 'APPS-URLS' });

const generateMonthData = (date: Date): ProductivityData[] => {
const year = date.getFullYear();
const month = date.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();

const generateMonthData = (date: Date): ProductivityData[] => {
const year = date.getFullYear();
const month = date.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();

return Array.from({ length: daysInMonth }, (_, i) => ({
date: new Date(year, month, i + 1).toISOString().split('T')[0],
productive: Math.floor(Math.random() * 50) + 25,
neutral: Math.floor(Math.random() * 40) + 20,
unproductive: Math.floor(Math.random() * 35) + 15,
}));
};
const monthData = generateMonthData(new Date());
return Array.from({ length: daysInMonth }, (_, i) => ({
date: new Date(year, month, i + 1).toISOString().split('T')[0],
productive: Math.floor(Math.random() * 50) + 25,
neutral: Math.floor(Math.random() * 40) + 20,
unproductive: Math.floor(Math.random() * 35) + 15
}));
};

const monthData = generateMonthData(new Date());
const monthTotals = monthData.reduce(
(acc, day) => ({
productive: acc.productive + day.productive,
Expand All @@ -76,6 +79,8 @@ function AppUrls() {
[currentLocale, t]
);

const handleBack = () => router.back();

return (
<MainLayout
className="items-start pb-1 !overflow-hidden w-full"
Expand All @@ -86,7 +91,7 @@ function AppUrls() {
<Container fullWidth={fullWidth} className={cn('flex flex-col gap-4 items-center w-full')}>
<div className="flex items-center pt-6 w-full">
<button
onClick={() => router.back()}
onClick={handleBack}
className="p-1 rounded-full transition-colors hover:bg-gray-100"
>
<ArrowLeftIcon className="text-dark dark:text-[#6b7280] h-6 w-6" />
Expand All @@ -100,12 +105,9 @@ function AppUrls() {
title="Apps & URLs Dashboard"
isManage={isManage}
showGroupBy={true}
onGroupByChange={()=>setGroupBy(groupBy)}
onGroupByChange={handleGroupByChange}
/>
<Card
shadow="bigger"
className="bg-white rounded-xl border border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light h-[403px] p-8 py-0 px-0"
>
<Card className="bg-white rounded-xl border border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light h-[403px] p-8 py-0 px-0">
<div className="flex flex-col gap-6 w-full">
<div className="flex justify-between items-center h-[105px] w-full border-b border-b-gray-200 dark:border-b-gray-700">
<ProductivityHeader month="October" year={2024} />
Expand All @@ -126,7 +128,7 @@ function AppUrls() {
}
>
<Container fullWidth={fullWidth} className={cn('flex flex-col gap-8 !px-4 py-6 w-full')}>
<ProductivityTable />
<ProductivityTable data={activityReport} isLoading={loadingActivityReport} />
</Container>
</MainLayout>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import { GroupByType } from '@/app/hooks/features/useReportActivity';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';

interface GroupBySelectProps {
onGroupByChange?: (value: string) => void;
onGroupByChange?: (value: GroupByType) => void;
}

export function GroupBySelect({ onGroupByChange }: GroupBySelectProps) {
Expand Down
218 changes: 116 additions & 102 deletions apps/web/app/[locale]/dashboard/app-url/components/ProductivityTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,23 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { Card } from '@/lib/components';

interface AppUsageData {
member: {
name: string;
avatarUrl?: string;
};
project?: string;
application?: string;
timeSpent?: string;
percentUsed: number;
}
import { Card } from '@components/ui/card';
import { format } from 'date-fns';
import { IActivityReport, IActivityReportGroupByDate, IActivityItem } from '@app/interfaces/activity/IActivityReport';
import React from 'react';

export function ProductivityTable({
data,
isLoading
}: {
data?: AppUsageData[];
data?: IActivityReport[];
isLoading?: boolean;
}) {
const sampleData: AppUsageData[] = [
{
member: {
name: 'Elanor Pena',
avatarUrl: '/avatars/elanor.jpg'
},
project: 'EverTeams',
application: 'Figma',
timeSpent: '03:46:11',
percentUsed: 60
},
{
member: {
name: 'Elanor Pena',
avatarUrl: '/avatars/elanor.jpg'
},
project: 'EverTeams',
application: 'Slack',
timeSpent: '1:17:02',
percentUsed: 20
},
{
member: {
name: 'Elanor Pena',
avatarUrl: '/avatars/elanor.jpg'
},
project: 'EverTeams',
application: 'Arc',
timeSpent: '46:44',
percentUsed: 15
},
{
member: {
name: 'Elanor Pena',
avatarUrl: '/avatars/elanor.jpg'
},
project: 'EverTeams',
application: 'Postman',
timeSpent: '12:54',
percentUsed: 5
}
];

// Use sample data for now
const displayData = data || sampleData;
const reportData = data as IActivityReportGroupByDate[] | undefined;

if (isLoading) {
return (
<Card className="bg-white rounded-md border border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light min-h-[600px]">
<Table>
<TableHeader>
<TableRow>
Expand All @@ -82,27 +31,39 @@ export function ProductivityTable({
</TableRow>
</TableHeader>
<TableBody>
{[...Array(4)].map((_, i) => (
{[...Array(7)].map((_, i) => (
<TableRow key={i}>
<TableCell>
<div className="flex gap-2 items-center">
<Skeleton className="w-8 h-8 rounded-full" />
<Skeleton className="w-24 h-4" />
</div>
</TableCell>
<TableCell><Skeleton className="w-24 h-4" /></TableCell>
<TableCell><Skeleton className="w-16 h-4" /></TableCell>
<TableCell><Skeleton className="w-16 h-4" /></TableCell>
<TableCell><Skeleton className="w-24 h-4" /></TableCell>
<TableCell><Skeleton className="w-24 h-4"/></TableCell>
<TableCell><Skeleton className="w-16 h-4"/></TableCell>
<TableCell><Skeleton className="w-16 h-4"/></TableCell>
<TableCell><Skeleton className="w-24 h-4"/></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
}

if (!reportData || reportData.length === 0) {
return (
<Card className="bg-white rounded-md border border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light min-h-[600px] flex items-center justify-center">
<div className="text-center text-gray-500 dark:text-gray-400">
<p className="text-lg font-medium">No activity data available</p>
<p className="text-sm">Select a different date range or check back later</p>
</div>
</Card>
);
}

return (
<Card shadow="custom" className="rounded-md border bg-white border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light min-h-[600px]">
<Card className="bg-white rounded-md border border-gray-100 dark:border-gray-700 dark:bg-dark--theme-light min-h-[600px]">
<Table>
<TableHeader>
<TableRow>
Expand All @@ -114,46 +75,99 @@ export function ProductivityTable({
</TableRow>
</TableHeader>
<TableBody>
{displayData?.map((item, index) => (
<TableRow key={index}>
<TableCell>
<div className="flex gap-2 items-center">
<Avatar className="w-8 h-8">
{item.member.avatarUrl && (
<AvatarImage src={item.member.avatarUrl} alt={item.member.name} />
)}
<AvatarFallback>
{item.member.name?.trim()
? item.member.name
.trim()
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
: '?'}
</AvatarFallback>
</Avatar>
<span>{item.member.name}</span>
</div>
</TableCell>
<TableCell>{item.project}</TableCell>
<TableCell>{item.application}</TableCell>
<TableCell>{item.timeSpent}</TableCell>
<TableCell>
<div className="flex gap-2 items-center">
<div className="overflow-hidden w-24 h-2 bg-gray-200 rounded-full">
<div
className="h-full bg-blue-500"
style={{ width: `${item.percentUsed}%` }}
/>
</div>
<span>{item.percentUsed}%</span>
</div>
</TableCell>
</TableRow>
))}
{reportData.map((dayData) => {
const employeeActivities = new Map<string, { employee: any; activities: IActivityItem[] }>();
dayData.employees.forEach(employeeData => {
employeeData.projects[0]?.activity.forEach((activity: IActivityItem) => {
const employeeId = activity.employee.id;
if (!employeeActivities.has(employeeId)) {
employeeActivities.set(employeeId, {
employee: activity.employee,
activities: []
});
}
employeeActivities.get(employeeId)?.activities.push(activity);
});
});

const hasActivities = Array.from(employeeActivities.values()).some(({ activities }) => activities.length > 0);

if (!hasActivities) {
return (
<React.Fragment key={dayData.date}>
<TableRow>
<TableCell colSpan={5} className="px-6 py-4 font-medium bg-gray-50 dark:bg-gray-800">
{format(new Date(dayData.date), 'EEEE dd MMM yyyy')}
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={5} className="py-4 text-center text-gray-500 dark:text-gray-400">
No activities recorded for this day
</TableCell>
</TableRow>
</React.Fragment>
);
}

return (
<React.Fragment key={dayData.date}>
<TableRow>
<TableCell colSpan={5} className="px-6 py-4 font-medium bg-gray-50 dark:bg-gray-800">
{format(new Date(dayData.date), 'EEEE dd MMM yyyy')}
</TableCell>
</TableRow>
{Array.from(employeeActivities.values()).map(({ employee, activities }) => (
activities.map((activity, index) => (
<TableRow key={`${employee.id}-${index}`}>
{index === 0 && (
<TableCell className="align-top" rowSpan={activities.length}>
<div className="flex gap-2 items-center">
<Avatar className="w-8 h-8">
{employee.user.imageUrl && (
<AvatarImage
src={employee.user.imageUrl}
alt={employee.fullName}
/>
)}
<AvatarFallback>
{employee.fullName.split(' ').map((n: string) => n[0]).join('').toUpperCase()}
</AvatarFallback>
</Avatar>
<span>{employee.fullName}</span>
</div>
</TableCell>
)}
<TableCell>Ever Teams</TableCell>
<TableCell>{activity.title}</TableCell>
<TableCell>{formatDuration(activity.duration.toString())}</TableCell>
<TableCell>
<div className="flex gap-2 items-center">
<div className="overflow-hidden w-24 h-2 bg-gray-200 rounded-full">
<div
className="h-full bg-blue-500"
style={{ width: `${activity.duration_percentage}%` }}
/>
</div>
<span>{Math.round(parseFloat(activity.duration_percentage))}%</span>
</div>
</TableCell>
</TableRow>
))
))}
</React.Fragment>
);
})}
</TableBody>
</Table>
</Card>
);
}

function formatDuration(seconds: string): string {
const totalSeconds = parseInt(seconds);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const remainingSeconds = totalSeconds % 60;

return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
}
Loading

0 comments on commit 72e8642

Please sign in to comment.