expand sub tasks

This commit is contained in:
chamiakJ
2025-07-03 01:31:05 +05:30
parent 3bef18901a
commit ecd4d29a38
435 changed files with 13150 additions and 11087 deletions

View File

@@ -124,7 +124,9 @@ const MembersReportsTable = () => {
dataSource={membersList}
rowKey={record => record.id}
pagination={{ showSizeChanger: true, defaultPageSize: 10, total: total }}
onChange={(pagination, filters, sorter, extra) => handleOnChange(pagination, filters, sorter, extra)}
onChange={(pagination, filters, sorter, extra) =>
handleOnChange(pagination, filters, sorter, extra)
}
scroll={{ x: 'max-content' }}
loading={isLoading}
onRow={record => {

View File

@@ -22,7 +22,6 @@ const TasksProgressCell = ({ tasksStat }: TasksProgressCellProps) => {
{ percent: donePercent, color: '#98d4b1', label: 'done' },
{ percent: doingPercent, color: '#bce3cc', label: 'doing' },
{ percent: todoPercent, color: '#e3f4ea', label: 'todo' },
];
return (

View File

@@ -28,10 +28,14 @@ const MembersReports = () => {
const { archived, searchQuery, total } = useAppSelector(state => state.membersReportsReducer);
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
const handleExport = () => {
if (!currentSession?.team_name) return;
reportingExportApiService.exportMembers(currentSession.team_name, duration, dateRange, archived);
reportingExportApiService.exportMembers(
currentSession.team_name,
duration,
dateRange,
archived
);
};
useEffect(() => {

View File

@@ -30,26 +30,29 @@ const OverviewReports = () => {
}, [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]);
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]);
const teamsText = useMemo(
() => (
<Typography.Text strong style={{ fontSize: 16 }}>
{t('teamsText')}
</Typography.Text>
),
[t]
);
return (
<Flex vertical gap={24}>
<CustomPageHeader
title={t('overviewTitle')}
children={headerChildren}
/>
<CustomPageHeader title={t('overviewTitle')} children={headerChildren} />
<OverviewStats />

View File

@@ -8,143 +8,147 @@ interface InsightCardProps {
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');
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 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]);
return (
<div
className={`overview-stat-card ${isDarkMode ? 'dark-mode' : 'light-mode'}`}
style={{
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
border: isDarkMode ? '1px solid #303030' : '1px solid #f0f0f0',
// Memoize card container styles with dark mode support
const cardContainerStyle = useMemo(
() => ({
width: '100%',
borderRadius: '0px',
boxShadow: isDarkMode
? '0 2px 8px rgba(0, 0, 0, 0.3)'
: '0 2px 8px rgba(0, 0, 0, 0.06)',
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',
position: 'relative' as const,
cursor: 'default',
width: '100%',
}}
>
<Card
style={{
backgroundColor: 'transparent',
border: 'none',
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]
);
return (
<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%',
}}
styles={{
body: {
padding: '24px',
backgroundColor: 'transparent',
}
}}
loading={loading}
>
<Flex gap={20} align="flex-start">
<div style={iconContainerStyle}>
{icon}
</div>
<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={8} style={{ flex: 1, minWidth: 0 }}>
<Typography.Text style={titleStyle}>
{title}
</Typography.Text>
<Flex vertical gap={8} style={{ flex: 1, minWidth: 0 }}>
<Typography.Text style={titleStyle}>{title}</Typography.Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
marginTop: '4px'
}}>
{children}
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '6px',
marginTop: '4px',
}}
>
{children}
</div>
</Flex>
</Flex>
</Flex>
{/* Decorative element */}
<div style={decorativeStyle} />
</Card>
</div>
);
});
{/* Decorative element */}
<div style={decorativeStyle} />
</Card>
</div>
);
}
);
OverviewStatCard.displayName = 'OverviewStatCard';

View File

@@ -36,142 +36,158 @@ const OverviewStats = () => {
getOverviewStats();
}, [getOverviewStats]);
const renderStatText = useCallback((count: number = 0, singularKey: string, pluralKey: string) => {
return `${count} ${count === 1 ? t(singularKey) : t(pluralKey)}`;
}, [t]);
const renderStatText = useCallback(
(count: number = 0, singularKey: string, pluralKey: string) => {
return `${count} ${count === 1 ? t(singularKey) : t(pluralKey)}`;
},
[t]
);
const renderStatCard = useCallback((
icon: React.ReactNode,
mainCount: number = 0,
mainKey: string,
stats: { text: string; type?: 'secondary' | 'danger' }[]
) => (
<OverviewStatCard
icon={icon}
title={renderStatText(mainCount, mainKey, `${mainKey}Plural`)}
loading={loading}
>
<Flex vertical>
{stats.map((stat, index) => (
<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]);
const renderStatCard = useCallback(
(
icon: React.ReactNode,
mainCount: number = 0,
mainKey: string,
stats: { text: string; type?: 'secondary' | 'danger' }[]
) => (
<OverviewStatCard
icon={icon}
title={renderStatText(mainCount, mainKey, `${mainKey}Plural`)}
loading={loading}
>
<Flex vertical>
{stats.map((stat, index) => (
<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]);
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]);
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]);
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 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 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))'
}} />
), []);
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(
teamIcon,
stats?.teams?.count,
'teamCount',
teamStats
)}
{renderStatCard(teamIcon, stats?.teams?.count, 'teamCount', teamStats)}
{renderStatCard(
projectIcon,
stats?.projects?.count,
'projectCount',
projectStats
)}
{renderStatCard(projectIcon, stats?.projects?.count, 'projectCount', projectStats)}
{renderStatCard(
memberIcon,
stats?.members?.count,
'memberCount',
memberStats
)}
{renderStatCard(memberIcon, stats?.members?.count, 'memberCount', memberStats)}
</Flex>
);
};

View File

