-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feat]: Apps Url Activity Grouping (#3620)
* 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
1 parent
ee8d72d
commit 5d32521
Showing
8 changed files
with
521 additions
and
266 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
166 changes: 166 additions & 0 deletions
166
...e]/dashboard/app-url/components/productivity-application/ProductivityApplicationTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')}`; | ||
} |
35 changes: 35 additions & 0 deletions
35
apps/web/app/[locale]/dashboard/app-url/components/productivity-application/activity-bar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
Oops, something went wrong.