Skip to content

Commit

Permalink
[Fea]: Activity Modal (#3551)
Browse files Browse the repository at this point in the history
* feat: make team stats table responsive

- Add horizontal scroll for small screens
- Set minimum widths for each column
- Add whitespace-nowrap to prevent content wrapping
- Make pagination controls responsive
- Improve dark mode styling
- Fix avatar and text truncation

* fix

* fix: align table body columns with header widths

- Replace min-w with fixed w for consistent column widths
- Add overflow-hidden to prevent content overflow
- Fix table cell width alignment across header and body
- Use consistent width values for each column
- Add proper key props for React lists

* feat: enhance team stats table UI

- Add chart icon column for visual indicators
- Increase member column width for better readability
- Improve text colors and spacing consistency
- Update column alignment and styling
- Fix table header font weights and colors

* feat: add activity modal with detailed stats

- Create ActivityModal component with activity breakdown
- Add tabs for Activity and Tracked vs Manual time
- Implement circular progress charts with SVG
- Show detailed time breakdowns with percentages
- Integrate modal with team stats table chart button

* refactor: simplify activity modal

- Remove tabs and keep only activity breakdown chart
- Use custom Modal component with useModal hook
- Update chart segments with fixed percentages
- Improve modal styling and layout

* refactor(activity-modal): optimize component structure and performance

- Extract reusable Circle and LegendItem components
- Add memoization for time calculations
- Improve TypeScript types and interfaces
- Clean up code formatting and remove unused variables
- Enhance code maintainability and readability

* feat(activity-modal): implement modal for tracking and managing activities

* deep scan

* Add accessibility attributes to tab buttons.

* fix: coderabbitai
  • Loading branch information
Innocent-Akim authored Jan 28, 2025
1 parent 2f8f946 commit 5650129
Show file tree
Hide file tree
Showing 4 changed files with 397 additions and 135 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
'use client';

import { Modal, Avatar } from '@/lib/components';
import { ITimerEmployeeLog } from '@/app/interfaces/timer/ITimerLog';
import { useState, useMemo } from 'react';

interface ActivityModalProps {
employeeLog: ITimerEmployeeLog;
isOpen: boolean;
closeModal: () => void;
}

type TabType = 'tracked' | 'activity';

interface CircleProps {
color: string;
dashArray: string;
dashOffset?: string;
}

const Circle = ({ color, dashArray, dashOffset = '0' }: CircleProps) => (
<circle
cx="50"
cy="50"
r="40"
fill="none"
stroke={color}
strokeWidth="20"
strokeDasharray={`${dashArray} 251.327`}
strokeDashoffset={dashOffset}
className="transition-all duration-1000"
/>
);

const LegendItem = ({
color,
label,
time,
percentage
}: {
color: string;
label: string;
time: string;
percentage: number;
}) => (
<div className="flex justify-between items-center text-sm">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: color }}></div>
<span className="text-gray-700 dark:text-gray-300">{label}</span>
</div>
<span className="font-normal text-gray-900 dark:text-gray-100">
{time} ({percentage}%)
</span>
</div>
);

const formatTime = (minutes: number): string => {
const hours = Math.floor(minutes / 60);
const mins = Math.floor(minutes % 60);
return `${hours}h ${mins.toString().padStart(2, '0')}min`;
};

