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:
chamikaJ
2025-06-13 16:04:32 +05:30
parent d0c231ee43
commit 4426b5f3ef
7 changed files with 423 additions and 155 deletions

View File

@@ -1,4 +1,5 @@
import { Avatar, Tooltip } from 'antd'; import { Avatar, Tooltip } from 'antd';
import React, { useCallback, useMemo } from 'react';
import { InlineMember } from '@/types/teamMembers/inlineMember.types'; import { InlineMember } from '@/types/teamMembers/inlineMember.types';
interface AvatarsProps { interface AvatarsProps {
@@ -6,41 +7,54 @@ interface AvatarsProps {
maxCount?: number; maxCount?: number;
} }
const renderAvatar = (member: InlineMember, index: number) => ( const Avatars: React.FC<AvatarsProps> = React.memo(({ members, maxCount }) => {
<Tooltip const stopPropagation = useCallback((e: React.MouseEvent) => {
key={member.team_member_id || index} e.stopPropagation();
title={member.end && member.names ? member.names.join(', ') : member.name} }, []);
>
{member.avatar_url ? ( const renderAvatar = useCallback((member: InlineMember, index: number) => (
<span onClick={(e: React.MouseEvent) => e.stopPropagation()}> <Tooltip
<Avatar src={member.avatar_url} size={28} key={member.team_member_id || index} /> key={member.team_member_id || index}
</span> title={member.end && member.names ? member.names.join(', ') : member.name}
) : ( >
<span onClick={(e: React.MouseEvent) => e.stopPropagation()}> {member.avatar_url ? (
<Avatar <span onClick={stopPropagation}>
size={28} <Avatar src={member.avatar_url} size={28} key={member.team_member_id || index} />
key={member.team_member_id || index} </span>
style={{ ) : (
backgroundColor: member.color_code || '#ececec', <span onClick={stopPropagation}>
fontSize: '14px', <Avatar
}} size={28}
> key={member.team_member_id || index}
{member.end && member.names ? member.name : member.name?.charAt(0).toUpperCase()} style={{
</Avatar> backgroundColor: member.color_code || '#ececec',
</span> fontSize: '14px',
)} }}
</Tooltip> >
); {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 ( return (
<div onClick={(e: React.MouseEvent) => e.stopPropagation()}> <div onClick={stopPropagation}>
<Avatar.Group> <Avatar.Group>
{visibleMembers.map((member, index) => renderAvatar(member, index))} {avatarElements}
</Avatar.Group> </Avatar.Group>
</div> </div>
); );
}; });
Avatars.displayName = 'Avatars';
export default Avatars; export default Avatars;

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useCallback, useMemo } from 'react';
import { Button, Card, Checkbox, Flex, Typography } from 'antd'; import { Button, Card, Checkbox, Flex, Typography } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle'; import { useDocumentTitle } from '@/hooks/useDoumentTItle';
@@ -25,29 +25,37 @@ const OverviewReports = () => {
trackMixpanelEvent(evt_reporting_overview); trackMixpanelEvent(evt_reporting_overview);
}, [trackMixpanelEvent]); }, [trackMixpanelEvent]);
const handleArchiveToggle = () => { const handleArchiveToggle = useCallback(() => {
dispatch(toggleIncludeArchived()); 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 ( return (
<Flex vertical gap={24}> <Flex vertical gap={24}>
<CustomPageHeader <CustomPageHeader
title={t('overviewTitle')} title={t('overviewTitle')}
children={ children={headerChildren}
<Button type="text" onClick={handleArchiveToggle}>
<Checkbox checked={includeArchivedProjects} />
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
</Button>
}
/> />
<OverviewStats /> <OverviewStats />
<Card> <Card>
<Flex vertical gap={12}> <Flex vertical gap={12}>
<Typography.Text strong style={{ fontSize: 16 }}> {teamsText}
{t('teamsText')}
</Typography.Text>
<OverviewReportsTable /> <OverviewReportsTable />
</Flex> </Flex>
</Card> </Card>

View File

@@ -1,32 +1,151 @@
import { ReactNode } from 'react'; import { Card, Flex, Typography, theme } from 'antd';
import { Card, Flex, Typography } from 'antd'; import React, { useMemo } from 'react';
type InsightCardProps = { interface InsightCardProps {
icon: ReactNode; icon: React.ReactNode;
title: string; title: string;
children: ReactNode; children: React.ReactNode;
loading?: boolean; 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 ( return (
<Card <div
className="custom-insights-card" className={`overview-stat-card ${isDarkMode ? 'dark-mode' : 'light-mode'}`}
style={{ width: '100%' }} style={{
styles={{ body: { paddingInline: 16 } }} backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
loading={loading} 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"> <Card
{icon} 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}> <Flex vertical gap={8} style={{ flex: 1, minWidth: 0 }}>
<Typography.Text style={{ fontSize: 16 }}>{title}</Typography.Text> <Typography.Text style={titleStyle}>
{title}
</Typography.Text>
<>{children}</> <div style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
marginTop: '4px'
}}>
{children}
</div>
</Flex>
</Flex> </Flex>
</Flex>
</Card> {/* Decorative element */}
<div style={decorativeStyle} />
</Card>
</div>
); );
}; });
OverviewStatCard.displayName = 'OverviewStatCard';
export default OverviewStatCard; export default OverviewStatCard;

View File

@@ -1,5 +1,5 @@
import { Flex, Typography } from 'antd'; import { Flex, Typography, theme } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import OverviewStatCard from './overview-stat-card'; import OverviewStatCard from './overview-stat-card';
import { BankOutlined, FileOutlined, UsergroupAddOutlined } from '@ant-design/icons'; import { BankOutlined, FileOutlined, UsergroupAddOutlined } from '@ant-design/icons';
import { colors } from '@/styles/colors'; import { colors } from '@/styles/colors';
@@ -12,11 +12,12 @@ const OverviewStats = () => {
const [stats, setStats] = useState<IRPTOverviewStatistics>({}); const [stats, setStats] = useState<IRPTOverviewStatistics>({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation('reporting-overview'); const { t } = useTranslation('reporting-overview');
const { token } = theme.useToken();
const includeArchivedProjects = useAppSelector( const includeArchivedProjects = useAppSelector(
state => state.reportingReducer.includeArchivedProjects state => state.reportingReducer.includeArchivedProjects
); );
const getOverviewStats = async () => { const getOverviewStats = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const { done, body } = const { done, body } =
@@ -29,17 +30,17 @@ const OverviewStats = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [includeArchivedProjects]);
useEffect(() => { useEffect(() => {
getOverviewStats(); 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)}`; return `${count} ${count === 1 ? t(singularKey) : t(pluralKey)}`;
}; }, [t]);
const renderStatCard = ( const renderStatCard = useCallback((
icon: React.ReactNode, icon: React.ReactNode,
mainCount: number = 0, mainCount: number = 0,
mainKey: string, mainKey: string,
@@ -52,81 +53,127 @@ const OverviewStats = () => {
> >
<Flex vertical> <Flex vertical>
{stats.map((stat, index) => ( {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} {stat.text}
</Typography.Text> </Typography.Text>
))} ))}
</Flex> </Flex>
</OverviewStatCard> </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 ( return (
<Flex gap={24}> <Flex gap={24}>
{renderStatCard( {renderStatCard(
<BankOutlined style={{ color: colors.skyBlue, fontSize: 42 }} />, teamIcon,
stats?.teams?.count, stats?.teams?.count,
'teamCount', 'teamCount',
[ teamStats
{
text: renderStatText(stats?.teams?.projects, 'projectCount', 'projectCountPlural'),
type: 'secondary',
},
{
text: renderStatText(stats?.teams?.members, 'memberCount', 'memberCountPlural'),
type: 'secondary',
},
]
)} )}
{renderStatCard( {renderStatCard(
<FileOutlined style={{ color: colors.limeGreen, fontSize: 42 }} />, projectIcon,
stats?.projects?.count, stats?.projects?.count,
'projectCount', 'projectCount',
[ projectStats
{
text: renderStatText(
stats?.projects?.active,
'activeProjectCount',
'activeProjectCountPlural'
),
type: 'secondary',
},
{
text: renderStatText(
stats?.projects?.overdue,
'overdueProjectCount',
'overdueProjectCountPlural'
),
type: 'danger',
},
]
)} )}
{renderStatCard( {renderStatCard(
<UsergroupAddOutlined style={{ color: colors.lightGray, fontSize: 42 }} />, memberIcon,
stats?.members?.count, stats?.members?.count,
'memberCount', 'memberCount',
[ memberStats
{
text: renderStatText(
stats?.members?.unassigned,
'unassignedMemberCount',
'unassignedMemberCountPlural'
),
type: 'secondary',
},
{
text: renderStatText(
stats?.members?.overdue,
'memberWithOverdueTaskCount',
'memberWithOverdueTaskCountPlural'
),
type: 'danger',
},
]
)} )}
</Flex> </Flex>
); );
}; };
export default OverviewStats; export default React.memo(OverviewStats);

View File

@@ -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 { ConfigProvider, Table, TableColumnsType } from 'antd';
import { useAppDispatch } from '../../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../../hooks/useAppDispatch';
import CustomTableTitle from '../../../../components/CustomTableTitle'; 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 OverviewTeamInfoDrawer from '@/components/reporting/drawers/overview-team-info/overview-team-info-drawer';
import { toggleOverViewTeamDrawer } from '@/features/reporting/reporting.slice'; import { toggleOverViewTeamDrawer } from '@/features/reporting/reporting.slice';
const OverviewReportsTable = () => { const OverviewReportsTable = memo(() => {
const { t } = useTranslation('reporting-overview'); const { t } = useTranslation('reporting-overview');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -22,7 +22,7 @@ const OverviewReportsTable = () => {
const [teams, setTeams] = useState<IRPTTeam[]>([]); const [teams, setTeams] = useState<IRPTTeam[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const getTeams = async () => { const getTeams = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const { done, body } = await reportingApiService.getOverviewTeams(includeArchivedProjects); const { done, body } = await reportingApiService.getOverviewTeams(includeArchivedProjects);
@@ -34,18 +34,19 @@ const OverviewReportsTable = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [includeArchivedProjects]);
useEffect(() => { useEffect(() => {
getTeams(); getTeams();
}, [includeArchivedProjects]); }, [getTeams]);
const handleDrawerOpen = (team: IRPTTeam) => { const handleDrawerOpen = useCallback((team: IRPTTeam) => {
setSelectedTeam(team); setSelectedTeam(team);
dispatch(toggleOverViewTeamDrawer()); dispatch(toggleOverViewTeamDrawer());
}; }, [dispatch]);
const columns: TableColumnsType = [ // Memoize table columns to prevent recreation on every render
const columns: TableColumnsType<IRPTTeam> = useMemo(() => [
{ {
key: 'name', key: 'name',
title: <CustomTableTitle title={t('nameColumn')} />, title: <CustomTableTitle title={t('nameColumn')} />,
@@ -61,39 +62,45 @@ const OverviewReportsTable = () => {
{ {
key: 'members', key: 'members',
title: <CustomTableTitle title={t('membersColumn')} />, 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 ( return (
<ConfigProvider <ConfigProvider {...tableConfig}>
theme={{
components: {
Table: {
cellPaddingBlock: 8,
cellPaddingInline: 10,
},
},
}}
>
<Table <Table
columns={columns} columns={columns}
dataSource={teams} dataSource={teams}
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
rowKey={record => record.id} rowKey={record => record.id}
loading={loading} loading={loading}
onRow={record => { onRow={getRowProps}
return {
onClick: () => handleDrawerOpen(record as IRPTTeam),
style: { height: 48, cursor: 'pointer' },
className: 'group even:bg-[#4e4e4e10]',
};
}}
/> />
<OverviewTeamInfoDrawer team={selectedTeam} /> <OverviewTeamInfoDrawer team={selectedTeam} />
</ConfigProvider> </ConfigProvider>
); );
}; });
export default memo(OverviewReportsTable); OverviewReportsTable.displayName = 'OverviewReportsTable';
export default OverviewReportsTable;

View File

@@ -42,10 +42,6 @@ const ReportingSider = () => {
theme={{ theme={{
components: { components: {
Menu: { Menu: {
itemHoverBg: colors.transparent,
itemHoverColor: colors.skyBlue,
borderRadius: 12,
itemMarginBlock: 4,
subMenuItemBg: colors.transparent, subMenuItemBg: colors.transparent,
}, },
}, },

View File

@@ -79,6 +79,83 @@
border: 1px solid #d3d3d3; 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 */ /* reporting sidebar */
.custom-reporting-sider .ant-menu-item-selected { .custom-reporting-sider .ant-menu-item-selected {
border-inline-end: 3px solid #1890ff !important; border-inline-end: 3px solid #1890ff !important;