feat(reporting): enhance date handling and export functionality

- Improved date range handling in ReportingControllerBaseWithTimezone to support various date formats from the frontend, ensuring robust parsing and timezone conversion.
- Updated SQL queries to use consistent table aliases for clarity.
- Added export functionality for project members and tasks in ProjectReportsDrawer, allowing users to download relevant data.
- Enhanced the Excel export handler in ProjectsReports to streamline project data exports based on the current session's team name.
This commit is contained in:
chamikaJ
2025-07-29 13:05:55 +05:30
parent 29b8c1b2af
commit b915de2b93
3 changed files with 111 additions and 31 deletions

View File

@@ -17,7 +17,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
JOIN timezones tz ON u.timezone_id = tz.id JOIN timezones tz ON u.timezone_id = tz.id
WHERE u.id = $1`; WHERE u.id = $1`;
const result = await db.query(q, [userId]); const result = await db.query(q, [userId]);
return result.rows[0]?.timezone || 'UTC'; return result.rows[0]?.timezone || "UTC";
} }
/** /**
@@ -30,20 +30,43 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
protected static getDateRangeClauseWithTimezone(key: string, dateRange: string[], userTimezone: string) { protected static getDateRangeClauseWithTimezone(key: string, dateRange: string[], userTimezone: string) {
// For custom date ranges // For custom date ranges
if (dateRange.length === 2) { if (dateRange.length === 2) {
// Convert dates to user's timezone start/end of day try {
const start = moment.tz(dateRange[0], userTimezone).startOf('day'); // Handle different date formats that might come from frontend
const end = moment.tz(dateRange[1], userTimezone).endOf('day'); let startDate, endDate;
// Convert to UTC for database comparison // Try to parse the date - it might be a full JS Date string or ISO string
const startUtc = start.utc().format("YYYY-MM-DD HH:mm:ss"); if (dateRange[0].includes("GMT") || dateRange[0].includes("(")) {
const endUtc = end.utc().format("YYYY-MM-DD HH:mm:ss"); // Parse JavaScript Date toString() format
startDate = moment(new Date(dateRange[0]));
if (start.isSame(end, 'day')) { endDate = moment(new Date(dateRange[1]));
// Single day selection } else {
return `AND task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`; // Parse ISO format or other formats
startDate = moment(dateRange[0]);
endDate = moment(dateRange[1]);
}
// Convert to user's timezone and get start/end of day
const start = startDate.tz(userTimezone).startOf("day");
const end = endDate.tz(userTimezone).endOf("day");
// Convert to UTC for database comparison
const startUtc = start.utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = end.utc().format("YYYY-MM-DD HH:mm:ss");
if (start.isSame(end, "day")) {
// Single day selection
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
}
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
} catch (error) {
console.error("Error parsing date range:", error, { dateRange, userTimezone });
// Fallback to current date if parsing fails
const now = moment.tz(userTimezone);
const startUtc = now.clone().startOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = now.clone().endOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
} }
return `AND task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`;
} }
// For predefined ranges, calculate based on user's timezone // For predefined ranges, calculate based on user's timezone
@@ -52,20 +75,20 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
switch (key) { switch (key) {
case DATE_RANGES.YESTERDAY: case DATE_RANGES.YESTERDAY:
startDate = now.clone().subtract(1, 'day').startOf('day'); startDate = now.clone().subtract(1, "day").startOf("day");
endDate = now.clone().subtract(1, 'day').endOf('day'); endDate = now.clone().subtract(1, "day").endOf("day");
break; break;
case DATE_RANGES.LAST_WEEK: case DATE_RANGES.LAST_WEEK:
startDate = now.clone().subtract(1, 'week').startOf('week'); startDate = now.clone().subtract(1, "week").startOf("week");
endDate = now.clone().subtract(1, 'week').endOf('week'); endDate = now.clone().subtract(1, "week").endOf("week");
break; break;
case DATE_RANGES.LAST_MONTH: case DATE_RANGES.LAST_MONTH:
startDate = now.clone().subtract(1, 'month').startOf('month'); startDate = now.clone().subtract(1, "month").startOf("month");
endDate = now.clone().subtract(1, 'month').endOf('month'); endDate = now.clone().subtract(1, "month").endOf("month");
break; break;
case DATE_RANGES.LAST_QUARTER: case DATE_RANGES.LAST_QUARTER:
startDate = now.clone().subtract(3, 'months').startOf('day'); startDate = now.clone().subtract(3, "months").startOf("day");
endDate = now.clone().endOf('day'); endDate = now.clone().endOf("day");
break; break;
default: default:
return ""; return "";
@@ -74,7 +97,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
if (startDate && endDate) { if (startDate && endDate) {
const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss"); const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss");
const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss"); const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss");
return `AND task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`; return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
} }
return ""; return "";
@@ -87,7 +110,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
* @param format - Moment format string * @param format - Moment format string
* @returns Formatted date string * @returns Formatted date string
*/ */
protected static formatDateInTimezone(date: string | Date, userTimezone: string, format: string = "YYYY-MM-DD HH:mm:ss") { protected static formatDateInTimezone(date: string | Date, userTimezone: string, format = "YYYY-MM-DD HH:mm:ss") {
return moment.tz(date, userTimezone).format(format); return moment.tz(date, userTimezone).format(format);
} }
@@ -104,12 +127,12 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
let workingDays = 0; let workingDays = 0;
const current = start.clone(); const current = start.clone();
while (current.isSameOrBefore(end, 'day')) { while (current.isSameOrBefore(end, "day")) {
// Monday = 1, Friday = 5 // Monday = 1, Friday = 5
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) { if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
workingDays++; workingDays++;
} }
current.add(1, 'day'); current.add(1, "day");
} }
return workingDays; return workingDays;