export const ActivityModal = ({ employeeLog, isOpen, closeModal }: ActivityModalProps) => {
const [activeTab, setActiveTab] = useState<TabType>('activity');

const timeCalculations = useMemo(() => {
const totalTime = employeeLog.sum || 0;
return {
totalTime,
activeTime: totalTime * 0.57,
idleTime: totalTime * 0.13,
unknownTime: totalTime * 0.3,
trackedTime: totalTime * 0.75,
manualTime: totalTime * 0.25
};
}, [employeeLog.sum]);

const { activeTime, idleTime, unknownTime, trackedTime, manualTime } = timeCalculations;

const formattedTimes = useMemo(
() => ({
active: formatTime(activeTime),
idle: formatTime(idleTime),
unknown: formatTime(unknownTime),
tracked: formatTime(trackedTime),
manual: formatTime(manualTime)
}),
[activeTime, idleTime, unknownTime, trackedTime, manualTime]
);

return (
<Modal
isOpen={isOpen}
closeModal={closeModal}
className="bg-light--theme-light dark:bg-dark--theme-light p-5 rounded-xl w-full md:w-40 md:min-w-[32rem] justify-start h-[auto]"
titleClass="font-bold flex justify-start w-full"
>
<div className="flex flex-col w-full gap-4 justify-start md:w-40 md:min-w-[32rem] p-4">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center space-x-3">
<Avatar
size={40}
imageUrl={employeeLog.employee.user?.imageUrl}
imageTitle={employeeLog.employee.fullName}
className="relative"
/>
<h3 className="text-xl font-medium text-gray-900 dark:text-gray-100">
{employeeLog.employee.fullName}
</h3>
</div>
</div>

<div className="flex p-1 space-x-2 bg-gray-100 dark:bg-[#1B1D22] rounded-lg">
{(['tracked', 'activity'] as const).map((tab) => (
<button
key={tab}
role="tab"
aria-selected={activeTab === tab}
aria-label={tab === 'tracked' ? 'Show tracked vs manual time' : 'Show activity breakdown'}
onClick={() => setActiveTab(tab)}
className={`flex-1 px-4 py-2 rounded-md transition-all duration-200 ${
activeTab === tab
? 'bg-white shadow-sm dark:bg-dark--theme-light text-primary dark:text-primary-light font-normal'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
}`}
>
{tab === 'tracked' ? 'Tracked vs Manual' : 'Activity Breakdown'}
</button>
))}
</div>

<div className="relative mt-8">
{activeTab === 'tracked' ? (
<div className="transition-opacity duration-200 ease-in-out">
<div className="relative mx-auto mb-8 w-48 h-48">
<svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
<Circle color="#2563EB" dashArray="188.49556" />
<Circle color="#9333EA" dashArray="62.83185" dashOffset="-188.49556" />
</svg>
</div>

<div className="space-y-4">
<LegendItem
color="#2563EB"
label="Tracked Time"
time={formattedTimes.tracked}
percentage={75}
/>
<LegendItem
color="#9333EA"
label="Manual Time"
time={formattedTimes.manual}
percentage={25}
/>
</div>
</div>
) : (
<div className="transition-opacity duration-200 ease-in-out">
<div className="relative mx-auto mb-8 w-48 h-48">
<svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
<Circle color="#2563EB" dashArray="143.25639" />
<Circle color="#F59E0B" dashArray="32.67251" dashOffset="-143.25639" />
<Circle color="#EF4444" dashArray="75.3981" dashOffset="-175.9289" />
</svg>
</div>

<div className="space-y-4">
<LegendItem
color="#2563EB"
label="Active Time"
time={formattedTimes.active}
percentage={57}
/>
<LegendItem
color="#F59E0B"
label="Idle Time"
time={formattedTimes.idle}
percentage={13}
/>
<LegendItem
color="#EF4444"
label="Unknown Activity"
time={formattedTimes.unknown}
percentage={30}
/>
</div>
</div>
)}
</div>
</div>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const ChartIcon = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.67967 3.98016C1.83301 5.10016 1.33301 6.4935 1.33301 8.00016C1.33301 11.6802 4.31967 14.6668 7.99967 14.6668C11.6797 14.6668 14.6663 11.6802 14.6663 8.00016C14.6663 4.32016 11.6797 1.3335 7.99967 1.3335"
stroke="#C4C4C4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.33301 8.00016C3.33301 10.5802 5.41967 12.6668 7.99967 12.6668C10.5797 12.6668 12.6663 10.5802 12.6663 8.00016C12.6663 5.42016 10.5797 3.3335 7.99967 3.3335"
stroke="#C4C4C4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 10.6668C9.47333 10.6668 10.6667 9.4735 10.6667 8.00016C10.6667 6.52683 9.47333 5.3335 8 5.3335"
stroke="#C4C4C4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
Loading

0 comments on commit 5650129

Please sign in to comment.