feat(reporting): enhance overview reports with memoization and dark mode support
- Refactored components in the reporting section to utilize React.memo, useCallback, and useMemo for improved performance and reduced unnecessary re-renders. - Updated the OverviewStatCard to support dark mode styling and added enhanced hover effects. - Improved the Avatars component by memoizing rendering logic and preventing event propagation. - Enhanced OverviewReportsTable with memoized columns and row props for better performance. - Applied consistent styling adjustments across various components to ensure a cohesive user experience.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Avatar, Tooltip } from 'antd';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||
|
||||
interface AvatarsProps {
|
||||
@@ -6,41 +7,54 @@ interface AvatarsProps {
|
||||
maxCount?: number;
|
||||
}
|
||||
|
||||
const renderAvatar = (member: InlineMember, index: number) => (
|
||||
<Tooltip
|
||||
key={member.team_member_id || index}
|
||||
title={member.end && member.names ? member.names.join(', ') : member.name}
|
||||
>
|
||||
{member.avatar_url ? (
|
||||
<span onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<Avatar src={member.avatar_url} size={28} key={member.team_member_id || index} />
|
||||
</span>
|
||||
) : (
|
||||
<span onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<Avatar
|
||||
size={28}
|
||||
key={member.team_member_id || index}
|
||||
style={{
|
||||
backgroundColor: member.color_code || '#ececec',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
const Avatars: React.FC<AvatarsProps> = React.memo(({ members, maxCount }) => {
|
||||
const stopPropagation = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const renderAvatar = useCallback((member: InlineMember, index: number) => (
|
||||
<Tooltip
|
||||
key={member.team_member_id || index}
|
||||
title={member.end && member.names ? member.names.join(', ') : member.name}
|
||||
>
|
||||
{member.avatar_url ? (
|
||||
<span onClick={stopPropagation}>
|
||||
<Avatar src={member.avatar_url} size={28} key={member.team_member_id || index} />
|
||||
</span>
|
||||
) : (
|
||||
<span onClick={stopPropagation}>
|
||||
<Avatar
|
||||
size={28}
|
||||
key={member.team_member_id || index}
|
||||
style={{
|
||||
backgroundColor: member.color_code || '#ececec',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
), [stopPropagation]);
|
||||
|
||||
const visibleMembers = useMemo(() => {
|
||||
return maxCount ? members.slice(0, maxCount) : members;
|
||||
}, [members, maxCount]);
|
||||
|
||||
const avatarElements = useMemo(() => {
|
||||
return visibleMembers.map((member, index) => renderAvatar(member, index));
|
||||
}, [visibleMembers, renderAvatar]);
|
||||
|
||||
const Avatars: React.FC<AvatarsProps> = ({ members, maxCount }) => {
|
||||
const visibleMembers = maxCount ? members.slice(0, maxCount) : members;
|
||||
return (
|
||||
<div onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<div onClick={stopPropagation}>
|
||||
<Avatar.Group>
|
||||
{visibleMembers.map((member, index) => renderAvatar(member, index))}
|
||||
{avatarElements}
|
||||
</Avatar.Group>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Avatars.displayName = 'Avatars';
|
||||
|
||||
export default Avatars;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { Button, Card, Checkbox, Flex, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||
@@ -25,29 +25,37 @@ const OverviewReports = () => {
|
||||
trackMixpanelEvent(evt_reporting_overview);
|
||||
}, [trackMixpanelEvent]);
|
||||
|
||||
const handleArchiveToggle = () => {
|
||||
const handleArchiveToggle = useCallback(() => {
|
||||
dispatch(toggleIncludeArchived());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// Memoize the header children to prevent unnecessary re-renders
|
||||
const headerChildren = useMemo(() => (
|
||||
<Button type="text" onClick={handleArchiveToggle}>
|
||||
<Checkbox checked={includeArchivedProjects} />
|
||||
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
|
||||
</Button>
|
||||
), [handleArchiveToggle, includeArchivedProjects, t]);
|
||||
|
||||
// Memoize the teams text to prevent unnecessary re-renders
|
||||
const teamsText = useMemo(() => (
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>
|
||||
{t('teamsText')}
|
||||
</Typography.Text>
|
||||
), [t]);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={24}>
|
||||
<CustomPageHeader
|
||||
title={t('overviewTitle')}
|
||||
children={
|
||||
<Button type="text" onClick={handleArchiveToggle}>
|
||||
<Checkbox checked={includeArchivedProjects} />
|
||||
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
|
||||
</Button>
|
||||
}
|
||||
children={headerChildren}
|
||||
/>
|
||||
|
||||
<OverviewStats />
|
||||
|
||||
<Card>
|
||||
<Flex vertical gap={12}>
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>
|
||||
{t('teamsText')}
|
||||
</Typography.Text>
|
||||
{teamsText}
|
||||
<OverviewReportsTable />
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
@@ -1,32 +1,151 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Card, Flex, Typography } from 'antd';
|
||||
import { Card, Flex, Typography, theme } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
type InsightCardProps = {
|
||||
icon: ReactNode;
|
||||
interface InsightCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
children: React.ReactNode;
|
||||
loading?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const OverviewStatCard = React.memo(({ icon, title, children, loading = false }: InsightCardProps) => {
|
||||
const { token } = theme.useToken();
|
||||
// Better dark mode detection using multiple token properties
|
||||
const isDarkMode = token.colorBgContainer === '#1f1f1f' ||
|
||||
token.colorBgBase === '#141414' ||
|
||||
token.colorBgElevated === '#1f1f1f' ||
|
||||
document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
document.body.classList.contains('dark');
|
||||
|
||||
// Memoize enhanced card styles with dark mode support
|
||||
const cardStyles = useMemo(() => ({
|
||||
body: {
|
||||
padding: '24px',
|
||||
background: isDarkMode
|
||||
? '#1f1f1f !important'
|
||||
: '#ffffff !important',
|
||||
}
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Memoize card container styles with dark mode support
|
||||
const cardContainerStyle = useMemo(() => ({
|
||||
width: '100%',
|
||||
borderRadius: '0px',
|
||||
border: isDarkMode
|
||||
? '1px solid #303030'
|
||||
: '1px solid #f0f0f0',
|
||||
boxShadow: isDarkMode
|
||||
? '0 2px 8px rgba(0, 0, 0, 0.3)'
|
||||
: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
overflow: 'hidden',
|
||||
position: 'relative' as const,
|
||||
cursor: 'default',
|
||||
backgroundColor: isDarkMode ? '#1f1f1f !important' : '#ffffff !important',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Memoize icon container styles with dark mode support
|
||||
const iconContainerStyle = useMemo(() => ({
|
||||
padding: '12px',
|
||||
borderRadius: '0px',
|
||||
background: isDarkMode
|
||||
? '#2a2a2a'
|
||||
: '#f8f9ff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: '64px',
|
||||
minHeight: '64px',
|
||||
boxShadow: isDarkMode
|
||||
? '0 2px 4px rgba(24, 144, 255, 0.2)'
|
||||
: '0 2px 4px rgba(24, 144, 255, 0.1)',
|
||||
border: isDarkMode
|
||||
? '1px solid #404040'
|
||||
: '1px solid rgba(24, 144, 255, 0.1)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Memoize title styles with dark mode support
|
||||
const titleStyle = useMemo(() => ({
|
||||
fontSize: '18px',
|
||||
fontWeight: 600,
|
||||
color: isDarkMode ? '#ffffff !important' : '#262626 !important',
|
||||
marginBottom: '8px',
|
||||
lineHeight: '1.4',
|
||||
}), [isDarkMode]);
|
||||
|
||||
// Memoize decorative element styles with dark mode support
|
||||
const decorativeStyle = useMemo(() => ({
|
||||
position: 'absolute' as const,
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
background: isDarkMode
|
||||
? 'linear-gradient(135deg, rgba(24, 144, 255, 0.15) 0%, rgba(24, 144, 255, 0.08) 100%)'
|
||||
: 'linear-gradient(135deg, rgba(24, 144, 255, 0.05) 0%, rgba(24, 144, 255, 0.02) 100%)',
|
||||
opacity: isDarkMode ? 0.8 : 0.6,
|
||||
clipPath: 'polygon(100% 0%, 0% 100%, 100% 100%)',
|
||||
}), [isDarkMode]);
|
||||
|
||||
const OverviewStatCard = ({ icon, title, children, loading = false }: InsightCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
className="custom-insights-card"
|
||||
style={{ width: '100%' }}
|
||||
styles={{ body: { paddingInline: 16 } }}
|
||||
loading={loading}
|
||||
<div
|
||||
className={`overview-stat-card ${isDarkMode ? 'dark-mode' : 'light-mode'}`}
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
|
||||
border: isDarkMode ? '1px solid #303030' : '1px solid #f0f0f0',
|
||||
borderRadius: '0px',
|
||||
boxShadow: isDarkMode
|
||||
? '0 2px 8px rgba(0, 0, 0, 0.3)'
|
||||
: '0 2px 8px rgba(0, 0, 0, 0.06)',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
cursor: 'default',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Flex gap={16} align="flex-start">
|
||||
{icon}
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '0px',
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
padding: '24px',
|
||||
backgroundColor: 'transparent',
|
||||
}
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
<Flex gap={20} align="flex-start">
|
||||
<div style={iconContainerStyle}>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<Flex vertical gap={12}>
|
||||
<Typography.Text style={{ fontSize: 16 }}>{title}</Typography.Text>
|
||||
<Flex vertical gap={8} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography.Text style={titleStyle}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
|
||||
<>{children}</>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{/* Decorative element */}
|
||||
<div style={decorativeStyle} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
OverviewStatCard.displayName = 'OverviewStatCard';
|
||||
|
||||
export default OverviewStatCard;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Flex, Typography } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Flex, Typography, theme } from 'antd';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import OverviewStatCard from './overview-stat-card';
|
||||
import { BankOutlined, FileOutlined, UsergroupAddOutlined } from '@ant-design/icons';
|
||||
import { colors } from '@/styles/colors';
|
||||
@@ -12,11 +12,12 @@ const OverviewStats = () => {
|
||||
const [stats, setStats] = useState<IRPTOverviewStatistics>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation('reporting-overview');
|
||||
const { token } = theme.useToken();
|
||||
const includeArchivedProjects = useAppSelector(
|
||||
state => state.reportingReducer.includeArchivedProjects
|
||||
);
|
||||
|
||||
const getOverviewStats = async () => {
|
||||
const getOverviewStats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { done, body } =
|
||||
@@ -29,17 +30,17 @@ const OverviewStats = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [includeArchivedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
getOverviewStats();
|
||||
}, [includeArchivedProjects]);
|
||||
}, [getOverviewStats]);
|
||||
|
||||
const renderStatText = (count: number = 0, singularKey: string, pluralKey: string) => {
|
||||
const renderStatText = useCallback((count: number = 0, singularKey: string, pluralKey: string) => {
|
||||
return `${count} ${count === 1 ? t(singularKey) : t(pluralKey)}`;
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const renderStatCard = (
|
||||
const renderStatCard = useCallback((
|
||||
icon: React.ReactNode,
|
||||
mainCount: number = 0,
|
||||
mainKey: string,
|
||||
@@ -52,81 +53,127 @@ const OverviewStats = () => {
|
||||
>
|
||||
<Flex vertical>
|
||||
{stats.map((stat, index) => (
|
||||
<Typography.Text key={index} type={stat.type}>
|
||||
<Typography.Text
|
||||
key={index}
|
||||
type={stat.type}
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
color: stat.type === 'danger'
|
||||
? '#ff4d4f'
|
||||
: stat.type === 'secondary'
|
||||
? token.colorTextSecondary
|
||||
: token.colorText
|
||||
}}
|
||||
>
|
||||
{stat.text}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</Flex>
|
||||
</OverviewStatCard>
|
||||
);
|
||||
), [renderStatText, loading, token]);
|
||||
|
||||
// Memoize team stats to prevent unnecessary recalculations
|
||||
const teamStats = useMemo(() => [
|
||||
{
|
||||
text: renderStatText(stats?.teams?.projects, 'projectCount', 'projectCountPlural'),
|
||||
type: 'secondary' as const,
|
||||
},
|
||||
{
|
||||
text: renderStatText(stats?.teams?.members, 'memberCount', 'memberCountPlural'),
|
||||
type: 'secondary' as const,
|
||||
},
|
||||
], [stats?.teams?.projects, stats?.teams?.members, renderStatText]);
|
||||
|
||||
// Memoize project stats to prevent unnecessary recalculations
|
||||
const projectStats = useMemo(() => [
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.projects?.active,
|
||||
'activeProjectCount',
|
||||
'activeProjectCountPlural'
|
||||
),
|
||||
type: 'secondary' as const,
|
||||
},
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.projects?.overdue,
|
||||
'overdueProjectCount',
|
||||
'overdueProjectCountPlural'
|
||||
),
|
||||
type: 'danger' as const,
|
||||
},
|
||||
], [stats?.projects?.active, stats?.projects?.overdue, renderStatText]);
|
||||
|
||||
// Memoize member stats to prevent unnecessary recalculations
|
||||
const memberStats = useMemo(() => [
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.members?.unassigned,
|
||||
'unassignedMemberCount',
|
||||
'unassignedMemberCountPlural'
|
||||
),
|
||||
type: 'secondary' as const,
|
||||
},
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.members?.overdue,
|
||||
'memberWithOverdueTaskCount',
|
||||
'memberWithOverdueTaskCountPlural'
|
||||
),
|
||||
type: 'danger' as const,
|
||||
},
|
||||
], [stats?.members?.unassigned, stats?.members?.overdue, renderStatText]);
|
||||
|
||||
// Memoize icons with enhanced styling for better visibility
|
||||
const teamIcon = useMemo(() => (
|
||||
<BankOutlined style={{
|
||||
color: colors.skyBlue,
|
||||
fontSize: 42,
|
||||
filter: 'drop-shadow(0 2px 4px rgba(24, 144, 255, 0.3))'
|
||||
}} />
|
||||
), []);
|
||||
|
||||
const projectIcon = useMemo(() => (
|
||||
<FileOutlined style={{
|
||||
color: colors.limeGreen,
|
||||
fontSize: 42,
|
||||
filter: 'drop-shadow(0 2px 4px rgba(82, 196, 26, 0.3))'
|
||||
}} />
|
||||
), []);
|
||||
|
||||
const memberIcon = useMemo(() => (
|
||||
<UsergroupAddOutlined style={{
|
||||
color: colors.lightGray,
|
||||
fontSize: 42,
|
||||
filter: 'drop-shadow(0 2px 4px rgba(112, 112, 112, 0.3))'
|
||||
}} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<Flex gap={24}>
|
||||
{renderStatCard(
|
||||
<BankOutlined style={{ color: colors.skyBlue, fontSize: 42 }} />,
|
||||
teamIcon,
|
||||
stats?.teams?.count,
|
||||
'teamCount',
|
||||
[
|
||||
{
|
||||
text: renderStatText(stats?.teams?.projects, 'projectCount', 'projectCountPlural'),
|
||||
type: 'secondary',
|
||||
},
|
||||
{
|
||||
text: renderStatText(stats?.teams?.members, 'memberCount', 'memberCountPlural'),
|
||||
type: 'secondary',
|
||||
},
|
||||
]
|
||||
teamStats
|
||||
)}
|
||||
|
||||
{renderStatCard(
|
||||
<FileOutlined style={{ color: colors.limeGreen, fontSize: 42 }} />,
|
||||
projectIcon,
|
||||
stats?.projects?.count,
|
||||
'projectCount',
|
||||
[
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.projects?.active,
|
||||
'activeProjectCount',
|
||||
'activeProjectCountPlural'
|
||||
),
|
||||
type: 'secondary',
|
||||
},
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.projects?.overdue,
|
||||
'overdueProjectCount',
|
||||
'overdueProjectCountPlural'
|
||||
),
|
||||
type: 'danger',
|
||||
},
|
||||
]
|
||||
projectStats
|
||||
)}
|
||||
|
||||
{renderStatCard(
|
||||
<UsergroupAddOutlined style={{ color: colors.lightGray, fontSize: 42 }} />,
|
||||
memberIcon,
|
||||
stats?.members?.count,
|
||||
'memberCount',
|
||||
[
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.members?.unassigned,
|
||||
'unassignedMemberCount',
|
||||
'unassignedMemberCountPlural'
|
||||
),
|
||||
type: 'secondary',
|
||||
},
|
||||
{
|
||||
text: renderStatText(
|
||||
stats?.members?.overdue,
|
||||
'memberWithOverdueTaskCount',
|
||||
'memberWithOverdueTaskCountPlural'
|
||||
),
|
||||
type: 'danger',
|
||||
},
|
||||
]
|
||||
memberStats
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewStats;
|
||||
export default React.memo(OverviewStats);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { memo, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { ConfigProvider, Table, TableColumnsType } from 'antd';
|
||||
import { useAppDispatch } from '../../../../hooks/useAppDispatch';
|
||||
import CustomTableTitle from '../../../../components/CustomTableTitle';
|
||||
@@ -11,7 +11,7 @@ import Avatars from '@/components/avatars/avatars';
|
||||
import OverviewTeamInfoDrawer from '@/components/reporting/drawers/overview-team-info/overview-team-info-drawer';
|
||||
import { toggleOverViewTeamDrawer } from '@/features/reporting/reporting.slice';
|
||||
|
||||
const OverviewReportsTable = () => {
|
||||
const OverviewReportsTable = memo(() => {
|
||||
const { t } = useTranslation('reporting-overview');
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -22,7 +22,7 @@ const OverviewReportsTable = () => {
|
||||
const [teams, setTeams] = useState<IRPTTeam[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getTeams = async () => {
|
||||
const getTeams = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { done, body } = await reportingApiService.getOverviewTeams(includeArchivedProjects);
|
||||
@@ -34,18 +34,19 @@ const OverviewReportsTable = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [includeArchivedProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
getTeams();
|
||||
}, [includeArchivedProjects]);
|
||||
}, [getTeams]);
|
||||
|
||||
const handleDrawerOpen = (team: IRPTTeam) => {
|
||||
const handleDrawerOpen = useCallback((team: IRPTTeam) => {
|
||||
setSelectedTeam(team);
|
||||
dispatch(toggleOverViewTeamDrawer());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const columns: TableColumnsType = [
|
||||
// Memoize table columns to prevent recreation on every render
|
||||
const columns: TableColumnsType<IRPTTeam> = useMemo(() => [
|
||||
{
|
||||
key: 'name',
|
||||
title: <CustomTableTitle title={t('nameColumn')} />,
|
||||
@@ -61,39 +62,45 @@ const OverviewReportsTable = () => {
|
||||
{
|
||||
key: 'members',
|
||||
title: <CustomTableTitle title={t('membersColumn')} />,
|
||||
render: record => <Avatars members={record.members} maxCount={3} />,
|
||||
render: (record: IRPTTeam) => <Avatars members={record.members} maxCount={3} />,
|
||||
},
|
||||
];
|
||||
], [t]);
|
||||
|
||||
// Memoize table configuration
|
||||
const tableConfig = useMemo(() => ({
|
||||
theme: {
|
||||
components: {
|
||||
Table: {
|
||||
cellPaddingBlock: 8,
|
||||
cellPaddingInline: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}), []);
|
||||
|
||||
// Memoize row props generator
|
||||
const getRowProps = useCallback((record: IRPTTeam) => ({
|
||||
onClick: () => handleDrawerOpen(record),
|
||||
style: { height: 48, cursor: 'pointer' },
|
||||
className: 'group even:bg-[#4e4e4e10]',
|
||||
}), [handleDrawerOpen]);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
components: {
|
||||
Table: {
|
||||
cellPaddingBlock: 8,
|
||||
cellPaddingInline: 10,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ConfigProvider {...tableConfig}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={teams}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowKey={record => record.id}
|
||||
loading={loading}
|
||||
onRow={record => {
|
||||
return {
|
||||
onClick: () => handleDrawerOpen(record as IRPTTeam),
|
||||
style: { height: 48, cursor: 'pointer' },
|
||||
className: 'group even:bg-[#4e4e4e10]',
|
||||
};
|
||||
}}
|
||||
onRow={getRowProps}
|
||||
/>
|
||||
|
||||
<OverviewTeamInfoDrawer team={selectedTeam} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(OverviewReportsTable);
|
||||
OverviewReportsTable.displayName = 'OverviewReportsTable';
|
||||
|
||||
export default OverviewReportsTable;
|
||||
|
||||
@@ -42,10 +42,6 @@ const ReportingSider = () => {
|
||||
theme={{
|
||||
components: {
|
||||
Menu: {
|
||||
itemHoverBg: colors.transparent,
|
||||
itemHoverColor: colors.skyBlue,
|
||||
borderRadius: 12,
|
||||
itemMarginBlock: 4,
|
||||
subMenuItemBg: colors.transparent,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -79,6 +79,83 @@
|
||||
border: 1px solid #d3d3d3;
|
||||
}
|
||||
|
||||
/* Enhanced overview stat card styles */
|
||||
.overview-stat-card {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.overview-stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Light mode stat cards */
|
||||
.overview-stat-card.light-mode {
|
||||
background-color: #ffffff !important;
|
||||
border: 1px solid #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.light-mode:hover {
|
||||
border-color: #1890ff !important;
|
||||
box-shadow: 0 4px 16px rgba(24, 144, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.light-mode .ant-card {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.light-mode .ant-card-body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Dark mode stat cards */
|
||||
.overview-stat-card.dark-mode {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.dark-mode:hover {
|
||||
border-color: #1890ff !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.dark-mode .ant-card {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.overview-stat-card.dark-mode .ant-card-body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Force dark mode styles when body has dark class or data attribute */
|
||||
body.dark .overview-stat-card,
|
||||
[data-theme="dark"] .overview-stat-card,
|
||||
.ant-theme-dark .overview-stat-card {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
body.dark .overview-stat-card .ant-card,
|
||||
[data-theme="dark"] .overview-stat-card .ant-card,
|
||||
.ant-theme-dark .overview-stat-card .ant-card {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
body.dark .overview-stat-card .ant-card-body,
|
||||
[data-theme="dark"] .overview-stat-card .ant-card-body,
|
||||
.ant-theme-dark .overview-stat-card .ant-card-body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure no border radius on card components */
|
||||
.overview-stat-card .ant-card,
|
||||
.overview-stat-card .ant-card-body {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* reporting sidebar */
|
||||
.custom-reporting-sider .ant-menu-item-selected {
|
||||
border-inline-end: 3px solid #1890ff !important;
|
||||
|
||||
Reference in New Issue
Block a user