@@ -40,50 +40,62 @@ const OverviewReportsTable = memo(() => {
getTeams();
}, [getTeams]);
const handleDrawerOpen = useCallback((team: IRPTTeam) => {
setSelectedTeam(team);
dispatch(toggleOverViewTeamDrawer());
}, [dispatch]);
const handleDrawerOpen = useCallback(
(team: IRPTTeam) => {
setSelectedTeam(team);
dispatch(toggleOverViewTeamDrawer());
},
[dispatch]
);
// Memoize table columns to prevent recreation on every render
const columns: TableColumnsType<IRPTTeam> = useMemo(() => [
{
key: 'name',
title: <CustomTableTitle title={t('nameColumn')} />,
className: 'group-hover:text-[#1890ff]',
dataIndex: 'name',
},
{
key: 'projects',
title: <CustomTableTitle title={t('projectsColumn')} />,
className: 'group-hover:text-[#1890ff]',
dataIndex: 'projects_count',
},
{
key: 'members',
title: <CustomTableTitle title={t('membersColumn')} />,
render: (record: IRPTTeam) => <Avatars members={record.members} maxCount={3} />,
},
], [t]);
const columns: TableColumnsType<IRPTTeam> = useMemo(
() => [
{
key: 'name',
title: <CustomTableTitle title={t('nameColumn')} />,
className: 'group-hover:text-[#1890ff]',
dataIndex: 'name',
},
{
key: 'projects',
title: <CustomTableTitle title={t('projectsColumn')} />,
className: 'group-hover:text-[#1890ff]',
dataIndex: 'projects_count',
},
{
key: 'members',
title: <CustomTableTitle title={t('membersColumn')} />,
render: (record: IRPTTeam) => <Avatars members={record.members} maxCount={3} />,
},
],
[t]
);
// Memoize table configuration
const tableConfig = useMemo(() => ({
theme: {
components: {
Table: {
cellPaddingBlock: 8,
cellPaddingInline: 10,
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]);
const getRowProps = useCallback(
(record: IRPTTeam) => ({
onClick: () => handleDrawerOpen(record),
style: { height: 48, cursor: 'pointer' },
className: 'group even:bg-[#4e4e4e10]',
}),
[handleDrawerOpen]
);
return (
<ConfigProvider {...tableConfig}>

View File

@@ -48,7 +48,6 @@ const ProjectCategoriesFilterDropdown = () => {
// Add filtered categories memo
const filteredCategories = useMemo(() => {
if (!searchQuery.trim()) return orgCategories;
return orgCategories.filter(category =>
@@ -92,22 +91,22 @@ const ProjectCategoriesFilterDropdown = () => {
{filteredCategories.length ? (
filteredCategories.map(category => (
<List.Item
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
key={category.id}
style={{
display: 'flex',
justifyContent: 'flex-start',
gap: 8,
padding: '4px 8px',
border: 'none',
}}
className={`custom-list-item ${themeMode === 'dark' ? 'dark' : ''}`}
key={category.id}
style={{
display: 'flex',
justifyContent: 'flex-start',
gap: 8,
padding: '4px 8px',
border: 'none',
}}
>
<Checkbox id={category.id} onChange={() => handleCategoryChange(category)}>
<Flex gap={8}>
<Badge color={category.color_code} />
{category.name}
</Flex>
</Checkbox>
<Checkbox id={category.id} onChange={() => handleCategoryChange(category)}>
<Flex gap={8}>
<Badge color={category.color_code} />
{category.name}
</Flex>
</Checkbox>
</List.Item>
))
) : (
@@ -129,10 +128,11 @@ const ProjectCategoriesFilterDropdown = () => {
icon={<CaretDownFilled />}
iconPosition="end"
loading={projectCategoriesLoading}
className={`transition-colors duration-300 ${isDropdownOpen
className={`transition-colors duration-300 ${
isDropdownOpen
? 'border-[#1890ff] text-[#1890ff]'
: 'hover:text-[#1890ff hover:border-[#1890ff]'
}`}
}`}
>
{t('categoryText')}
</Button>

View File

@@ -1,5 +1,8 @@
import { fetchProjectHealth } from '@/features/projects/lookups/projectHealth/projectHealthSlice';
import { fetchProjectData, setSelectedProjectHealths } from '@/features/reporting/projectReports/project-reports-slice';
import {
fetchProjectData,
setSelectedProjectHealths,
} from '@/features/reporting/projectReports/project-reports-slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { IProjectHealth } from '@/types/project/projectHealth.types';
@@ -23,22 +26,19 @@ const ProjectHealthFilterDropdown = () => {
state => state.projectReportsReducer
);
useEffect(() => {
setSelectedHealths(selectedProjectHealths);
}, [selectedProjectHealths]);
useEffect(() => {
if (!projectHealthsLoading) dispatch(fetchProjectHealth());
}, [dispatch]);
const debouncedUpdate = useCallback(
debounce((healths: IProjectHealth[]) => {
dispatch(setSelectedProjectHealths(healths));
dispatch(fetchProjectData());
}, 300),
dispatch(fetchProjectData());
}, 300),
[dispatch]
);
@@ -52,8 +52,8 @@ const ProjectHealthFilterDropdown = () => {
updatedHealths = [...selectedHealths, health];
}
setSelectedHealths(updatedHealths);
debouncedUpdate(updatedHealths);
setSelectedHealths(updatedHealths);
debouncedUpdate(updatedHealths);
};
const projectHealthDropdownContent = (
@@ -75,7 +75,7 @@ const ProjectHealthFilterDropdown = () => {
id={item.id}
checked={selectedHealths.some(h => h.id === item.id)}
onChange={() => handleHealthChange(item)}
disabled={projectLoading}
disabled={projectLoading}
>
{item.name}
</Checkbox>
@@ -96,7 +96,7 @@ const ProjectHealthFilterDropdown = () => {
<Button
icon={<CaretDownFilled />}
iconPosition="end"
loading={projectHealthsLoading}
loading={projectHealthsLoading}
className={`transition-colors duration-300 ${
isDropdownOpen
? 'border-[#1890ff] text-[#1890ff]'
@@ -109,4 +109,4 @@ const ProjectHealthFilterDropdown = () => {
);
};
export default ProjectHealthFilterDropdown;
export default ProjectHealthFilterDropdown;

View File

@@ -43,8 +43,8 @@ const ProjectManagersFilterDropdown = () => {
useEffect(() => {
if (!projectManagersLoading) dispatch(fetchProjectManagers());
}, [dispatch]);
}, [dispatch]);
const projectManagerDropdownContent = (
<Card className="custom-card" styles={{ body: { padding: 8, width: 260 } }}>
<Flex vertical gap={8}>

View File

@@ -17,31 +17,40 @@ const ProjectsReportsFilters = () => {
const { searchQuery } = useAppSelector(state => state.projectReportsReducer);
// Memoize the search query handler to prevent recreation on every render
const handleSearchQueryChange = useCallback((text: string) => {
dispatch(setSearchQuery(text));
}, [dispatch]);
const handleSearchQueryChange = useCallback(
(text: string) => {
dispatch(setSearchQuery(text));
},
[dispatch]
);
// Memoize the filter dropdowns container to prevent recreation on every render
const filterDropdowns = useMemo(() => (
<Flex gap={8} wrap={'wrap'}>
<ProjectStatusFilterDropdown />
<ProjectHealthFilterDropdown />
<ProjectCategoriesFilterDropdown />
<ProjectManagersFilterDropdown />
</Flex>
), []);
const filterDropdowns = useMemo(
() => (
<Flex gap={8} wrap={'wrap'}>
<ProjectStatusFilterDropdown />
<ProjectHealthFilterDropdown />
<ProjectCategoriesFilterDropdown />
<ProjectManagersFilterDropdown />
</Flex>
),
[]
);
// Memoize the right side controls to prevent recreation on every render
const rightControls = useMemo(() => (
<Flex gap={12}>
<ProjectTableShowFieldsDropdown />
<CustomSearchbar
placeholderText={t('searchByNamePlaceholder')}
searchQuery={searchQuery}
setSearchQuery={handleSearchQueryChange}
/>
</Flex>
), [t, searchQuery, handleSearchQueryChange]);
const rightControls = useMemo(
() => (
<Flex gap={12}>
<ProjectTableShowFieldsDropdown />
<CustomSearchbar
placeholderText={t('searchByNamePlaceholder')}
searchQuery={searchQuery}
setSearchQuery={handleSearchQueryChange}
/>
</Flex>
),
[t, searchQuery, handleSearchQueryChange]
);
return (
<Flex gap={8} align="center" justify="space-between">

View File

@@ -1,5 +1,8 @@
import { fetchProjectStatuses } from '@/features/projects/lookups/projectStatuses/projectStatusesSlice';
import { fetchProjectData, setSelectedProjectStatuses } from '@/features/reporting/projectReports/project-reports-slice';
import {
fetchProjectData,
setSelectedProjectStatuses,
} from '@/features/reporting/projectReports/project-reports-slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { IProjectStatus } from '@/types/project/projectStatus.types';
@@ -30,7 +33,7 @@ const ProjectStatusFilterDropdown = () => {
const debouncedUpdate = useCallback(
debounce((statuses: IProjectStatus[]) => {
dispatch(setSelectedProjectStatuses(statuses));
dispatch(fetchProjectData());
dispatch(fetchProjectData());
}, 300),
[dispatch]
);
@@ -45,7 +48,7 @@ const ProjectStatusFilterDropdown = () => {
updatedStatuses = [...selectedStatuses, status];
}
setSelectedStatuses(updatedStatuses);
setSelectedStatuses(updatedStatuses);
debouncedUpdate(updatedStatuses);
};
@@ -65,11 +68,7 @@ const ProjectStatusFilterDropdown = () => {
}}
>
<Space>
<Checkbox
id={item.id}
key={item.id}
onChange={e => handleProjectStatusClick(item)}
>
<Checkbox id={item.id} key={item.id} onChange={e => handleProjectStatusClick(item)}>
{item.name}
</Checkbox>
</Space>

View File

@@ -35,11 +35,7 @@ const ProjectTableShowFieldsDropdown = () => {
};
return (
<Dropdown
menu={menuItems}
trigger={['click']}
onOpenChange={open => setIsDropdownOpen(open)}
>
<Dropdown menu={menuItems} trigger={['click']} onOpenChange={open => setIsDropdownOpen(open)}>
<Button
icon={<MoreOutlined />}
className={`transition-colors duration-300 ${

View File

@@ -64,10 +64,13 @@ const ProjectsReportsTable = () => {
const columnsVisibility = useAppSelector(state => state.projectReportsTableColumnsReducer);
// Memoize the drawer open handler to prevent recreation on every render
const handleDrawerOpen = useCallback((record: IRPTProject) => {
setSelectedProject(record);
dispatch(toggleProjectReportsDrawer());
}, [dispatch]);
const handleDrawerOpen = useCallback(
(record: IRPTProject) => {
setSelectedProject(record);
dispatch(toggleProjectReportsDrawer());
},
[dispatch]
);
const columns: TableColumnsType<IRPTProject> = useMemo(
() => [
@@ -242,12 +245,15 @@ const ProjectsReportsTable = () => {
);
// Memoize the table change handler to prevent recreation on every render
const handleTableChange = useCallback((pagination: PaginationProps, filters: any, sorter: any) => {
if (sorter.order) dispatch(setOrder(sorter.order));
if (sorter.field) dispatch(setField(sorter.field));
dispatch(setIndex(pagination.current));
dispatch(setPageSize(pagination.pageSize));
}, [dispatch]);
const handleTableChange = useCallback(
(pagination: PaginationProps, filters: any, sorter: any) => {
if (sorter.order) dispatch(setOrder(sorter.order));
if (sorter.field) dispatch(setField(sorter.field));
dispatch(setIndex(pagination.current));
dispatch(setPageSize(pagination.pageSize));
},
[dispatch]
);
useEffect(() => {
if (!isLoading) dispatch(fetchProjectData());
@@ -295,13 +301,16 @@ const ProjectsReportsTable = () => {
);
// Memoize pagination configuration to prevent recreation on every render
const paginationConfig = useMemo(() => ({
showSizeChanger: true,
defaultPageSize: 10,
total: total,
current: index,
pageSizeOptions: PAGE_SIZE_OPTIONS,
}), [total, index]);
const paginationConfig = useMemo(
() => ({
showSizeChanger: true,
defaultPageSize: 10,
total: total,
current: index,
pageSizeOptions: PAGE_SIZE_OPTIONS,
}),
[total, index]
);
// Memoize scroll configuration to prevent recreation on every render
const scrollConfig = useMemo(() => ({ x: 'max-content' }), []);

View File

@@ -58,20 +58,20 @@ const ProjectCategoryCell = ({ id, name, color_code, projectId }: ProjectCategor
</Typography.Text>
),
}));
// handle category select
const onClick: MenuProps['onClick'] = e => {
const newCategory = filteredCategoriesData.find(category => category.id === e.key);
if (newCategory && connected && socket) {
// Update local state immediately
setSelectedCategory(newCategory);
// Emit socket event
socket.emit(
SocketEvents.PROJECT_CATEGORY_CHANGE.toString(),
JSON.stringify({
project_id: projectId,
category_id: newCategory.id
category_id: newCategory.id,
})
);
}
@@ -133,12 +133,14 @@ const ProjectCategoryCell = ({ id, name, color_code, projectId }: ProjectCategor
if (parsedData && parsedData.project_id === projectId) {
// Update local state
setSelectedCategory(parsedData.category);
// Update redux store
dispatch(updateProjectCategory({
projectId: parsedData.project_id,
category: parsedData.category
}));
dispatch(
updateProjectCategory({
projectId: parsedData.project_id,
category: parsedData.category,
})
);
}
} catch (error) {
console.error('Error handling category change response:', error);
@@ -200,7 +202,7 @@ const ProjectCategoryCell = ({ id, name, color_code, projectId }: ProjectCategor
// Action creator for updating project category
const updateProjectCategory = (payload: { projectId: string; category: IProjectCategory }) => ({
type: 'projects/updateCategory',
payload
payload,
});
export default ProjectCategoryCell;

View File

@@ -5,7 +5,10 @@ import dayjs, { Dayjs } from 'dayjs';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setProjectEndDate, setProjectStartDate } from '@/features/reporting/projectReports/project-reports-slice';
import {
setProjectEndDate,
setProjectStartDate,
} from '@/features/reporting/projectReports/project-reports-slice';
import logger from '@/utils/errorLogger';
type ProjectDatesCellProps = {
@@ -41,10 +44,13 @@ const ProjectDatesCell = ({ projectId, startDate, endDate }: ProjectDatesCellPro
if (!socket) {
throw new Error('Socket connection not available');
}
socket.emit(SocketEvents.PROJECT_START_DATE_CHANGE.toString(), JSON.stringify({
project_id: projectId,
start_date: date?.format('YYYY-MM-DD'),
}));
socket.emit(
SocketEvents.PROJECT_START_DATE_CHANGE.toString(),
JSON.stringify({
project_id: projectId,
start_date: date?.format('YYYY-MM-DD'),
})
);
} catch (error) {
logger.error('Error sending start date change:', error);
}
@@ -55,10 +61,13 @@ const ProjectDatesCell = ({ projectId, startDate, endDate }: ProjectDatesCellPro
if (!socket) {
throw new Error('Socket connection not available');
}
socket.emit(SocketEvents.PROJECT_END_DATE_CHANGE.toString(), JSON.stringify({
project_id: projectId,
end_date: date?.format('YYYY-MM-DD'),
}));
socket.emit(
SocketEvents.PROJECT_END_DATE_CHANGE.toString(),
JSON.stringify({
project_id: projectId,
end_date: date?.format('YYYY-MM-DD'),
})
);
} catch (error) {
logger.error('Error sending end date change:', error);
}
@@ -70,8 +79,14 @@ const ProjectDatesCell = ({ projectId, startDate, endDate }: ProjectDatesCellPro
socket.on(SocketEvents.PROJECT_END_DATE_CHANGE.toString(), handleEndDateChangeResponse);
return () => {
socket.removeListener(SocketEvents.PROJECT_START_DATE_CHANGE.toString(), handleStartDateChangeResponse);
socket.removeListener(SocketEvents.PROJECT_END_DATE_CHANGE.toString(), handleEndDateChangeResponse);
socket.removeListener(
SocketEvents.PROJECT_START_DATE_CHANGE.toString(),
handleStartDateChangeResponse
);
socket.removeListener(
SocketEvents.PROJECT_END_DATE_CHANGE.toString(),
handleEndDateChangeResponse
);
};
}
}, [connected, socket]);

View File

@@ -47,10 +47,13 @@ const ProjectHealthCell = ({ value, label, color, projectId }: HealthStatusDataT
const onClick: MenuProps['onClick'] = e => {
if (!e.key || !projectId) return;
socket?.emit(SocketEvents.PROJECT_HEALTH_CHANGE.toString(), JSON.stringify({
project_id: projectId,
health_id: e.key,
}));
socket?.emit(
SocketEvents.PROJECT_HEALTH_CHANGE.toString(),
JSON.stringify({
project_id: projectId,
health_id: e.key,
})
);
};
// dropdown items

View File

@@ -35,7 +35,7 @@ const ProjectStatusCell = ({ currentStatus, projectId }: ProjectStatusCellProps)
{getStatusIcon(status.icon || '', status.color_code || '')}
{t(`${status.name}`)}
</Typography.Text>
)
),
}));
const handleStatusChange = (value: string) => {

View File

@@ -12,7 +12,7 @@ const ProjectUpdateCell = ({ updates }: ProjectUpdateCellProps) => {
ellipsis={{ expanded: false }}
className="group-hover:text-[#1890ff]"
>
<div dangerouslySetInnerHTML={{__html: updates}} />
<div dangerouslySetInnerHTML={{ __html: updates }} />
</Typography.Text>
);
};

View File

@@ -39,36 +39,37 @@ const ProjectsReports = () => {
}, [dispatch, archived]);
// Memoize the dropdown menu items to prevent recreation on every render
const dropdownMenuItems = useMemo(() => [
{ key: '1', label: t('excelButton'), onClick: handleExcelExport }
], [t, handleExcelExport]);
const dropdownMenuItems = useMemo(
() => [{ key: '1', label: t('excelButton'), onClick: handleExcelExport }],
[t, handleExcelExport]
);
// Memoize the header children to prevent recreation on every render
const headerChildren = useMemo(() => (
<Space>
<Button>
<Checkbox checked={archived} onChange={handleArchivedChange}>
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
</Checkbox>
</Button>
<Dropdown menu={{ items: dropdownMenuItems }}>
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('exportButton')}
const headerChildren = useMemo(
() => (
<Space>
<Button>
<Checkbox checked={archived} onChange={handleArchivedChange}>
<Typography.Text>{t('includeArchivedButton')}</Typography.Text>
</Checkbox>
</Button>
</Dropdown>
</Space>
), [archived, handleArchivedChange, t, dropdownMenuItems]);
<Dropdown menu={{ items: dropdownMenuItems }}>
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('exportButton')}
</Button>
</Dropdown>
</Space>
),
[archived, handleArchivedChange, t, dropdownMenuItems]
);
// Memoize the card title to prevent recreation on every render
const cardTitle = useMemo(() => <ProjectsReportsFilters />, []);
return (
<Flex vertical>
<CustomPageHeader
title={pageTitle}
children={headerChildren}
/>
<CustomPageHeader title={pageTitle} children={headerChildren} />
<Card title={cardTitle}>
<ProjectReportsTable />

View File

@@ -40,7 +40,6 @@ const ReportingCollapsedButton = ({
}
};
useEffect(() => {
getOrganizationDetails();
}, []);
@@ -66,7 +65,7 @@ const ReportingCollapsedButton = ({
/>
<Typography.Text strong>
{loading ? 'Loading...' : organization?.name || 'Unknown Organization'}
{loading ? 'Loading...' : organization?.name || 'Unknown Organization'}
</Typography.Text>
</Flex>
</Tooltip>

View File

@@ -19,15 +19,23 @@ import { reportingTimesheetApiService } from '@/api/reporting/reporting.timeshee
// Project color palette
const PROJECT_COLORS = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEEAD',
'#D4A5A5', '#9B59B6', '#3498DB', '#F1C40F', '#1ABC9C'
'#FF6B6B',
'#4ECDC4',
'#45B7D1',
'#96CEB4',
'#FFEEAD',
'#D4A5A5',
'#9B59B6',
'#3498DB',
'#F1C40F',
'#1ABC9C',
];
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);
enum IToggleOptions {
'WORKING_DAYS',
'MAN_DAYS'
'MAN_DAYS',
}
export interface EstimatedVsActualTimeSheetRef {
@@ -38,18 +46,21 @@ interface IEstimatedVsActualTimeSheetProps {
type: string;
}
const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEstimatedVsActualTimeSheetProps>(({ type }, ref) => {
const EstimatedVsActualTimeSheet = forwardRef<
EstimatedVsActualTimeSheetRef,
IEstimatedVsActualTimeSheetProps
>(({ type }, ref) => {
const chartRef = useRef<any>(null);
// State for filters and data
const [jsonData, setJsonData] = useState<IRPTTimeProject[]>([]);
const [loading, setLoading] = useState(false);
const [chartHeight, setChartHeight] = useState(600);
const [chartWidth, setChartWidth] = useState(1080);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const {
teams,
loadingTeams,
@@ -61,28 +72,29 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
billable,
archived,
} = useAppSelector(state => state.timeReportsOverviewReducer);
const {
duration,
dateRange,
} = useAppSelector(state => state.reportingReducer);
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
// Add type checking before mapping
const labels = Array.isArray(jsonData) ? jsonData.map(item => item.name) : [];
const actualDays = Array.isArray(jsonData) ? jsonData.map(item => {
const value = item.value ? parseFloat(item.value) : 0;
return (isNaN(value) ? 0 : value).toString();
}) : [];
const estimatedDays = Array.isArray(jsonData) ? jsonData.map(item => {
const value = item.estimated_value ? parseFloat(item.estimated_value) : 0;
return (isNaN(value) ? 0 : value).toString();
}) : [];
const actualDays = Array.isArray(jsonData)
? jsonData.map(item => {
const value = item.value ? parseFloat(item.value) : 0;
return (isNaN(value) ? 0 : value).toString();
})
: [];
const estimatedDays = Array.isArray(jsonData)
? jsonData.map(item => {
const value = item.estimated_value ? parseFloat(item.estimated_value) : 0;
return (isNaN(value) ? 0 : value).toString();
})
: [];
// Format date helper
const formatDate = (date: Date): string => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
year: 'numeric',
});
};
@@ -126,7 +138,7 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
}
return '';
},
}
},
},
datalabels: {
color: 'white',
@@ -190,14 +202,14 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
projects: selectedProjects.map(p => p.id),
duration: duration,
date_range: dateRange,
billable
billable,
};
const res = await reportingTimesheetApiService.getProjectEstimatedVsActual(body, archived);
if (res.done) {
// Ensure res.body is an array before setting it
const dataArray = Array.isArray(res.body) ? res.body : [];
setJsonData(dataArray);
// Update chart dimensions based on data
if (dataArray.length) {
const containerWidth = window.innerWidth - 300;
@@ -224,14 +236,14 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
billable,
archived,
type,
noCategory
noCategory,
]);
const exportChart = () => {
if (chartRef.current) {
// Get the canvas element
const canvas = chartRef.current.canvas;
// Create a temporary canvas to draw with background
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
@@ -262,19 +274,20 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
};
useImperativeHandle(ref, () => ({
exportChart
exportChart,
}));
return (
<div style={{ position: 'relative' }}>
{/* Outer container with fixed width */}
<div style={{
width: '100%',
position: 'relative',
overflowX: 'auto',
overflowY: 'hidden'
}}>
<div
style={{
width: '100%',
position: 'relative',
overflowX: 'auto',
overflowY: 'hidden',
}}
>
{/* Chart container */}
<div
style={{
@@ -283,12 +296,12 @@ const EstimatedVsActualTimeSheet = forwardRef<EstimatedVsActualTimeSheetRef, IEs
minWidth: 'max-content',
}}
>
<Bar
<Bar
ref={chartRef}
data={data}
options={options}
width={chartWidth}
height={chartHeight}
data={data}
options={options}
width={chartWidth}
height={chartHeight}
/>
</div>
</div>

View File

@@ -44,10 +44,12 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const [jsonData, setJsonData] = useState<IRPTTimeMember[]>([]);
const labels = Array.isArray(jsonData) ? jsonData.map(item => item.name) : [];
const dataValues = Array.isArray(jsonData) ? jsonData.map(item => {
const loggedTimeInHours = parseFloat(item.logged_time || '0') / 3600;
return loggedTimeInHours.toFixed(2);
}) : [];
const dataValues = Array.isArray(jsonData)
? jsonData.map(item => {
const loggedTimeInHours = parseFloat(item.logged_time || '0') / 3600;
return loggedTimeInHours.toFixed(2);
})
: [];
const colors = Array.isArray(jsonData) ? jsonData.map(item => item.color_code) : [];
const themeMode = useAppSelector(state => state.themeReducer.mode);
@@ -83,7 +85,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
},
tooltip: {
callbacks: {
label: function(context: any) {
label: function (context: any) {
const idx = context.dataIndex;
const member = jsonData[idx];
const hours = member?.utilized_hours || '0.00';
@@ -92,11 +94,11 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
return [
`${context.dataset.label}: ${hours} h`,
`Utilization: ${percent}%`,
`Over/Under Utilized: ${overUnder} h`
`Over/Under Utilized: ${overUnder} h`,
];
}
}
}
},
},
},
},
backgroundColor: 'black',
indexAxis: 'y' as const,
@@ -136,7 +138,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const fetchChartData = async () => {
try {
setLoading(true);
const selectedTeams = teams.filter(team => team.selected);
const selectedProjects = filterProjects.filter(project => project.selected);
const selectedCategories = categories.filter(category => category.selected);
@@ -169,7 +171,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
if (chartRef.current) {
// Get the canvas element
const canvas = chartRef.current.canvas;
// Create a temporary canvas to draw with background
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
@@ -195,7 +197,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
};
useImperativeHandle(ref, () => ({
exportChart
exportChart,
}));
return (

View File

@@ -11,9 +11,7 @@ import {
} from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { useAppDispatch } from '../../../../hooks/useAppDispatch';
import {
setLabelAndToggleDrawer,
} from '../../../../features/timeReport/projects/timeLogSlice';
import { setLabelAndToggleDrawer } from '../../../../features/timeReport/projects/timeLogSlice';
import ProjectTimeLogDrawer from '../../../../features/timeReport/projects/ProjectTimeLogDrawer';
import { useAppSelector } from '../../../../hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
@@ -65,16 +63,22 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
const data = {
labels: Array.isArray(jsonData) ? jsonData.map(item => item?.name || '') : [],
datasets: [{
label: t('loggedTime'),
data: Array.isArray(jsonData) ? jsonData.map(item => {
const loggedTime = item?.logged_time || '0';
const loggedTimeInHours = parseFloat(loggedTime) / 3600;
return loggedTimeInHours.toFixed(2);
}) : [],
backgroundColor: Array.isArray(jsonData) ? jsonData.map(item => item?.color_code || '#000000') : [],
barThickness: BAR_THICKNESS,
}],
datasets: [
{
label: t('loggedTime'),
data: Array.isArray(jsonData)
? jsonData.map(item => {
const loggedTime = item?.logged_time || '0';
const loggedTimeInHours = parseFloat(loggedTime) / 3600;
return loggedTimeInHours.toFixed(2);
})
: [],
backgroundColor: Array.isArray(jsonData)
? jsonData.map(item => item?.color_code || '#000000')
: [],
barThickness: BAR_THICKNESS,
},
],
};
const options = {
@@ -170,14 +174,14 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
archived,
loadingTeams,
loadingProjects,
loadingCategories
loadingCategories,
]);
const exportChart = () => {
if (chartRef.current) {
// Get the canvas element
const canvas = chartRef.current.canvas;
// Create a temporary canvas to draw with background
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
@@ -203,7 +207,7 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
};
useImperativeHandle(ref, () => ({
exportChart
exportChart,
}));
// if (loading) {
@@ -224,11 +228,7 @@ const ProjectTimeSheetChart = forwardRef<ProjectTimeSheetChartRef>((_, ref) => {
height: `${60 * data.labels.length}px`,
}}
>
<Bar
data={data}
options={options}
ref={chartRef}
/>
<Bar data={data} options={options} ref={chartRef} />
</div>
<ProjectTimeLogDrawer />
</div>

View File

@@ -51,7 +51,8 @@
border-left: none !important;
}
.member-name, .total-time {
.member-name,
.total-time {
border-bottom: 2px solid var(--border-color, rgba(0, 0, 0, 0.06));
}
}

View File

@@ -1,6 +1,8 @@
import { Card, Flex, Segmented } from 'antd';
import TimeReportPageHeader from '@/pages/reporting/timeReports/page-header/time-report-page-header';
import EstimatedVsActualTimeSheet, { EstimatedVsActualTimeSheetRef } from '@/pages/reporting/time-reports/estimated-vs-actual-time-sheet/estimated-vs-actual-time-sheet';
import EstimatedVsActualTimeSheet, {
EstimatedVsActualTimeSheetRef,
} from '@/pages/reporting/time-reports/estimated-vs-actual-time-sheet/estimated-vs-actual-time-sheet';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
@@ -23,9 +25,7 @@ const EstimatedVsActualTimeReports = () => {
<Flex vertical>
<TimeReportingRightHeader
title={t('estimatedVsActual')}
exportType={[
{ key: 'png', label: 'PNG' },
]}
exportType={[{ key: 'png', label: 'PNG' }]}
export={handleExport}
/>
@@ -42,13 +42,16 @@ const EstimatedVsActualTimeReports = () => {
<TimeReportPageHeader />
<Segmented
style={{ fontWeight: 500 }}
options={[{
label: t('workingDays'),
value: 'WORKING_DAYS',
}, {
label: t('manDays'),
value: 'MAN_DAYS',
}]}
options={[
{
label: t('workingDays'),
value: 'WORKING_DAYS',
},
{
label: t('manDays'),
value: 'MAN_DAYS',
},
]}
onChange={value => setType(value)}
/>
</div>

View File

@@ -1,6 +1,8 @@
import { Card, Flex } from 'antd';
import TimeReportPageHeader from '@/pages/reporting/timeReports/page-header/time-report-page-header';
import MembersTimeSheet, { MembersTimeSheetRef } from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet';
import MembersTimeSheet, {
MembersTimeSheetRef,
} from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet';
import TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';

View File

@@ -1,4 +1,9 @@
import { fetchReportingProjects, setNoCategory, setSelectOrDeselectAllCategories, setSelectOrDeselectCategory } from '@/features/reporting/time-reports/time-reports-overview.slice';
import {
fetchReportingProjects,
setNoCategory,
setSelectOrDeselectAllCategories,
setSelectOrDeselectCategory,
} from '@/features/reporting/time-reports/time-reports-overview.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { CaretDownFilled } from '@ant-design/icons';
@@ -36,7 +41,6 @@ const Categories: React.FC = () => {
await dispatch(setNoCategory(isChecked));
await dispatch(setSelectOrDeselectAllCategories(isChecked));
await dispatch(fetchReportingProjects());
};
const handleNoCategoryChange = async (checked: boolean) => {
@@ -51,15 +55,17 @@ const Categories: React.FC = () => {
placement="bottomLeft"
trigger={['click']}
dropdownRender={() => (
<div style={{
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadow,
padding: '4px 0',
maxHeight: '330px',
display: 'flex',
flexDirection: 'column'
}}>
<div
style={{
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadow,
padding: '4px 0',
maxHeight: '330px',
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ padding: '8px', flexShrink: 0 }}>
<Input
onClick={e => e.stopPropagation()}
@@ -89,17 +95,19 @@ const Categories: React.FC = () => {
</Checkbox>
</div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<div style={{
overflowY: 'auto',
flex: 1
}}>
<div
style={{
overflowY: 'auto',
flex: 1,
}}
>
{filteredItems.length > 0 ? (
filteredItems.map(item => (
<div
<div
key={item.id}
style={{
style={{
padding: '8px 12px',
cursor: 'pointer'
cursor: 'pointer',
}}
>
<Checkbox
@@ -112,9 +120,7 @@ const Categories: React.FC = () => {
</div>
))
) : (
<div style={{ padding: '8px 12px' }}>
{t('noCategories')}
</div>
<div style={{ padding: '8px 12px' }}>{t('noCategories')}</div>
)}
</div>
</div>

View File

@@ -1,8 +1,32 @@
import { setSelectOrDeselectAllProjects, setSelectOrDeselectProject } from '@/features/reporting/time-reports/time-reports-overview.slice';
import {
setSelectOrDeselectAllProjects,
setSelectOrDeselectProject,
} from '@/features/reporting/time-reports/time-reports-overview.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { CaretDownFilled, SearchOutlined, ClearOutlined, DownOutlined, RightOutlined, FilterOutlined } from '@ant-design/icons';
import { Button, Checkbox, Divider, Dropdown, Input, theme, Typography, Badge, Collapse, Select, Space, Tooltip, Empty } from 'antd';
import {
CaretDownFilled,
SearchOutlined,
ClearOutlined,
DownOutlined,
RightOutlined,
FilterOutlined,
} from '@ant-design/icons';
import {
Button,
Checkbox,
Divider,
Dropdown,
Input,
theme,
Typography,
Badge,
Collapse,
Select,
Space,
Tooltip,
Empty,
} from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useState, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -34,57 +58,63 @@ const Projects: React.FC = () => {
const themeMode = useAppSelector(state => state.themeReducer.mode);
// Theme-aware color utilities
const getThemeAwareColor = useCallback((lightColor: string, darkColor: string) => {
return themeWiseColor(lightColor, darkColor, themeMode);
}, [themeMode]);
const getThemeAwareColor = useCallback(
(lightColor: string, darkColor: string) => {
return themeWiseColor(lightColor, darkColor, themeMode);
},
[themeMode]
);
// Enhanced color processing for project/group colors
const processColor = useCallback((color: string | undefined, fallback?: string) => {
if (!color) return fallback || token.colorPrimary;
// If it's a hex color, ensure it has good contrast in both themes
if (color.startsWith('#')) {
// For dark mode, lighten dark colors and darken light colors for better visibility
if (themeMode === 'dark') {
// Simple brightness adjustment for dark mode
const hex = color.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculate brightness (0-255)
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If color is too dark in dark mode, lighten it
if (brightness < 100) {
const factor = 1.5;
const newR = Math.min(255, Math.floor(r * factor));
const newG = Math.min(255, Math.floor(g * factor));
const newB = Math.min(255, Math.floor(b * factor));
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}
} else {
// For light mode, ensure colors aren't too light
const hex = color.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If color is too light in light mode, darken it
if (brightness > 200) {
const factor = 0.7;
const newR = Math.floor(r * factor);
const newG = Math.floor(g * factor);
const newB = Math.floor(b * factor);
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
const processColor = useCallback(
(color: string | undefined, fallback?: string) => {
if (!color) return fallback || token.colorPrimary;
// If it's a hex color, ensure it has good contrast in both themes
if (color.startsWith('#')) {
// For dark mode, lighten dark colors and darken light colors for better visibility
if (themeMode === 'dark') {
// Simple brightness adjustment for dark mode
const hex = color.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculate brightness (0-255)
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If color is too dark in dark mode, lighten it
if (brightness < 100) {
const factor = 1.5;
const newR = Math.min(255, Math.floor(r * factor));
const newG = Math.min(255, Math.floor(g * factor));
const newB = Math.min(255, Math.floor(b * factor));
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}
} else {
// For light mode, ensure colors aren't too light
const hex = color.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
// If color is too light in light mode, darken it
if (brightness > 200) {
const factor = 0.7;
const newR = Math.floor(r * factor);
const newG = Math.floor(g * factor);
const newB = Math.floor(b * factor);
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}
}
}
}
return color;
}, [themeMode, token.colorPrimary]);
return color;
},
[themeMode, token.colorPrimary]
);
// Memoized filtered projects
const filteredProjects = useMemo(() => {
@@ -102,15 +132,17 @@ const Projects: React.FC = () => {
// Memoized grouped projects
const groupedProjects = useMemo(() => {
if (groupBy === 'none') {
return [{
key: 'all',
name: t('projects'),
projects: filteredProjects
}];
return [
{
key: 'all',
name: t('projects'),
projects: filteredProjects,
},
];
}
const groups: { [key: string]: ProjectGroup } = {};
filteredProjects.forEach(project => {
let groupKey: string;
let groupName: string;
@@ -142,7 +174,7 @@ const Projects: React.FC = () => {
key: groupKey,
name: groupName,
color: processColor(groupColor),
projects: []
projects: [],
};
}
@@ -153,64 +185,67 @@ const Projects: React.FC = () => {
}, [filteredProjects, groupBy, t, processColor]);
// Selected projects count
const selectedCount = useMemo(() =>
projects.filter(p => p.selected).length,
[projects]
);
const selectedCount = useMemo(() => projects.filter(p => p.selected).length, [projects]);
const allSelected = useMemo(() =>
filteredProjects.length > 0 && filteredProjects.every(p => p.selected),
const allSelected = useMemo(
() => filteredProjects.length > 0 && filteredProjects.every(p => p.selected),
[filteredProjects]
);
const indeterminate = useMemo(() =>
filteredProjects.some(p => p.selected) && !allSelected,
const indeterminate = useMemo(
() => filteredProjects.some(p => p.selected) && !allSelected,
[filteredProjects, allSelected]
);
// Memoize group by options
const groupByOptions = useMemo(() => [
{ value: 'none', label: t('groupByNone') },
{ value: 'category', label: t('groupByCategory') },
{ value: 'team', label: t('groupByTeam') },
{ value: 'status', label: t('groupByStatus') },
], [t]);
const groupByOptions = useMemo(
() => [
{ value: 'none', label: t('groupByNone') },
{ value: 'category', label: t('groupByCategory') },
{ value: 'team', label: t('groupByTeam') },
{ value: 'status', label: t('groupByStatus') },
],
[t]
);
// Memoize dropdown styles to prevent recalculation on every render
const dropdownStyles = useMemo(() => ({
dropdown: {
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadowSecondary,
border: `1px solid ${token.colorBorder}`,
},
groupHeader: {
backgroundColor: getThemeAwareColor(token.colorFillTertiary, token.colorFillQuaternary),
borderRadius: token.borderRadiusSM,
padding: '8px 12px',
marginBottom: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease',
border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
},
projectItem: {
padding: '8px 12px',
borderRadius: token.borderRadiusSM,
transition: 'all 0.2s ease',
cursor: 'pointer',
border: `1px solid transparent`,
},
toggleIcon: {
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
fontSize: '12px',
transition: 'all 0.2s ease',
},
expandedToggleIcon: {
color: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
fontSize: '12px',
transition: 'all 0.2s ease',
}
}), [token, getThemeAwareColor]);
const dropdownStyles = useMemo(
() => ({
dropdown: {
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadowSecondary,
border: `1px solid ${token.colorBorder}`,
},
groupHeader: {
backgroundColor: getThemeAwareColor(token.colorFillTertiary, token.colorFillQuaternary),
borderRadius: token.borderRadiusSM,
padding: '8px 12px',
marginBottom: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease',
border: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
},
projectItem: {
padding: '8px 12px',
borderRadius: token.borderRadiusSM,
transition: 'all 0.2s ease',
cursor: 'pointer',
border: `1px solid transparent`,
},
toggleIcon: {
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary),
fontSize: '12px',
transition: 'all 0.2s ease',
},
expandedToggleIcon: {
color: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
fontSize: '12px',
transition: 'all 0.2s ease',
},
}),
[token, getThemeAwareColor]
);
// Memoize search placeholder and clear tooltip
const searchPlaceholder = useMemo(() => t('searchByProject'), [t]);
@@ -224,15 +259,21 @@ const Projects: React.FC = () => {
const collapseAllText = useMemo(() => t('collapseAll'), [t]);
// Handle checkbox change for individual items
const handleCheckboxChange = useCallback((key: string, checked: boolean) => {
dispatch(setSelectOrDeselectProject({ id: key, selected: checked }));
}, [dispatch]);
const handleCheckboxChange = useCallback(
(key: string, checked: boolean) => {
dispatch(setSelectOrDeselectProject({ id: key, selected: checked }));
},
[dispatch]
);
// Handle "Select All" checkbox change
const handleSelectAllChange = useCallback((e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
dispatch(setSelectOrDeselectAllProjects(isChecked));
}, [dispatch]);
const handleSelectAllChange = useCallback(
(e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
dispatch(setSelectOrDeselectAllProjects(isChecked));
},
[dispatch]
);
// Clear search
const clearSearch = useCallback(() => {
@@ -241,49 +282,57 @@ const Projects: React.FC = () => {
// Toggle group expansion
const toggleGroupExpansion = useCallback((groupKey: string) => {
setExpandedGroups(prev =>
prev.includes(groupKey)
? prev.filter(key => key !== groupKey)
: [...prev, groupKey]
setExpandedGroups(prev =>
prev.includes(groupKey) ? prev.filter(key => key !== groupKey) : [...prev, groupKey]
);
}, []);
// Expand/Collapse all groups
const toggleAllGroups = useCallback((expand: boolean) => {
if (expand) {
setExpandedGroups(groupedProjects.map(g => g.key));
} else {
setExpandedGroups([]);
}
}, [groupedProjects]);
const toggleAllGroups = useCallback(
(expand: boolean) => {
if (expand) {
setExpandedGroups(groupedProjects.map(g => g.key));
} else {
setExpandedGroups([]);
}
},
[groupedProjects]
);
// Render project group
const renderProjectGroup = (group: ProjectGroup) => {
const isExpanded = expandedGroups.includes(group.key) || groupBy === 'none';
const groupSelectedCount = group.projects.filter(p => p.selected).length;
return (
<div key={group.key} style={{ marginBottom: '8px' }}>
{groupBy !== 'none' && (
<div
<div
style={{
...dropdownStyles.groupHeader,
backgroundColor: isExpanded
backgroundColor: isExpanded
? getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary)
: dropdownStyles.groupHeader.backgroundColor
: dropdownStyles.groupHeader.backgroundColor,
}}
onClick={() => toggleGroupExpansion(group.key)}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary);
e.currentTarget.style.borderColor = getThemeAwareColor(token.colorBorder, token.colorBorderSecondary);
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = getThemeAwareColor(
token.colorFillSecondary,
token.colorFillTertiary
);
e.currentTarget.style.borderColor = getThemeAwareColor(
token.colorBorder,
token.colorBorderSecondary
);
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isExpanded
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = isExpanded
? getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary)
: dropdownStyles.groupHeader.backgroundColor;
e.currentTarget.style.borderColor = getThemeAwareColor(token.colorBorderSecondary, token.colorBorder);
e.currentTarget.style.borderColor = getThemeAwareColor(
token.colorBorderSecondary,
token.colorBorder
);
}}
>
<Space>
@@ -292,42 +341,53 @@ const Projects: React.FC = () => {
) : (
<RightOutlined style={dropdownStyles.toggleIcon} />
)}
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: group.color || processColor(undefined, token.colorPrimary),
flexShrink: 0,
border: `1px solid ${getThemeAwareColor('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.2)')}`
}} />
<Text strong style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase)
}}>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: group.color || processColor(undefined, token.colorPrimary),
flexShrink: 0,
border: `1px solid ${getThemeAwareColor('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.2)')}`,
}}
/>
<Text
strong
style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase),
}}
>
{group.name}
</Text>
<Badge
count={groupSelectedCount}
size="small"
style={{
<Badge
count={groupSelectedCount}
size="small"
style={{
backgroundColor: getThemeAwareColor(token.colorPrimary, token.colorPrimaryActive),
color: getThemeAwareColor('#fff', token.colorTextLightSolid)
color: getThemeAwareColor('#fff', token.colorTextLightSolid),
}}
/>
</Space>
</div>
)}
{isExpanded && (
<div style={{ paddingLeft: groupBy !== 'none' ? '24px' : '0' }}>
{group.projects.map(project => (
<div
<div
key={project.id}
style={dropdownStyles.projectItem}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary);
e.currentTarget.style.borderColor = getThemeAwareColor(token.colorBorderSecondary, token.colorBorder);
onMouseEnter={e => {
e.currentTarget.style.backgroundColor = getThemeAwareColor(
token.colorFillAlter,
token.colorFillQuaternary
);
e.currentTarget.style.borderColor = getThemeAwareColor(
token.colorBorderSecondary,
token.colorBorder
);
}}
onMouseLeave={(e) => {
onMouseLeave={e => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.borderColor = 'transparent';
}}
@@ -338,17 +398,24 @@ const Projects: React.FC = () => {
onChange={e => handleCheckboxChange(project.id || '', e.target.checked)}
>
<Space>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: processColor((project as any).color_code, token.colorPrimary),
flexShrink: 0,
border: `1px solid ${getThemeAwareColor('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.2)')}`
}} />
<Text style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase)
}}>
<div
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: processColor(
(project as any).color_code,
token.colorPrimary
),
flexShrink: 0,
border: `1px solid ${getThemeAwareColor('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.2)')}`,
}}
/>
<Text
style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase),
}}
>
{project.name}
</Text>
</Space>
@@ -369,14 +436,16 @@ const Projects: React.FC = () => {
trigger={['click']}
open={dropdownVisible}
dropdownRender={() => (
<div style={{
...dropdownStyles.dropdown,
padding: '8px 0',
maxHeight: '500px',
width: '400px',
display: 'flex',
flexDirection: 'column'
}}>
<div
style={{
...dropdownStyles.dropdown,
padding: '8px 0',
maxHeight: '500px',
width: '400px',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Header with search and controls */}
<div style={{ padding: '8px 12px', flexShrink: 0 }}>
<Space direction="vertical" style={{ width: '100%' }} size="small">
@@ -385,28 +454,48 @@ const Projects: React.FC = () => {
placeholder={searchPlaceholder}
value={searchText}
onChange={e => setSearchText(e.target.value)}
prefix={<SearchOutlined style={{ color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary) }} />}
suffix={searchText && (
<Tooltip title={clearTooltip}>
<ClearOutlined
onClick={clearSearch}
style={{
cursor: 'pointer',
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
transition: 'color 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary);
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary);
}}
/>
</Tooltip>
)}
prefix={
<SearchOutlined
style={{
color: getThemeAwareColor(
token.colorTextTertiary,
token.colorTextQuaternary
),
}}
/>
}
suffix={
searchText && (
<Tooltip title={clearTooltip}>
<ClearOutlined
onClick={clearSearch}
style={{
cursor: 'pointer',
color: getThemeAwareColor(
token.colorTextTertiary,
token.colorTextQuaternary
),
transition: 'color 0.2s ease',
}}
onMouseEnter={e => {
e.currentTarget.style.color = getThemeAwareColor(
token.colorTextSecondary,
token.colorTextTertiary
);
}}
onMouseLeave={e => {
e.currentTarget.style.color = getThemeAwareColor(
token.colorTextTertiary,
token.colorTextQuaternary
);
}}
/>
</Tooltip>
)
}
onClick={e => e.stopPropagation()}
/>
{/* Controls row */}
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Space size="small">
@@ -417,25 +506,31 @@ const Projects: React.FC = () => {
style={{ width: '120px' }}
options={groupByOptions}
/>
{groupBy !== 'none' && (
<Space size="small">
<Button
type="text"
<Button
type="text"
size="small"
onClick={() => toggleAllGroups(true)}
style={{
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary)
style={{
color: getThemeAwareColor(
token.colorTextSecondary,
token.colorTextTertiary
),
}}
>
{expandAllText}
</Button>
<Button
type="text"
<Button
type="text"
size="small"
onClick={() => toggleAllGroups(false)}
style={{
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary)
style={{
color: getThemeAwareColor(
token.colorTextSecondary,
token.colorTextTertiary
),
}}
>
{collapseAllText}
@@ -443,16 +538,23 @@ const Projects: React.FC = () => {
</Space>
)}
</Space>
<Tooltip title={showSelectedTooltip}>
<Button
type={showSelectedOnly ? 'primary' : 'text'}
size="small"
icon={<FilterOutlined />}
onClick={() => setShowSelectedOnly(!showSelectedOnly)}
style={!showSelectedOnly ? {
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary)
} : {}}
style={
!showSelectedOnly
? {
color: getThemeAwareColor(
token.colorTextSecondary,
token.colorTextTertiary
),
}
: {}
}
/>
</Tooltip>
</Space>
@@ -468,18 +570,23 @@ const Projects: React.FC = () => {
indeterminate={indeterminate}
>
<Space>
<Text style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase)
}}>
<Text
style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase),
}}
>
{selectAllText}
</Text>
{selectedCount > 0 && (
<Badge
count={selectedCount}
<Badge
count={selectedCount}
size="small"
style={{
backgroundColor: getThemeAwareColor(token.colorSuccess, token.colorSuccessActive),
color: getThemeAwareColor('#fff', token.colorTextLightSolid)
style={{
backgroundColor: getThemeAwareColor(
token.colorSuccess,
token.colorSuccessActive
),
color: getThemeAwareColor('#fff', token.colorTextLightSolid),
}}
/>
)}
@@ -490,18 +597,25 @@ const Projects: React.FC = () => {
<Divider style={{ margin: '8px 0', flexShrink: 0 }} />
{/* Projects list */}
<div style={{
overflowY: 'auto',
flex: 1,
padding: '0 12px'
}}>
<div
style={{
overflowY: 'auto',
flex: 1,
padding: '0 12px',
}}
>
{filteredProjects.length === 0 ? (
<Empty
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<Text style={{
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary)
}}>
<Text
style={{
color: getThemeAwareColor(
token.colorTextTertiary,
token.colorTextQuaternary
),
}}
>
{searchText ? noProjectsText : noDataText}
</Text>
}
@@ -516,17 +630,25 @@ const Projects: React.FC = () => {
{selectedCount > 0 && (
<>
<Divider style={{ margin: '8px 0', flexShrink: 0 }} />
<div style={{
padding: '8px 12px',
flexShrink: 0,
backgroundColor: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
borderRadius: `0 0 ${token.borderRadius}px ${token.borderRadius}px`,
borderTop: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`
}}>
<Text type="secondary" style={{
fontSize: '12px',
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary)
}}>
<div
style={{
padding: '8px 12px',
flexShrink: 0,
backgroundColor: getThemeAwareColor(
token.colorFillAlter,
token.colorFillQuaternary
),
borderRadius: `0 0 ${token.borderRadius}px ${token.borderRadius}px`,
borderTop: `1px solid ${getThemeAwareColor(token.colorBorderSecondary, token.colorBorder)}`,
}}
>
<Text
type="secondary"
style={{
fontSize: '12px',
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary),
}}
>
{selectedCount} {projectsSelectedText}
</Text>
</div>
@@ -545,7 +667,7 @@ const Projects: React.FC = () => {
<Badge count={selectedCount} size="small" offset={[-5, 5]}>
<Button loading={loadingProjects}>
<Space>
{t('projects')}
{t('projects')}
<CaretDownFilled />
</Space>
</Button>

View File

@@ -8,7 +8,13 @@ import { reportingApiService } from '@/api/reporting/reporting.api.service';
import logger from '@/utils/errorLogger';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { fetchReportingCategories, fetchReportingProjects, fetchReportingTeams, setSelectOrDeselectAllTeams, setSelectOrDeselectTeam } from '@/features/reporting/time-reports/time-reports-overview.slice';
import {
fetchReportingCategories,
fetchReportingProjects,
fetchReportingTeams,
setSelectOrDeselectAllTeams,
setSelectOrDeselectTeam,
} from '@/features/reporting/time-reports/time-reports-overview.slice';
const Team: React.FC = () => {
const dispatch = useAppDispatch();
@@ -46,15 +52,17 @@ const Team: React.FC = () => {
placement="bottomLeft"
trigger={['click']}
dropdownRender={() => (
<div style={{
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadow,
padding: '4px 0',
maxHeight: '330px',
display: 'flex',
flexDirection: 'column'
}}>
<div
style={{
background: token.colorBgContainer,
borderRadius: token.borderRadius,
boxShadow: token.boxShadow,
padding: '4px 0',
maxHeight: '330px',
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ padding: '8px', flexShrink: 0 }}>
<Input
placeholder={t('searchByName')}
@@ -73,16 +81,18 @@ const Team: React.FC = () => {
</Checkbox>
</div>
<Divider style={{ margin: '4px 0', flexShrink: 0 }} />
<div style={{
overflowY: 'auto',
flex: 1
}}>
<div
style={{
overflowY: 'auto',
flex: 1,
}}
>
{filteredItems.map(item => (
<div
<div
key={item.id}
style={{
style={{
padding: '8px 12px',
cursor: 'pointer'
cursor: 'pointer',
}}
>
<Checkbox

View File

@@ -1,6 +1,8 @@
import { Card, Flex } from 'antd';
import TimeReportPageHeader from '@/pages/reporting/timeReports/page-header/time-report-page-header';
import ProjectTimeSheetChart, { ProjectTimeSheetChartRef } from '@/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart';
import ProjectTimeSheetChart, {
ProjectTimeSheetChartRef,
} from '@/pages/reporting/time-reports/project-time-sheet/project-time-sheet-chart';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
import TimeReportingRightHeader from './timeReportingRightHeader/TimeReportingRightHeader';
@@ -22,9 +24,7 @@ const ProjectsTimeReports = () => {
<Flex vertical>
<TimeReportingRightHeader
title={t('projectsTimeSheet')}
exportType={[
{ key: 'png', label: 'PNG' },
]}
exportType={[{ key: 'png', label: 'PNG' }]}
export={handleExport}
/>

View File

@@ -14,7 +14,11 @@ interface headerState {
export: (key: string) => void;
}
const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, export: exportFn }) => {
const TimeReportingRightHeader: React.FC<headerState> = ({
title,
exportType,
export: exportFn,
}) => {
const { t } = useTranslation('time-report');
const dispatch = useAppDispatch();
const { archived } = useAppSelector(state => state.timeReportsOverviewReducer);
@@ -22,7 +26,7 @@ const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, ex
const menuItems = exportType.map(item => ({
key: item.key,
label: item.label,
onClick: () => exportFn(item.key)
onClick: () => exportFn(item.key),
}));
return (
@@ -36,8 +40,8 @@ const TimeReportingRightHeader: React.FC<headerState> = ({ title, exportType, ex
</Checkbox>
</Button>
<TimeWiseFilter />
<Dropdown menu={{ items: menuItems }}>
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
<Dropdown menu={{ items: menuItems }}>
<Button type="primary" icon={<DownOutlined />} iconPosition="end">
{t('export')}
</Button>
</Dropdown>