diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts index 4db8e3d5..32513b1e 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts @@ -94,7 +94,7 @@ export default class ReportingAllocationController extends ReportingControllerBa SELECT name, (SELECT COALESCE(SUM(time_spent), 0) FROM task_work_log - LEFT JOIN tasks ON task_work_log.task_id = tasks.id + LEFT JOIN tasks ON task_work_log.task_id = tasks.id WHERE user_id = users.id ${billableQuery} AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END AND tasks.project_id = projects.id @@ -473,17 +473,24 @@ export default class ReportingAllocationController extends ReportingControllerBa : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `; const billableQuery = this.buildBillableQuery(billable); - + const members = (req.body.members || []) as string[]; + // Prepare members filter + let membersFilter = ""; + if (members.length > 0) { + const memberIds = members.map(id => `'${id}'`).join(","); + membersFilter = `AND tmiv.team_member_id IN (${memberIds})`; + } const q = ` - SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time - FROM team_member_info_view tmiv - LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id - LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery} - LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id - WHERE p.id IN (${projectIds}) - ${durationClause} ${archivedClause} - GROUP BY tmiv.email, tmiv.name - ORDER BY logged_time DESC;`; + SELECT tmiv.team_member_id, tmiv.email, tmiv.name, SUM(time_spent) AS logged_time + FROM team_member_info_view tmiv + LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id + LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery} + LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id + WHERE p.id IN (${projectIds}) + ${durationClause} ${archivedClause} + ${membersFilter} + GROUP BY tmiv.email, tmiv.name, tmiv.team_member_id + ORDER BY logged_time DESC;`; const result = await db.query(q, []); for (const member of result.rows) { diff --git a/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts b/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts index 1529d46b..0c74203e 100644 --- a/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts +++ b/worklenz-frontend/src/api/reporting/reporting.timesheet.api.service.ts @@ -27,7 +27,9 @@ export const reportingTimesheetApiService = { getMemberTimeSheets: async (body = {}, archived = false): Promise> => { const q = toQueryString({ archived }); + console.log('getMemberTimeSheets body:', body); const response = await apiClient.post(`${rootUrl}/time-reports/members/${q}`, body); + console.log('getMemberTimeSheets response:', response); return response.data; }, diff --git a/worklenz-frontend/src/features/reporting/time-reports/time-reports-overview.slice.ts b/worklenz-frontend/src/features/reporting/time-reports/time-reports-overview.slice.ts index 9518495b..9de106e6 100644 --- a/worklenz-frontend/src/features/reporting/time-reports/time-reports-overview.slice.ts +++ b/worklenz-frontend/src/features/reporting/time-reports/time-reports-overview.slice.ts @@ -23,6 +23,9 @@ interface ITimeReportsOverviewState { billable: boolean; nonBillable: boolean; }; + + members: any[]; + loadingMembers: boolean; } const initialState: ITimeReportsOverviewState = { @@ -42,6 +45,12 @@ const initialState: ITimeReportsOverviewState = { billable: true, nonBillable: true, }, + members: [], + loadingMembers: false, +}; + +const selectedMembers = (state: ITimeReportsOverviewState) => { + return state.members.filter(member => member.selected).map(member => member.id) as string[]; }; const selectedTeams = (state: ITimeReportsOverviewState) => { @@ -54,6 +63,26 @@ const selectedCategories = (state: ITimeReportsOverviewState) => { .map(category => category.id) as string[]; }; +export const fetchReportingMembers = createAsyncThunk( + 'timeReportsOverview/fetchReportingMembers', + async (_, { rejectWithValue, getState }) => { + const state = getState() as { timeReportsOverviewReducer: ITimeReportsOverviewState }; + const { timeReportsOverviewReducer } = state; + + try { + const res = await reportingApiService.getMembers(selectedMembers(timeReportsOverviewReducer)); + if (res.done) { + // Extract members from the response + return res.body.members; // Use `body.members` instead of `body` + } else { + return rejectWithValue(res.message || 'Failed to fetch members'); + } + } catch (error) { + return rejectWithValue(error.message || 'An error occurred while fetching members'); + } + } +); + export const fetchReportingTeams = createAsyncThunk( 'timeReportsOverview/fetchReportingTeams', async () => { @@ -123,6 +152,7 @@ const timeReportsOverviewSlice = createSlice({ setSelectOrDeselectProject: (state, action) => { const project = state.projects.find(project => project.id === action.payload.id); if (project) { + console.log('setSelectOrDeselectProject', project, action.payload); project.selected = action.payload.selected; } }, @@ -141,6 +171,17 @@ const timeReportsOverviewSlice = createSlice({ setArchived: (state, action: PayloadAction) => { state.archived = action.payload; }, + setSelectOrDeselectMember: (state, action: PayloadAction<{ id: string; selected: boolean }>) => { + const member = state.members.find(member => member.id === action.payload.id); + if (member) { + member.selected = action.payload.selected; + } + }, + setSelectOrDeselectAllMembers: (state, action: PayloadAction) => { + state.members.forEach(member => { + member.selected = action.payload; + }); + }, }, extraReducers: builder => { builder.addCase(fetchReportingTeams.fulfilled, (state, action) => { @@ -185,6 +226,29 @@ const timeReportsOverviewSlice = createSlice({ builder.addCase(fetchReportingProjects.rejected, state => { state.loadingProjects = false; }); + builder.addCase(fetchReportingMembers.fulfilled, (state, action) => { + console.log('fetchReportingMembers fulfilled', action.payload); + const members = action.payload.map((member: any) => ({ + id: member.id, + name: member.name, + selected: true, // Default to selected + avatar_url: member.avatar_url, // Include avatar URL if needed + email: member.email, // Include email if needed + })); + state.members = members; + state.loadingMembers = false; + }); + + builder.addCase(fetchReportingMembers.pending, state => { + console.log('fetchReportingMembers pending'); + state.loadingMembers = true; + }); + + builder.addCase(fetchReportingMembers.rejected, (state, action) => { + console.log('fetchReportingMembers rejected', action.payload); + state.loadingMembers = false; + console.error('Error fetching members:', action.payload); + }); }, }); @@ -197,6 +261,8 @@ export const { setSelectOrDeselectProject, setSelectOrDeselectAllProjects, setSelectOrDeselectBillable, + setSelectOrDeselectMember, + setSelectOrDeselectAllMembers, setNoCategory, setArchived, } = timeReportsOverviewSlice.actions; diff --git a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx index 4caa7f49..310f8880 100644 --- a/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx +++ b/worklenz-frontend/src/pages/reporting/time-reports/members-time-sheet/members-time-sheet.tsx @@ -35,6 +35,8 @@ const MembersTimeSheet = forwardRef((_, ref) => { loadingCategories, projects: filterProjects, loadingProjects, + members, + loadingMembers, billable, archived, } = useAppSelector(state => state.timeReportsOverviewReducer); @@ -170,11 +172,13 @@ const MembersTimeSheet = forwardRef((_, ref) => { const selectedTeams = teams.filter(team => team.selected); const selectedProjects = filterProjects.filter(project => project.selected); const selectedCategories = categories.filter(category => category.selected); + const selectedMembers = members.filter(member => member.selected); // Use selected members const body = { teams: selectedTeams.map(t => t.id), projects: selectedProjects.map(project => project.id), categories: selectedCategories.map(category => category.id), + members: selectedMembers.map(member => member.id), // Include members in the request duration, date_range: dateRange, billable, @@ -185,6 +189,7 @@ const MembersTimeSheet = forwardRef((_, ref) => { setJsonData(res.body || []); } } catch (error) { + console.error('Error fetching chart data:', error); logger.error('Error fetching chart data:', error); } finally { setLoading(false); @@ -193,7 +198,7 @@ const MembersTimeSheet = forwardRef((_, ref) => { useEffect(() => { fetchChartData(); - }, [dispatch, duration, dateRange, billable, archived, teams, filterProjects, categories]); + }, [dispatch, duration, dateRange, billable, archived, teams, filterProjects, categories, members]); const exportChart = () => { if (chartRef.current) { diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx new file mode 100644 index 00000000..fd9a03e4 --- /dev/null +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/members.tsx @@ -0,0 +1,114 @@ +import React, { useState } from 'react'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { setSelectOrDeselectAllMembers, setSelectOrDeselectMember } from '@/features/reporting/time-reports/time-reports-overview.slice'; +import { Button, Checkbox, Divider, Dropdown, Input, Avatar, theme } from 'antd'; +import { CheckboxChangeEvent } from 'antd/es/checkbox'; +import { CaretDownFilled } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +const Members: React.FC = () => { + const dispatch = useAppDispatch(); + const { t } = useTranslation('time-report'); + const { members, loadingMembers } = useAppSelector(state => state.timeReportsOverviewReducer); + const { token } = theme.useToken(); + + const [searchText, setSearchText] = useState(''); + const [selectAll, setSelectAll] = useState(true); + + // Filter members based on search text + const filteredMembers = members.filter(member => + member.name?.toLowerCase().includes(searchText.toLowerCase()) + ); + + // Handle checkbox change for individual members + const handleCheckboxChange = (id: string, checked: boolean) => { + console.log('Select Change:', id); + dispatch(setSelectOrDeselectMember({ id, selected: checked })); + }; + + // Handle "Select All" checkbox change + const handleSelectAllChange = (e: CheckboxChangeEvent) => { + console.log('Select All Change:', e); + const isChecked = e.target.checked; + setSelectAll(isChecked); + dispatch(setSelectOrDeselectAllMembers(isChecked)); + }; + + return ( + ( +
+
+ e.stopPropagation()} + placeholder={t('searchByMember')} + value={searchText} + onChange={e => setSearchText(e.target.value)} + /> +
+
+ e.stopPropagation()} + onChange={handleSelectAllChange} + checked={selectAll} + > + {t('selectAll')} + +
+ +
+ {filteredMembers.map(member => ( +
+ + e.stopPropagation()} + checked={member.selected} + onChange={e => handleCheckboxChange(member.id, e.target.checked)} + > + {member.name} + +
+ ))} +
+
+ )} + > + +
+ ); +}; + +export default Members; \ No newline at end of file diff --git a/worklenz-frontend/src/pages/reporting/timeReports/page-header/time-report-page-header.tsx b/worklenz-frontend/src/pages/reporting/timeReports/page-header/time-report-page-header.tsx index 20c3e152..5b4a4b7c 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/page-header/time-report-page-header.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/page-header/time-report-page-header.tsx @@ -8,7 +8,9 @@ import { fetchReportingTeams, fetchReportingProjects, fetchReportingCategories, + fetchReportingMembers, } from '@/features/reporting/time-reports/time-reports-overview.slice'; +import Members from './members'; const TimeReportPageHeader: React.FC = () => { const dispatch = useAppDispatch(); @@ -18,6 +20,7 @@ const TimeReportPageHeader: React.FC = () => { await dispatch(fetchReportingTeams()); await dispatch(fetchReportingCategories()); await dispatch(fetchReportingProjects()); + await dispatch(fetchReportingMembers()); }; fetchData(); @@ -29,6 +32,7 @@ const TimeReportPageHeader: React.FC = () => { + ); };