group by client / category
This commit is contained in:
@@ -76,6 +76,8 @@ import groupByFilterDropdownReducer from '../features/group-by-filter-dropdown/g
|
|||||||
import homePageApiService from '@/api/home-page/home-page.api.service';
|
import homePageApiService from '@/api/home-page/home-page.api.service';
|
||||||
import { projectsApi } from '@/api/projects/projects.v1.api.service';
|
import { projectsApi } from '@/api/projects/projects.v1.api.service';
|
||||||
|
|
||||||
|
import projectViewReducer from '@features/project/project-view-slice';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
middleware: getDefaultMiddleware =>
|
middleware: getDefaultMiddleware =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
@@ -114,6 +116,8 @@ export const store = configureStore({
|
|||||||
boardReducer: boardReducer,
|
boardReducer: boardReducer,
|
||||||
projectDrawerReducer: projectDrawerReducer,
|
projectDrawerReducer: projectDrawerReducer,
|
||||||
|
|
||||||
|
projectViewReducer: projectViewReducer,
|
||||||
|
|
||||||
// Project Lookups
|
// Project Lookups
|
||||||
projectCategoriesReducer: projectCategoriesReducer,
|
projectCategoriesReducer: projectCategoriesReducer,
|
||||||
projectStatusesReducer: projectStatusesReducer,
|
projectStatusesReducer: projectStatusesReducer,
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ const ProjectGroupList: React.FC<ProjectGroupListProps> = ({
|
|||||||
)}
|
)}
|
||||||
<Title level={4} className="project-group-title">
|
<Title level={4} className="project-group-title">
|
||||||
{group.groupName}
|
{group.groupName}
|
||||||
<Text type="secondary" className="group-stats">
|
|
||||||
({group.count} projects • {group.averageProgress}% avg • {group.totalTasks} tasks)
|
|
||||||
</Text>
|
|
||||||
</Title>
|
</Title>
|
||||||
</div>
|
</div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
|
|||||||
47
worklenz-frontend/src/features/project/project-view-slice.ts
Normal file
47
worklenz-frontend/src/features/project/project-view-slice.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { ProjectGroupBy, ProjectViewType } from '@/types/project/project.types';
|
||||||
|
|
||||||
|
interface ProjectViewState {
|
||||||
|
mode: ProjectViewType;
|
||||||
|
groupBy: ProjectGroupBy;
|
||||||
|
lastUpdated?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCAL_STORAGE_KEY = 'project_view_preferences';
|
||||||
|
|
||||||
|
const loadInitialState = (): ProjectViewState => {
|
||||||
|
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
return saved
|
||||||
|
? JSON.parse(saved)
|
||||||
|
: {
|
||||||
|
mode: ProjectViewType.LIST,
|
||||||
|
groupBy: ProjectGroupBy.CATEGORY,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: ProjectViewState = loadInitialState();
|
||||||
|
|
||||||
|
export const projectViewSlice = createSlice({
|
||||||
|
name: 'projectView',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setViewMode: (state, action: PayloadAction<ProjectViewType>) => {
|
||||||
|
state.mode = action.payload;
|
||||||
|
state.lastUpdated = new Date().toISOString();
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
|
||||||
|
},
|
||||||
|
setGroupBy: (state, action: PayloadAction<ProjectGroupBy>) => {
|
||||||
|
state.groupBy = action.payload;
|
||||||
|
state.lastUpdated = new Date().toISOString();
|
||||||
|
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
|
||||||
|
},
|
||||||
|
resetViewState: () => {
|
||||||
|
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
||||||
|
return loadInitialState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setViewMode, setGroupBy, resetViewState } = projectViewSlice.actions;
|
||||||
|
export default projectViewSlice.reducer;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// Updated project-list.tsx
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ProjectViewType, ProjectGroupBy } from '@/types/project/project.types';
|
||||||
|
import { setViewMode, setGroupBy } from '@features/project/project-view-slice';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
Segmented,
|
Segmented,
|
||||||
|
Select,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Table,
|
Table,
|
||||||
TablePaginationConfig,
|
TablePaginationConfig,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Select,
|
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PageHeader } from '@ant-design/pro-components';
|
import { PageHeader } from '@ant-design/pro-components';
|
||||||
import {
|
import {
|
||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
PROJECT_SORT_FIELD,
|
PROJECT_SORT_FIELD,
|
||||||
PROJECT_SORT_ORDER,
|
PROJECT_SORT_ORDER,
|
||||||
} from '@/shared/constants';
|
} from '@/shared/constants';
|
||||||
import { IProjectFilter, ProjectGroupBy } from '@/types/project/project.types';
|
import { IProjectFilter } from '@/types/project/project.types';
|
||||||
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
|
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
|
||||||
|
|
||||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||||
@@ -64,32 +64,26 @@ import {
|
|||||||
} from '@/shared/worklenz-analytics-events';
|
} from '@/shared/worklenz-analytics-events';
|
||||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
import ProjectGroupList from '@/components/project-list/project-group/project-group-list';
|
import ProjectGroupList from '@/components/project-list/project-group/project-group-list';
|
||||||
import { groupProjectsByCategory, groupProjectsByClient } from '@/utils/project-group';
|
import { groupProjects } from '@/utils/project-group';
|
||||||
|
|
||||||
const ProjectList: React.FC = () => {
|
const ProjectList: React.FC = () => {
|
||||||
// All hooks must be called at the top level, in the same order every time
|
const [filteredInfo, setFilteredInfo] = useState<Record<string, FilterValue | null>>({});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation('all-project-list');
|
const { t } = useTranslation('all-project-list');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
useDocumentTitle('Projects');
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
|
|
||||||
// State hooks
|
// Get view state from Redux
|
||||||
const [filteredInfo, setFilteredInfo] = useState<Record<string, FilterValue | null>>({});
|
const { mode: viewMode, groupBy } = useAppSelector((state) => state.projectViewReducer);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [viewMode, setViewMode] = useState<'list' | 'group'>('list');
|
|
||||||
const [groupBy, setGroupBy] = useState<ProjectGroupBy>(ProjectGroupBy.CATEGORY);
|
|
||||||
|
|
||||||
// Custom hooks
|
|
||||||
useDocumentTitle('Projects');
|
|
||||||
|
|
||||||
// Selector hooks
|
|
||||||
const { requestParams } = useAppSelector(state => state.projectsReducer);
|
const { requestParams } = useAppSelector(state => state.projectsReducer);
|
||||||
const { projectStatuses } = useAppSelector(state => state.projectStatusesReducer);
|
const { projectStatuses } = useAppSelector(state => state.projectStatusesReducer);
|
||||||
const { projectHealths } = useAppSelector(state => state.projectHealthReducer);
|
const { projectHealths } = useAppSelector(state => state.projectHealthReducer);
|
||||||
const { projectCategories } = useAppSelector(state => state.projectCategoriesReducer);
|
const { projectCategories } = useAppSelector(state => state.projectCategoriesReducer);
|
||||||
|
|
||||||
// Query hooks
|
|
||||||
const {
|
const {
|
||||||
data: projectsData,
|
data: projectsData,
|
||||||
isLoading: loadingProjects,
|
isLoading: loadingProjects,
|
||||||
@@ -97,7 +91,6 @@ const ProjectList: React.FC = () => {
|
|||||||
refetch: refetchProjects,
|
refetch: refetchProjects,
|
||||||
} = useGetProjectsQuery(requestParams);
|
} = useGetProjectsQuery(requestParams);
|
||||||
|
|
||||||
// Callback hooks
|
|
||||||
const getFilterIndex = useCallback(() => {
|
const getFilterIndex = useCallback(() => {
|
||||||
return +(localStorage.getItem(FILTER_INDEX_KEY) || 0);
|
return +(localStorage.getItem(FILTER_INDEX_KEY) || 0);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -111,10 +104,8 @@ const ProjectList: React.FC = () => {
|
|||||||
localStorage.setItem(PROJECT_SORT_ORDER, order);
|
localStorage.setItem(PROJECT_SORT_ORDER, order);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoized values
|
|
||||||
const filters = useMemo(() => Object.values(IProjectFilter), []);
|
const filters = useMemo(() => Object.values(IProjectFilter), []);
|
||||||
|
|
||||||
// Create translated segment options for the filters
|
|
||||||
const segmentOptions = useMemo(() => {
|
const segmentOptions = useMemo(() => {
|
||||||
return filters.map(filter => ({
|
return filters.map(filter => ({
|
||||||
value: filter,
|
value: filter,
|
||||||
@@ -122,55 +113,48 @@ const ProjectList: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
}, [filters, t]);
|
}, [filters, t]);
|
||||||
|
|
||||||
// Toggle options for List/Group view
|
|
||||||
const viewToggleOptions = useMemo(
|
const viewToggleOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: 'list' as const,
|
value: ProjectViewType.LIST,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<Tooltip title={t('listView')}>
|
||||||
<UnorderedListOutlined />
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
List
|
<UnorderedListOutlined />
|
||||||
</div>
|
<span>{t('list')}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'group' as const,
|
value: ProjectViewType.GROUP,
|
||||||
label: (
|
label: (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<Tooltip title={t('groupView')}>
|
||||||
<AppstoreOutlined />
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
Group
|
<AppstoreOutlined />
|
||||||
</div>
|
<span>{t('group')}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group by options
|
|
||||||
const groupByOptions = useMemo(
|
const groupByOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: ProjectGroupBy.CATEGORY, label: 'Category' },
|
{
|
||||||
{ value: ProjectGroupBy.CLIENT, label: 'Client' },
|
value: ProjectGroupBy.CATEGORY,
|
||||||
|
label: t('groupBy.category'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ProjectGroupBy.CLIENT,
|
||||||
|
label: t('groupBy.client'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[]
|
[t]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get grouped projects based on current groupBy selection
|
|
||||||
const groupedProjects = useMemo(() => {
|
|
||||||
const projects = projectsData?.body?.data || [];
|
|
||||||
if (viewMode !== 'group') return [];
|
|
||||||
|
|
||||||
switch (groupBy) {
|
|
||||||
case ProjectGroupBy.CATEGORY:
|
|
||||||
return groupProjectsByCategory(projects);
|
|
||||||
case ProjectGroupBy.CLIENT:
|
|
||||||
return groupProjectsByClient(projects);
|
|
||||||
default:
|
|
||||||
return groupProjectsByCategory(projects);
|
|
||||||
}
|
|
||||||
}, [projectsData?.body?.data, viewMode, groupBy]);
|
|
||||||
|
|
||||||
const paginationConfig = useMemo(
|
const paginationConfig = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
current: requestParams.index,
|
current: requestParams.index,
|
||||||
@@ -184,28 +168,6 @@ const ProjectList: React.FC = () => {
|
|||||||
[requestParams.index, requestParams.size, projectsData?.body?.total]
|
[requestParams.index, requestParams.size, projectsData?.body?.total]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Effect hooks
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(loadingProjects || isFetchingProjects);
|
|
||||||
}, [loadingProjects, isFetchingProjects]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const filterIndex = getFilterIndex();
|
|
||||||
dispatch(setRequestParams({ filter: filterIndex }));
|
|
||||||
}, [dispatch, getFilterIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
trackMixpanelEvent(evt_projects_page_visit);
|
|
||||||
refetchProjects();
|
|
||||||
}, [requestParams, refetchProjects, trackMixpanelEvent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectStatuses.length === 0) dispatch(fetchProjectStatuses());
|
|
||||||
if (projectCategories.length === 0) dispatch(fetchProjectCategories());
|
|
||||||
if (projectHealths.length === 0) dispatch(fetchProjectHealth());
|
|
||||||
}, [dispatch, projectStatuses.length, projectCategories.length, projectHealths.length]);
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
const handleTableChange = useCallback(
|
const handleTableChange = useCallback(
|
||||||
(
|
(
|
||||||
newPagination: TablePaginationConfig,
|
newPagination: TablePaginationConfig,
|
||||||
@@ -242,13 +204,13 @@ const ProjectList: React.FC = () => {
|
|||||||
dispatch(setRequestParams(newParams));
|
dispatch(setRequestParams(newParams));
|
||||||
setFilteredInfo(filters);
|
setFilteredInfo(filters);
|
||||||
},
|
},
|
||||||
[dispatch, setSortingValues, requestParams]
|
[dispatch, setSortingValues]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
trackMixpanelEvent(evt_projects_refresh_click);
|
trackMixpanelEvent(evt_projects_refresh_click);
|
||||||
refetchProjects();
|
refetchProjects();
|
||||||
}, [refetchProjects, trackMixpanelEvent]);
|
}, [trackMixpanelEvent, refetchProjects]);
|
||||||
|
|
||||||
const handleSegmentChange = useCallback(
|
const handleSegmentChange = useCallback(
|
||||||
(value: IProjectFilter) => {
|
(value: IProjectFilter) => {
|
||||||
@@ -264,15 +226,15 @@ const ProjectList: React.FC = () => {
|
|||||||
trackMixpanelEvent(evt_projects_search);
|
trackMixpanelEvent(evt_projects_search);
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
dispatch(setRequestParams({ search: value }));
|
dispatch(setRequestParams({ search: value }));
|
||||||
}, [dispatch, trackMixpanelEvent]);
|
}, [trackMixpanelEvent, dispatch]);
|
||||||
|
|
||||||
const handleViewToggle = useCallback((value: 'list' | 'group') => {
|
const handleViewToggle = useCallback((value: ProjectViewType) => {
|
||||||
setViewMode(value);
|
dispatch(setViewMode(value));
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleGroupByChange = useCallback((value: ProjectGroupBy) => {
|
const handleGroupByChange = useCallback((value: ProjectGroupBy) => {
|
||||||
setGroupBy(value);
|
dispatch(setGroupBy(value));
|
||||||
}, []);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleDrawerClose = useCallback(() => {
|
const handleDrawerClose = useCallback(() => {
|
||||||
dispatch(setProject({} as IProjectViewModel));
|
dispatch(setProject({} as IProjectViewModel));
|
||||||
@@ -287,6 +249,26 @@ const ProjectList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(loadingProjects || isFetchingProjects);
|
||||||
|
}, [loadingProjects, isFetchingProjects]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filterIndex = getFilterIndex();
|
||||||
|
dispatch(setRequestParams({ filter: filterIndex }));
|
||||||
|
}, [dispatch, getFilterIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackMixpanelEvent(evt_projects_page_visit);
|
||||||
|
refetchProjects();
|
||||||
|
}, [requestParams, refetchProjects, trackMixpanelEvent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectStatuses.length === 0) dispatch(fetchProjectStatuses());
|
||||||
|
if (projectCategories.length === 0) dispatch(fetchProjectCategories());
|
||||||
|
if (projectHealths.length === 0) dispatch(fetchProjectHealth());
|
||||||
|
}, [dispatch, projectStatuses.length, projectCategories.length, projectHealths.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBlock: 65, minHeight: '90vh' }}>
|
<div style={{ marginBlock: 65, minHeight: '90vh' }}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -317,13 +299,12 @@ const ProjectList: React.FC = () => {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{viewMode === 'group' && (
|
{viewMode === ProjectViewType.GROUP && (
|
||||||
<Select
|
<Select
|
||||||
value={groupBy}
|
value={groupBy}
|
||||||
onChange={handleGroupByChange}
|
onChange={handleGroupByChange}
|
||||||
options={groupByOptions}
|
options={groupByOptions}
|
||||||
style={{ width: 120 }}
|
style={{ width: 150 }}
|
||||||
size="middle"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
@@ -340,7 +321,7 @@ const ProjectList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Card className="project-card">
|
<Card className="project-card">
|
||||||
<Skeleton active loading={isLoading} className="mt-4 p-4">
|
<Skeleton active loading={isLoading} className="mt-4 p-4">
|
||||||
{viewMode === 'list' ? (
|
{viewMode === ProjectViewType.LIST ? (
|
||||||
<Table<IProjectViewModel>
|
<Table<IProjectViewModel>
|
||||||
columns={TableColumns({
|
columns={TableColumns({
|
||||||
navigate,
|
navigate,
|
||||||
@@ -352,14 +333,14 @@ const ProjectList: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={paginationConfig}
|
pagination={paginationConfig}
|
||||||
locale={{ emptyText: <Empty description={t('noProjects')} /> }}
|
locale={{ emptyText: <Empty description={t('noProjects')} />}}
|
||||||
onRow={record => ({
|
onRow={record => ({
|
||||||
onClick: () => navigateToProject(record.id, record.team_member_default_view),
|
onClick: () => navigateToProject(record.id, record.team_member_default_view),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProjectGroupList
|
<ProjectGroupList
|
||||||
groups={groupedProjects}
|
groups={groupProjects(projectsData?.body?.data || [], groupBy)}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
onProjectSelect={id => navigateToProject(id, undefined)}
|
onProjectSelect={id => navigateToProject(id, undefined)}
|
||||||
onArchive={() => {}}
|
onArchive={() => {}}
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ export interface ProjectListTableProps {
|
|||||||
onArchive: (id: string) => void;
|
onArchive: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New types for grouping functionality
|
|
||||||
export enum ProjectViewType {
|
export enum ProjectViewType {
|
||||||
LIST = 'list',
|
LIST = 'list',
|
||||||
GROUP = 'group'
|
GROUP = 'group'
|
||||||
@@ -108,18 +107,10 @@ export interface GroupedProject {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectFilterConfig{
|
|
||||||
current_tab: string | null;
|
|
||||||
projects_group_by: number;
|
|
||||||
current_view: number;
|
|
||||||
is_group_view: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectViewControlsProps {
|
export interface ProjectViewControlsProps {
|
||||||
viewType: ProjectViewType;
|
viewState: ProjectViewState;
|
||||||
groupBy: ProjectGroupBy;
|
onViewChange: (state: ProjectViewState) => void;
|
||||||
onViewTypeChange: (type: ProjectViewType) => void;
|
availableGroupByOptions?: ProjectGroupBy[];
|
||||||
onGroupByChange: (groupBy: ProjectGroupBy) => void;
|
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,3 +143,9 @@ export interface GroupedProject {
|
|||||||
totalTasks: number;
|
totalTasks: number;
|
||||||
averageProgress?: number;
|
averageProgress?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectViewState {
|
||||||
|
mode: ProjectViewType;
|
||||||
|
groupBy: ProjectGroupBy;
|
||||||
|
lastUpdated?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,4 +7,8 @@ export interface IProjectFilterConfig {
|
|||||||
filter: string | null;
|
filter: string | null;
|
||||||
categories: string | null;
|
categories: string | null;
|
||||||
statuses: string | null;
|
statuses: string | null;
|
||||||
|
current_tab: string | null;
|
||||||
|
projects_group_by: number;
|
||||||
|
current_view: number;
|
||||||
|
is_group_view: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
// Updated project-group.ts
|
import { GroupedProject, ProjectGroupBy } from "@/types/project/project.types";
|
||||||
import { GroupedProject } from "@/types/project/project.types";
|
|
||||||
import { IProjectViewModel } from "@/types/project/projectViewModel.types";
|
import { IProjectViewModel } from "@/types/project/projectViewModel.types";
|
||||||
|
|
||||||
export const groupProjectsByCategory = (projects: IProjectViewModel[]): GroupedProject[] => {
|
export const groupProjects = (
|
||||||
|
projects: IProjectViewModel[],
|
||||||
|
groupBy: ProjectGroupBy
|
||||||
|
): GroupedProject[] => {
|
||||||
const grouped: Record<string, GroupedProject> = {};
|
const grouped: Record<string, GroupedProject> = {};
|
||||||
|
|
||||||
projects?.forEach(project => {
|
projects?.forEach(project => {
|
||||||
const categoryName = project.category_name || 'Uncategorized';
|
let groupKey: string;
|
||||||
const categoryColor = project.category_color || '#888';
|
let groupName: string;
|
||||||
|
let groupColor: string;
|
||||||
|
|
||||||
if (!grouped[categoryName]) {
|
switch (groupBy) {
|
||||||
grouped[categoryName] = {
|
case ProjectGroupBy.CLIENT:
|
||||||
groupKey: categoryName,
|
groupKey = project.client_name || 'No Client';
|
||||||
groupName: categoryName,
|
groupName = groupKey;
|
||||||
groupColor: categoryColor,
|
groupColor = '#688';
|
||||||
|
break;
|
||||||
|
case ProjectGroupBy.CATEGORY:
|
||||||
|
default:
|
||||||
|
groupKey = project.category_name || 'Uncategorized';
|
||||||
|
groupName = groupKey;
|
||||||
|
groupColor = project.category_color || '#888';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!grouped[groupKey]) {
|
||||||
|
grouped[groupKey] = {
|
||||||
|
groupKey,
|
||||||
|
groupName,
|
||||||
|
groupColor,
|
||||||
projects: [],
|
projects: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
totalProgress: 0,
|
totalProgress: 0,
|
||||||
@@ -21,48 +37,10 @@ export const groupProjectsByCategory = (projects: IProjectViewModel[]): GroupedP
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
grouped[categoryName].projects.push(project);
|
grouped[groupKey].projects.push(project);
|
||||||
grouped[categoryName].count++;
|
grouped[groupKey].count++;
|
||||||
grouped[categoryName].totalProgress += project.progress || 0;
|
grouped[groupKey].totalProgress += project.progress || 0;
|
||||||
grouped[categoryName].totalTasks += project.task_count || 0;
|
grouped[groupKey].totalTasks += project.task_count || 0;
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate average progress for each category
|
|
||||||
Object.values(grouped).forEach(group => {
|
|
||||||
group.averageProgress = group.count > 0 ? Math.round(group.totalProgress / group.count) : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.values(grouped);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const groupProjectsByClient = (projects: IProjectViewModel[]): GroupedProject[] => {
|
|
||||||
const grouped: Record<string, GroupedProject> = {};
|
|
||||||
|
|
||||||
projects?.forEach(project => {
|
|
||||||
const clientName = project.client_name || 'No Client';
|
|
||||||
const clientKey = project.client_id || 'no-client';
|
|
||||||
|
|
||||||
if (!grouped[clientKey]) {
|
|
||||||
grouped[clientKey] = {
|
|
||||||
groupKey: clientKey,
|
|
||||||
groupName: clientName,
|
|
||||||
groupColor: '#4A90E2', // Default blue color for clients
|
|
||||||
projects: [],
|
|
||||||
count: 0,
|
|
||||||
totalProgress: 0,
|
|
||||||
totalTasks: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
grouped[clientKey].projects.push(project);
|
|
||||||
grouped[clientKey].count++;
|
|
||||||
grouped[clientKey].totalProgress += project.progress || 0;
|
|
||||||
grouped[clientKey].totalTasks += project.task_count || 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate average progress for each client
|
|
||||||
Object.values(grouped).forEach(group => {
|
|
||||||
group.averageProgress = group.count > 0 ? Math.round(group.totalProgress / group.count) : 0;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.values(grouped);
|
return Object.values(grouped);
|
||||||
|
|||||||
Reference in New Issue
Block a user