feat(members-time-sheet): enhance API response handling and add noCategory filter
This commit is contained in:
@@ -77,8 +77,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
sps.icon AS status_icon,
|
sps.icon AS status_icon,
|
||||||
(SELECT COUNT(*)
|
(SELECT COUNT(*)
|
||||||
FROM tasks
|
FROM tasks
|
||||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END ${billableQuery}
|
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||||
AND project_id = projects.id) AS all_tasks_count,
|
AND project_id = projects.id ${billableQuery}) AS all_tasks_count,
|
||||||
(SELECT COUNT(*)
|
(SELECT COUNT(*)
|
||||||
FROM tasks
|
FROM tasks
|
||||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||||
@@ -95,9 +95,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
(SELECT COALESCE(SUM(time_spent), 0)
|
(SELECT COALESCE(SUM(time_spent), 0)
|
||||||
FROM task_work_log
|
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}
|
WHERE user_id = users.id
|
||||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||||
AND tasks.project_id = projects.id
|
AND tasks.project_id = projects.id
|
||||||
|
${billableQuery}
|
||||||
${duration}) AS time_logged
|
${duration}) AS time_logged
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id IN (${userIds})
|
WHERE id IN (${userIds})
|
||||||
@@ -121,10 +122,11 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
const q = `(SELECT id,
|
const q = `(SELECT id,
|
||||||
(SELECT COALESCE(SUM(time_spent), 0)
|
(SELECT COALESCE(SUM(time_spent), 0)
|
||||||
FROM task_work_log
|
FROM task_work_log
|
||||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery}
|
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
|
||||||
WHERE user_id = users.id
|
WHERE user_id = users.id
|
||||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||||
AND tasks.project_id IN (${projectIds})
|
AND tasks.project_id IN (${projectIds})
|
||||||
|
${billableQuery}
|
||||||
${duration}) AS time_logged
|
${duration}) AS time_logged
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id IN (${userIds})
|
WHERE id IN (${userIds})
|
||||||
@@ -346,6 +348,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
const projects = (req.body.projects || []) as string[];
|
const projects = (req.body.projects || []) as string[];
|
||||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||||
|
|
||||||
|
const categories = (req.body.categories || []) as string[];
|
||||||
|
const noCategory = req.body.noCategory || false;
|
||||||
const billable = req.body.billable;
|
const billable = req.body.billable;
|
||||||
|
|
||||||
if (!teamIds || !projectIds.length)
|
if (!teamIds || !projectIds.length)
|
||||||
@@ -361,6 +365,33 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
|
|
||||||
const billableQuery = this.buildBillableQuery(billable);
|
const billableQuery = this.buildBillableQuery(billable);
|
||||||
|
|
||||||
|
// Prepare projects filter
|
||||||
|
let projectsFilter = "";
|
||||||
|
if (projectIds.length > 0) {
|
||||||
|
projectsFilter = `AND p.id IN (${projectIds})`;
|
||||||
|
} else {
|
||||||
|
// If no projects are selected, don't show any data
|
||||||
|
projectsFilter = `AND 1=0`; // This will match no rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare categories filter - updated logic
|
||||||
|
let categoriesFilter = "";
|
||||||
|
if (categories.length > 0 && noCategory) {
|
||||||
|
// Both specific categories and "No Category" are selected
|
||||||
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
|
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||||
|
} else if (categories.length === 0 && noCategory) {
|
||||||
|
// Only "No Category" is selected
|
||||||
|
categoriesFilter = `AND p.category_id IS NULL`;
|
||||||
|
} else if (categories.length > 0 && !noCategory) {
|
||||||
|
// Only specific categories are selected
|
||||||
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
|
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||||
|
} else {
|
||||||
|
// categories.length === 0 && !noCategory - no categories selected, show nothing
|
||||||
|
categoriesFilter = `AND 1=0`; // This will match no rows
|
||||||
|
}
|
||||||
|
|
||||||
const q = `
|
const q = `
|
||||||
SELECT p.id,
|
SELECT p.id,
|
||||||
p.name,
|
p.name,
|
||||||
@@ -368,15 +399,13 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
SUM(total_minutes) AS estimated,
|
SUM(total_minutes) AS estimated,
|
||||||
color_code
|
color_code
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
LEFT JOIN tasks ON tasks.project_id = p.id
|
||||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
|
||||||
GROUP BY p.id, p.name
|
GROUP BY p.id, p.name
|
||||||
ORDER BY logged_time DESC;`;
|
ORDER BY logged_time DESC;`;
|
||||||
console.log('Query:', q);
|
|
||||||
const result = await db.query(q, []);
|
const result = await db.query(q, []);
|
||||||
console.log('Query result count:', result.rows.length);
|
|
||||||
console.log('Query results:', result.rows);
|
|
||||||
const utilization = (req.body.utilization || []) as string[];
|
const utilization = (req.body.utilization || []) as string[];
|
||||||
|
|
||||||
const data = [];
|
const data = [];
|
||||||
@@ -406,6 +435,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||||
|
|
||||||
const categories = (req.body.categories || []) as string[];
|
const categories = (req.body.categories || []) as string[];
|
||||||
|
const noCategory = req.body.noCategory || false;
|
||||||
const billable = req.body.billable;
|
const billable = req.body.billable;
|
||||||
|
|
||||||
if (!teamIds)
|
if (!teamIds)
|
||||||
@@ -529,13 +559,27 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
let projectsFilter = "";
|
let projectsFilter = "";
|
||||||
if (projectIds.length > 0) {
|
if (projectIds.length > 0) {
|
||||||
projectsFilter = `AND p.id IN (${projectIds})`;
|
projectsFilter = `AND p.id IN (${projectIds})`;
|
||||||
|
} else {
|
||||||
|
// If no projects are selected, don't show any data
|
||||||
|
projectsFilter = `AND 1=0`; // This will match no rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare categories filter
|
// Prepare categories filter - updated logic
|
||||||
let categoriesFilter = "";
|
let categoriesFilter = "";
|
||||||
if (categories.length > 0) {
|
if (categories.length > 0 && noCategory) {
|
||||||
|
// Both specific categories and "No Category" are selected
|
||||||
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
|
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||||
|
} else if (categories.length === 0 && noCategory) {
|
||||||
|
// Only "No Category" is selected
|
||||||
|
categoriesFilter = `AND p.category_id IS NULL`;
|
||||||
|
} else if (categories.length > 0 && !noCategory) {
|
||||||
|
// Only specific categories are selected
|
||||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||||
|
} else {
|
||||||
|
// categories.length === 0 && !noCategory - no categories selected, show nothing
|
||||||
|
categoriesFilter = `AND 1=0`; // This will match no rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create custom duration clause for twl table alias
|
// Create custom duration clause for twl table alias
|
||||||
@@ -569,13 +613,14 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
COALESCE(
|
COALESCE(
|
||||||
(SELECT SUM(twl.time_spent)
|
(SELECT SUM(twl.time_spent)
|
||||||
FROM task_work_log twl
|
FROM task_work_log twl
|
||||||
LEFT JOIN tasks t ON t.id = twl.task_id ${billableQuery}
|
LEFT JOIN tasks t ON t.id = twl.task_id
|
||||||
LEFT JOIN projects p ON p.id = t.project_id
|
LEFT JOIN projects p ON p.id = t.project_id
|
||||||
WHERE twl.user_id = tmiv.user_id
|
WHERE twl.user_id = tmiv.user_id
|
||||||
${customDurationClause}
|
${customDurationClause}
|
||||||
${projectsFilter}
|
${projectsFilter}
|
||||||
${categoriesFilter}
|
${categoriesFilter}
|
||||||
${archivedClause}
|
${archivedClause}
|
||||||
|
${billableQuery}
|
||||||
AND p.team_id = tmiv.team_id
|
AND p.team_id = tmiv.team_id
|
||||||
), 0
|
), 0
|
||||||
) AS logged_time
|
) AS logged_time
|
||||||
@@ -728,6 +773,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
|
|
||||||
const projects = (req.body.projects || []) as string[];
|
const projects = (req.body.projects || []) as string[];
|
||||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||||
|
|
||||||
|
const categories = (req.body.categories || []) as string[];
|
||||||
|
const noCategory = req.body.noCategory || false;
|
||||||
const { type, billable } = req.body;
|
const { type, billable } = req.body;
|
||||||
|
|
||||||
if (!teamIds || !projectIds.length)
|
if (!teamIds || !projectIds.length)
|
||||||
@@ -743,6 +791,33 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
|
|
||||||
const billableQuery = this.buildBillableQuery(billable);
|
const billableQuery = this.buildBillableQuery(billable);
|
||||||
|
|
||||||
|
// Prepare projects filter
|
||||||
|
let projectsFilter = "";
|
||||||
|
if (projectIds.length > 0) {
|
||||||
|
projectsFilter = `AND p.id IN (${projectIds})`;
|
||||||
|
} else {
|
||||||
|
// If no projects are selected, don't show any data
|
||||||
|
projectsFilter = `AND 1=0`; // This will match no rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare categories filter - updated logic
|
||||||
|
let categoriesFilter = "";
|
||||||
|
if (categories.length > 0 && noCategory) {
|
||||||
|
// Both specific categories and "No Category" are selected
|
||||||
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
|
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||||
|
} else if (categories.length === 0 && noCategory) {
|
||||||
|
// Only "No Category" is selected
|
||||||
|
categoriesFilter = `AND p.category_id IS NULL`;
|
||||||
|
} else if (categories.length > 0 && !noCategory) {
|
||||||
|
// Only specific categories are selected
|
||||||
|
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||||
|
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||||
|
} else {
|
||||||
|
// categories.length === 0 && !noCategory - no categories selected, show nothing
|
||||||
|
categoriesFilter = `AND 1=0`; // This will match no rows
|
||||||
|
}
|
||||||
|
|
||||||
const q = `
|
const q = `
|
||||||
SELECT p.id,
|
SELECT p.id,
|
||||||
p.name,
|
p.name,
|
||||||
@@ -756,9 +831,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
|||||||
WHERE project_id = p.id) AS estimated,
|
WHERE project_id = p.id) AS estimated,
|
||||||
color_code
|
color_code
|
||||||
FROM projects p
|
FROM projects p
|
||||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
LEFT JOIN tasks ON tasks.project_id = p.id
|
||||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
|
||||||
GROUP BY p.id, p.name
|
GROUP BY p.id, p.name
|
||||||
ORDER BY logged_time DESC;`;
|
ORDER BY logged_time DESC;`;
|
||||||
const result = await db.query(q, []);
|
const result = await db.query(q, []);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { reportingTimesheetApiService } from '@/api/reporting/reporting.timesheet.api.service';
|
import { reportingTimesheetApiService } from '@/api/reporting/reporting.timesheet.api.service';
|
||||||
import { IRPTTimeMember } from '@/types/reporting/reporting.types';
|
import { IRPTTimeMember } from '@/types/reporting/reporting.types';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ChartDataLabels);
|
||||||
@@ -29,7 +28,6 @@ export interface MembersTimeSheetRef {
|
|||||||
|
|
||||||
const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(({ onTotalsUpdate }, ref) => {
|
const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(({ onTotalsUpdate }, ref) => {
|
||||||
const { t } = useTranslation('time-report');
|
const { t } = useTranslation('time-report');
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
|
const chartRef = React.useRef<ChartJS<'bar', string[], unknown>>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -45,6 +43,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(
|
|||||||
loadingUtilization,
|
loadingUtilization,
|
||||||
billable,
|
billable,
|
||||||
archived,
|
archived,
|
||||||
|
noCategory,
|
||||||
} = useAppSelector(state => state.timeReportsOverviewReducer);
|
} = useAppSelector(state => state.timeReportsOverviewReducer);
|
||||||
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
|
const { duration, dateRange } = useAppSelector(state => state.reportingReducer);
|
||||||
|
|
||||||
@@ -223,23 +222,41 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(
|
|||||||
duration,
|
duration,
|
||||||
date_range: formattedDateRange,
|
date_range: formattedDateRange,
|
||||||
billable,
|
billable,
|
||||||
|
noCategory,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await reportingTimesheetApiService.getMemberTimeSheets(body, archived);
|
const res = await reportingTimesheetApiService.getMemberTimeSheets(body, archived);
|
||||||
if (res.done) {
|
|
||||||
setJsonData(res.body.filteredRows || []);
|
|
||||||
|
|
||||||
const totalsRaw = res.body.totals || {};
|
if (res.done) {
|
||||||
const totals = {
|
// Ensure filteredRows is always an array, even if API returns null/undefined
|
||||||
total_time_logs: totalsRaw.total_time_logs ?? "0",
|
setJsonData(res.body?.filteredRows || []);
|
||||||
total_estimated_hours: totalsRaw.total_estimated_hours ?? "0",
|
|
||||||
total_utilization: totalsRaw.total_utilization ?? "0",
|
const totalsRaw = res.body?.totals || {};
|
||||||
};
|
const totals = {
|
||||||
onTotalsUpdate(totals);
|
total_time_logs: totalsRaw.total_time_logs ?? "0",
|
||||||
|
total_estimated_hours: totalsRaw.total_estimated_hours ?? "0",
|
||||||
|
total_utilization: totalsRaw.total_utilization ?? "0",
|
||||||
|
};
|
||||||
|
onTotalsUpdate(totals);
|
||||||
|
} else {
|
||||||
|
// Handle API error case
|
||||||
|
setJsonData([]);
|
||||||
|
onTotalsUpdate({
|
||||||
|
total_time_logs: "0",
|
||||||
|
total_estimated_hours: "0",
|
||||||
|
total_utilization: "0"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching chart data:', error);
|
console.error('Error fetching chart data:', error);
|
||||||
logger.error('Error fetching chart data:', error);
|
logger.error('Error fetching chart data:', error);
|
||||||
|
// Reset data on error
|
||||||
|
setJsonData([]);
|
||||||
|
onTotalsUpdate({
|
||||||
|
total_time_logs: "0",
|
||||||
|
total_estimated_hours: "0",
|
||||||
|
total_utilization: "0"
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -247,7 +264,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef, MembersTimeSheetProps>(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchChartData();
|
fetchChartData();
|
||||||
}, [dispatch, duration, dateRange, billable, archived, teams, filterProjects, categories, members, utilization]);
|
}, [duration, dateRange, billable, archived, teams, filterProjects, categories, members, utilization]);
|
||||||
|
|
||||||
const exportChart = () => {
|
const exportChart = () => {
|
||||||
if (chartRef.current) {
|
if (chartRef.current) {
|
||||||
|
|||||||
Reference in New Issue
Block a user