feat(projects): implement grouped project retrieval and UI enhancements

- Added a new endpoint for retrieving projects grouped by category, client, or status.
- Enhanced the ProjectsController with a method to handle grouped project queries.
- Updated the projects API router to include the new grouped endpoint.
- Improved the frontend to support displaying grouped projects with pagination and filtering options.
- Updated localization files for English, Spanish, and Portuguese to include new grouping options.
- Refactored project list components to accommodate the new grouped view and improved UI elements.
This commit is contained in:
chamikaJ
2025-06-13 15:46:03 +05:30
parent 81f55adb41
commit 3cae2771de
12 changed files with 1284 additions and 323 deletions

View File

@@ -168,6 +168,61 @@ const Projects: React.FC = () => {
[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]);
// 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]);
// Memoize search placeholder and clear tooltip
const searchPlaceholder = useMemo(() => t('searchByProject'), [t]);
const clearTooltip = useMemo(() => t('clearSearch'), [t]);
const showSelectedTooltip = useMemo(() => t('showSelected'), [t]);
const selectAllText = useMemo(() => t('selectAll'), [t]);
const projectsSelectedText = useMemo(() => t('projectsSelected'), [t]);
const noProjectsText = useMemo(() => t('noProjects'), [t]);
const noDataText = useMemo(() => t('noData'), [t]);
const expandAllText = useMemo(() => t('expandAll'), [t]);
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 }));
@@ -202,54 +257,7 @@ const Projects: React.FC = () => {
}
}, [groupedProjects]);
// Get theme-aware styles
const getThemeStyles = useCallback(() => {
const isDark = themeMode === 'dark';
return {
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)}`,
'&:hover': {
backgroundColor: getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary),
borderColor: getThemeAwareColor(token.colorBorder, token.colorBorderSecondary),
}
},
projectItem: {
padding: '8px 12px',
borderRadius: token.borderRadiusSM,
transition: 'all 0.2s ease',
cursor: 'pointer',
border: `1px solid transparent`,
'&:hover': {
backgroundColor: getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary),
borderColor: getThemeAwareColor(token.colorBorderSecondary, token.colorBorder),
}
},
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, themeMode, getThemeAwareColor]);
const styles = getThemeStyles();
// Render project group
const renderProjectGroup = (group: ProjectGroup) => {
@@ -261,10 +269,10 @@ const Projects: React.FC = () => {
{groupBy !== 'none' && (
<div
style={{
...styles.groupHeader,
...dropdownStyles.groupHeader,
backgroundColor: isExpanded
? getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary)
: styles.groupHeader.backgroundColor
: dropdownStyles.groupHeader.backgroundColor
}}
onClick={() => toggleGroupExpansion(group.key)}
onMouseEnter={(e) => {
@@ -274,15 +282,15 @@ const Projects: React.FC = () => {
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isExpanded
? getThemeAwareColor(token.colorFillSecondary, token.colorFillTertiary)
: styles.groupHeader.backgroundColor;
: dropdownStyles.groupHeader.backgroundColor;
e.currentTarget.style.borderColor = getThemeAwareColor(token.colorBorderSecondary, token.colorBorder);
}}
>
<Space>
{isExpanded ? (
<DownOutlined style={styles.expandedToggleIcon} />
<DownOutlined style={dropdownStyles.expandedToggleIcon} />
) : (
<RightOutlined style={styles.toggleIcon} />
<RightOutlined style={dropdownStyles.toggleIcon} />
)}
<div style={{
width: '12px',
@@ -314,7 +322,7 @@ const Projects: React.FC = () => {
{group.projects.map(project => (
<div
key={project.id}
style={styles.projectItem}
style={dropdownStyles.projectItem}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = getThemeAwareColor(token.colorFillAlter, token.colorFillQuaternary);
e.currentTarget.style.borderColor = getThemeAwareColor(token.colorBorderSecondary, token.colorBorder);
@@ -362,7 +370,7 @@ const Projects: React.FC = () => {
open={dropdownVisible}
dropdownRender={() => (
<div style={{
...styles.dropdown,
...dropdownStyles.dropdown,
padding: '8px 0',
maxHeight: '500px',
width: '400px',
@@ -374,12 +382,12 @@ const Projects: React.FC = () => {
<Space direction="vertical" style={{ width: '100%' }} size="small">
{/* Search input */}
<Input
placeholder={t('searchByProject')}
placeholder={searchPlaceholder}
value={searchText}
onChange={e => setSearchText(e.target.value)}
prefix={<SearchOutlined style={{ color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary) }} />}
suffix={searchText && (
<Tooltip title={t('clearSearch')}>
<Tooltip title={clearTooltip}>
<ClearOutlined
onClick={clearSearch}
style={{
@@ -407,12 +415,7 @@ const Projects: React.FC = () => {
onChange={setGroupBy}
size="small"
style={{ width: '120px' }}
options={[
{ value: 'none', label: t('groupByNone') },
{ value: 'category', label: t('groupByCategory') },
{ value: 'team', label: t('groupByTeam') },
{ value: 'status', label: t('groupByStatus') },
]}
options={groupByOptions}
/>
{groupBy !== 'none' && (
@@ -425,7 +428,7 @@ const Projects: React.FC = () => {
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary)
}}
>
{t('expandAll')}
{expandAllText}
</Button>
<Button
type="text"
@@ -435,13 +438,13 @@ const Projects: React.FC = () => {
color: getThemeAwareColor(token.colorTextSecondary, token.colorTextTertiary)
}}
>
{t('collapseAll')}
{collapseAllText}
</Button>
</Space>
)}
</Space>
<Tooltip title={t('showSelected')}>
<Tooltip title={showSelectedTooltip}>
<Button
type={showSelectedOnly ? 'primary' : 'text'}
size="small"
@@ -468,7 +471,7 @@ const Projects: React.FC = () => {
<Text style={{
color: getThemeAwareColor(token.colorText, token.colorTextBase)
}}>
{t('selectAll')}
{selectAllText}
</Text>
{selectedCount > 0 && (
<Badge
@@ -499,7 +502,7 @@ const Projects: React.FC = () => {
<Text style={{
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary)
}}>
{searchText ? t('noProjects') : t('noData')}
{searchText ? noProjectsText : noDataText}
</Text>
}
style={{ margin: '20px 0' }}
@@ -524,7 +527,7 @@ const Projects: React.FC = () => {
fontSize: '12px',
color: getThemeAwareColor(token.colorTextTertiary, token.colorTextQuaternary)
}}>
{selectedCount} {t('projectsSelected')}
{selectedCount} {projectsSelectedText}
</Text>
</div>
</>