Skip to content

Commit

Permalink
[Feat]: Apps Url Activity Grouping (#3620)
Browse files Browse the repository at this point in the history
* feat: enhance productivity dashboard with improved grouping and UI

- Add application grouping type to activity reports
- Improve data structure with proper TypeScript interfaces
- Enhance error handling and data validation in ProductivityEmployeeTable
- Add local storage persistence for group-by selection
- Update UI components for better code organization and readability
- Fix date formatting and duration calculations
- Improve dark mode compatibility
- Add proper type definitions for employee and activity data
- Format code according to project style guidelines

* fix: improve data loading in productivity dashboard

- Add automatic data fetching on component mount
- Enhance data validation in ProductivityEmployeeTable
- Improve error handling and logging
- Add fallbacks for missing data fields
- Optimize useEffect dependencies in useReportActivity hook

* fix: improve type safety in ProductivityEmployeeTable data processing

- Add explicit unknown | any type annotations for dateGroup and project parameters
- Enhance type safety in array mapping and filtering operations
- Add type guards for project and activity objects
- Maintain data processing functionality while improving type checking

* fix: cspell

* feat(productivity): enhance application table grouping and display

- Implement application-based grouping in productivity table
- Add proper project name display with fallback options
- Fix TypeScript type issues in activity data handling
- Clean up unused imports and improve code organization

* cspll

* fix: conflits
  • Loading branch information
Innocent-Akim authored Feb 26, 2025
1 parent ee8d72d commit 5d32521
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 266 deletions.
15 changes: 9 additions & 6 deletions apps/web/app/[locale]/dashboard/app-url/[teamId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { Card } from '@components/ui/card';
import { ProductivityHeader } from '../components/ProductivityHeader';
import { ProductivityChart } from '../components/ProductivityChart';
import { ProductivityStats } from '../components/ProductivityStats';
import { ProductivityProjectTable } from '../components/productivity-project';
import { ProductivityTable } from '../components/ProductivityTable';
import { ProductivityEmployeeTable } from '../components/productivity-employee/ProductivityEmployeeTable';
import { useLocalStorageState } from '@/app/hooks';
import { ProductivityApplicationTable, ProductivityEmployeeTable, ProductivityProjectTable } from '../components';

interface ProductivityData {
date: string;
Expand All @@ -35,7 +35,7 @@ function AppUrls() {
const paramsUrl = useParams<{ locale: string }>();
const currentLocale = paramsUrl?.locale;
const { isTrackingEnabled } = useOrganizationTeams();
const [groupByType, setGroupByType] = React.useState<GroupByType>('date');
const [groupByType, setGroupByType] = useLocalStorageState<GroupByType>('group-by-type','date');

const {
activityReport,
Expand Down Expand Up @@ -114,10 +114,11 @@ function AppUrls() {
showGroupBy={true}
title="Apps & URLs Dashboard"
isManage={isManage}
groupByType={groupByType}
/>
<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">
<div className="flex justify-between items-center h-[105px] w-full border-b border-b-gray-200 dark:border-b-gray-700 pl-8">
<ProductivityHeader month="October" year={2024} />
<ProductivityStats
productivePercentage={productivePercentage}
Expand All @@ -140,10 +141,12 @@ function AppUrls() {
switch (groupByType) {
case 'project':
return <ProductivityProjectTable data={activityReport} isLoading={loadingActivityReport} />;
case 'employee':
return <ProductivityEmployeeTable data={activityReport} isLoading={loadingActivityReport} />;
case 'date':
return <ProductivityTable data={activityReport} isLoading={loadingActivityReport} />;
case 'employee':
return <ProductivityEmployeeTable data={activityReport} isLoading={loadingActivityReport} />;
case 'application':
return <ProductivityApplicationTable data={activityReport} isLoading={loadingActivityReport} />;
}
})()}
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@

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

export function GroupBySelect({ onGroupByChange }: GroupBySelectProps) {
export function GroupBySelect({ onGroupByChange, groupByType }: GroupBySelectProps) {
return (
<Select defaultValue="date" onValueChange={onGroupByChange}>
<Select defaultValue={groupByType} onValueChange={onGroupByChange}>
<SelectTrigger className="w-[180px] border border-[#E4E4E7] dark:border-[#2D2D2D] dark:bg-dark--theme-light">
<div className="flex gap-2 items-center">
<span className="text-gray-500">Group by</span>
<SelectValue placeholder="Date" className='text-[#2563EB]'/>
<SelectValue placeholder="Date" className="text-blue-600 dark:text-blue-500" />
</div>
</SelectTrigger>
<SelectContent className="dark:bg-dark--theme-light">
<SelectItem value="date">Date</SelectItem>
<SelectItem value="project">Project</SelectItem>
<SelectItem value="employee">Person</SelectItem>
<SelectItem value="application">Application</SelectItem>
<SelectContent className="dark:bg-dark--theme-light min-w-[180px]">
<SelectItem value="date" className="min-w-[160px] data-[state=checked]:text-blue-600">Date</SelectItem>
<SelectItem value="project" className="min-w-[160px] data-[state=checked]:text-blue-600">Project</SelectItem>
<SelectItem value="employee" className="min-w-[160px] data-[state=checked]:text-blue-600">Person</SelectItem>
<SelectItem value="application" className="min-w-[160px] data-[state=checked]:text-blue-600">Application</SelectItem>
</SelectContent>
</Select>
);
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/[locale]/dashboard/app-url/components/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './ProductivityTable'
export * from './productivity-application/ProductivityApplicationTable'
export * from './productivity-employee/ProductivityEmployeeTable'
export * from './productivity-project'
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
'use client';

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 '@components/ui/card';
import { format } from 'date-fns';
import {
IActivityReport,
IActivityReportGroupByDate,
IActivityItem,
IProjectWithActivity,
} from '@app/interfaces/activity/IActivityReport';
import React from 'react';
import { useTranslations } from 'next-intl';

export function ProductivityApplicationTable({ data, isLoading }: { data?: IActivityReport[]; isLoading?: boolean }) {
const reportData = data as IActivityReportGroupByDate[] | undefined;
const t = useTranslations();

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>
<TableHead>{t('common.DATE')}</TableHead>
<TableHead>{t('sidebar.PROJECTS')}</TableHead>
<TableHead>{t('common.MEMBER')}</TableHead>
<TableHead>{t('common.TIME_SPENT')}</TableHead>
<TableHead>{t('common.PERCENT_USED')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...Array(7)].map((_, i) => (
<TableRow key={i}>
<TableCell><Skeleton className="w-32 h-4" /></TableCell>
<TableCell><Skeleton className="w-24 h-4" /></TableCell>
<TableCell><Skeleton className="w-32 h-4" /></TableCell>
<TableCell><Skeleton className="w-24 h-4" /></TableCell>
<TableCell><Skeleton className="w-full 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">{t('common.NO_ACTIVITY_DATA')}</p>
<p className="text-sm">{t('common.SELECT_DIFFERENT_DATE')}</p>
</div>
</Card>
);
}

// Group activities by application
const groupedByApp = reportData.reduce((apps, dayData) => {
dayData.employees.forEach((employeeData) => {
employeeData.projects.forEach((projectData: IProjectWithActivity) => {
projectData.activity.forEach((activity: IActivityItem) => {
if (!apps[activity.title]) {
apps[activity.title] = [];
}
const projectName = projectData.project?.name || activity.project?.name || 'Ever Teams';
apps[activity.title].push({
date: dayData.date,
activity,
employee: activity.employee,
projectName
});
});
});
});
return apps;
}, {} as Record<string, Array<{ date: string; activity: IActivityItem; employee: any; projectName: string }>>);

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>
<TableHead>{t('common.DATE')}</TableHead>
<TableHead>{t('sidebar.PROJECTS')}</TableHead>
<TableHead>{t('common.MEMBER')}</TableHead>
<TableHead>{t('common.TIME_SPENT')}</TableHead>
<TableHead>{t('common.PERCENT_USED')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Object.entries(groupedByApp).map(([appName, activities]) => (
<React.Fragment key={appName}>
{/* Application Header */}
<TableRow>
<TableCell
colSpan={5}
className="px-6 py-4 font-medium bg-gray-50 dark:bg-gray-800"
>
{appName}
</TableCell>
</TableRow>
{/* Application Activities */}
{activities.map(({ date, activity, employee, projectName }, index) => (
<TableRow key={`${appName}-${date}-${index}`}>
<TableCell>{format(new Date(date), 'EEEE dd MMM yyyy')}</TableCell>
<TableCell>
<div className="flex gap-2 items-center">
<Avatar className="w-8 h-8">
<AvatarImage src="/ever-teams-logo.svg" alt="Ever Teams" />
<AvatarFallback>ET</AvatarFallback>
</Avatar>
<span>{projectName}</span>
</div>
</TableCell>
<TableCell>
<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>{formatDuration(activity.duration.toString())}</TableCell>
<TableCell>
<div className="flex gap-2 items-center">
<div className="overflow-hidden w-full 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(duration: string | number): string {
const totalSeconds = typeof duration === 'string' ? parseInt(duration) : duration;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';

interface ActivityBarProps {
percentage: string;
title: string;
}

export const ActivityBar: React.FC<ActivityBarProps> = ({ percentage, title }) => {
const percentageValue = Math.round(parseFloat(percentage));

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex gap-2 items-center">
<div className="overflow-hidden w-full h-2 bg-gray-200 rounded-full dark:bg-gray-700">
<div
className="h-full bg-blue-500 dark:bg-blue-600"
style={{ width: `${percentageValue}%` }}
/>
</div>
<span className="text-sm text-gray-600 dark:text-gray-300">{percentageValue}%</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Activity: {title}</p>
<p>Usage: {percentageValue}%</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

ActivityBar.displayName = 'ActivityBar';
Loading

0 comments on commit 5d32521

Please sign in to comment.