View File

@@ -1,5 +1,5 @@
import { Drawer, Typography, Flex, Button, Dropdown } from '@/shared/antd-imports'; import { Drawer, Typography, Flex, Button, Dropdown } from '@/shared/antd-imports';
import React, { useState } from 'react'; import React, { useState, useCallback } from 'react';
import { useAppSelector } from '../../../../hooks/useAppSelector'; import { useAppSelector } from '../../../../hooks/useAppSelector';
import { useAppDispatch } from '../../../../hooks/useAppDispatch'; import { useAppDispatch } from '../../../../hooks/useAppDispatch';
import { setSelectedProject, toggleProjectReportsDrawer } from '../project-reports-slice'; import { setSelectedProject, toggleProjectReportsDrawer } from '../project-reports-slice';
@@ -8,6 +8,8 @@ import ProjectReportsDrawerTabs from './ProjectReportsDrawerTabs';
import { colors } from '../../../../styles/colors'; import { colors } from '../../../../styles/colors';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IRPTProject } from '@/types/reporting/reporting.types'; import { IRPTProject } from '@/types/reporting/reporting.types';
import { useAuthService } from '../../../../hooks/useAuth';
import { reportingExportApiService } from '@/api/reporting/reporting-export.api.service';
type ProjectReportsDrawerProps = { type ProjectReportsDrawerProps = {
selectedProject: IRPTProject | null; selectedProject: IRPTProject | null;
@@ -17,6 +19,8 @@ const ProjectReportsDrawer = ({ selectedProject }: ProjectReportsDrawerProps) =>
const { t } = useTranslation('reporting-projects-drawer'); const { t } = useTranslation('reporting-projects-drawer');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const currentSession = useAuthService().getCurrentSession();
const [exporting, setExporting] = useState<boolean>(false);
// get drawer open state and project list from the reducer // get drawer open state and project list from the reducer
const isDrawerOpen = useAppSelector( const isDrawerOpen = useAppSelector(
@@ -35,6 +39,54 @@ const ProjectReportsDrawer = ({ selectedProject }: ProjectReportsDrawerProps) =>
} }
}; };
// Export handlers
const handleExportMembers = useCallback(() => {
if (!selectedProject?.id) return;
try {
setExporting(true);
const teamName = currentSession?.team_name || 'Team';
reportingExportApiService.exportProjectMembers(
selectedProject.id,
selectedProject.name,
teamName
);
} catch (error) {
console.error('Error exporting project members:', error);
} finally {
setExporting(false);
}
}, [selectedProject, currentSession?.team_name]);
const handleExportTasks = useCallback(() => {
if (!selectedProject?.id) return;
try {
setExporting(true);
const teamName = currentSession?.team_name || 'Team';
reportingExportApiService.exportProjectTasks(
selectedProject.id,
selectedProject.name,
teamName
);
} catch (error) {
console.error('Error exporting project tasks:', error);
} finally {
setExporting(false);
}
}, [selectedProject, currentSession?.team_name]);
const handleExportClick = useCallback((key: string) => {
switch (key) {
case '1':
handleExportMembers();
break;
case '2':
handleExportTasks();
break;
default:
break;
}
}, [handleExportMembers, handleExportTasks]);
return ( return (
<Drawer <Drawer
open={isDrawerOpen} open={isDrawerOpen}
@@ -56,9 +108,15 @@ const ProjectReportsDrawer = ({ selectedProject }: ProjectReportsDrawerProps) =>
{ key: '1', label: t('membersButton') }, { key: '1', label: t('membersButton') },
{ key: '2', label: t('tasksButton') }, { key: '2', label: t('tasksButton') },
], ],
onClick: ({ key }) => handleExportClick(key),
}} }}
> >
<Button type="primary" icon={<DownOutlined />} iconPosition="end"> <Button
type="primary"
loading={exporting}
icon={<DownOutlined />}
iconPosition="end"
>
{t('exportButton')} {t('exportButton')}
</Button> </Button>
</Dropdown> </Dropdown>

View File

@@ -28,9 +28,8 @@ const ProjectsReports = () => {
// Memoize the Excel export handler to prevent recreation on every render // Memoize the Excel export handler to prevent recreation on every render
const handleExcelExport = useCallback(() => { const handleExcelExport = useCallback(() => {
if (currentSession?.team_name) { const teamName = currentSession?.team_name || 'Team';
reportingExportApiService.exportProjects(currentSession.team_name); reportingExportApiService.exportProjects(teamName);
}
}, [currentSession?.team_name]); }, [currentSession?.team_name]);
// Memoize the archived checkbox handler to prevent recreation on every render // Memoize the archived checkbox handler to prevent recreation on every render