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:
@@ -17,7 +17,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
|
||||
JOIN timezones tz ON u.timezone_id = tz.id
|
||||
WHERE u.id = $1`;
|
||||
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) {
|
||||
// For custom date ranges
|
||||
if (dateRange.length === 2) {
|
||||
// Convert dates to user's timezone start/end of day
|
||||
const start = moment.tz(dateRange[0], userTimezone).startOf('day');
|
||||
const end = moment.tz(dateRange[1], userTimezone).endOf('day');
|
||||
try {
|
||||
// Handle different date formats that might come from frontend
|
||||
let startDate, endDate;
|
||||
|
||||
// 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");
|
||||
// Try to parse the date - it might be a full JS Date string or ISO string
|
||||
if (dateRange[0].includes("GMT") || dateRange[0].includes("(")) {
|
||||
// Parse JavaScript Date toString() format
|
||||
startDate = moment(new Date(dateRange[0]));
|
||||
endDate = moment(new Date(dateRange[1]));
|
||||
} else {
|
||||
// Parse ISO format or other formats
|
||||
startDate = moment(dateRange[0]);
|
||||
endDate = moment(dateRange[1]);
|
||||
}
|
||||
|
||||
if (start.isSame(end, 'day')) {
|
||||
// Single day selection
|
||||
return `AND task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`;
|
||||
// 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
|
||||
@@ -52,20 +75,20 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
|
||||
|
||||
switch (key) {
|
||||
case DATE_RANGES.YESTERDAY:
|
||||
startDate = now.clone().subtract(1, 'day').startOf('day');
|
||||
endDate = now.clone().subtract(1, 'day').endOf('day');
|
||||
startDate = now.clone().subtract(1, "day").startOf("day");
|
||||
endDate = now.clone().subtract(1, "day").endOf("day");
|
||||
break;
|
||||
case DATE_RANGES.LAST_WEEK:
|
||||
startDate = now.clone().subtract(1, 'week').startOf('week');
|
||||
endDate = now.clone().subtract(1, 'week').endOf('week');
|
||||
startDate = now.clone().subtract(1, "week").startOf("week");
|
||||
endDate = now.clone().subtract(1, "week").endOf("week");
|
||||
break;
|
||||
case DATE_RANGES.LAST_MONTH:
|
||||
startDate = now.clone().subtract(1, 'month').startOf('month');
|
||||
endDate = now.clone().subtract(1, 'month').endOf('month');
|
||||
startDate = now.clone().subtract(1, "month").startOf("month");
|
||||
endDate = now.clone().subtract(1, "month").endOf("month");
|
||||
break;
|
||||
case DATE_RANGES.LAST_QUARTER:
|
||||
startDate = now.clone().subtract(3, 'months').startOf('day');
|
||||
endDate = now.clone().endOf('day');
|
||||
startDate = now.clone().subtract(3, "months").startOf("day");
|
||||
endDate = now.clone().endOf("day");
|
||||
break;
|
||||
default:
|
||||
return "";
|
||||
@@ -74,7 +97,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
|
||||
if (startDate && endDate) {
|
||||
const startUtc = startDate.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 "";
|
||||
@@ -87,7 +110,7 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
|
||||
* @param format - Moment format 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);
|
||||
}
|
||||
|
||||
@@ -104,12 +127,12 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
|
||||
let workingDays = 0;
|
||||
|
||||
const current = start.clone();
|
||||
while (current.isSameOrBefore(end, 'day')) {
|
||||
while (current.isSameOrBefore(end, "day")) {
|
||||
// Monday = 1, Friday = 5
|
||||
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
|
||||
workingDays++;
|
||||
}
|
||||
current.add(1, 'day');
|
||||
current.add(1, "day");
|
||||
}
|
||||
|
||||
return workingDays;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useAppDispatch } from '../../../../hooks/useAppDispatch';
|
||||
import { setSelectedProject, toggleProjectReportsDrawer } from '../project-reports-slice';
|
||||
@@ -8,6 +8,8 @@ import ProjectReportsDrawerTabs from './ProjectReportsDrawerTabs';
|
||||
import { colors } from '../../../../styles/colors';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IRPTProject } from '@/types/reporting/reporting.types';
|
||||
import { useAuthService } from '../../../../hooks/useAuth';
|
||||
import { reportingExportApiService } from '@/api/reporting/reporting-export.api.service';
|
||||
|
||||
type ProjectReportsDrawerProps = {
|
||||
selectedProject: IRPTProject | null;
|
||||
@@ -17,6 +19,8 @@ const ProjectReportsDrawer = ({ selectedProject }: ProjectReportsDrawerProps) =>
|
||||
const { t } = useTranslation('reporting-projects-drawer');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const [exporting, setExporting] = useState<boolean>(false);
|
||||
|
||||
// get drawer open state and project list from the reducer
|
||||
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 (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
@@ -56,9 +108,15 @@ const ProjectReportsDrawer = ({ selectedProject }: ProjectReportsDrawerProps) =>
|
||||
{ key: '1', label: t('membersButton') },
|
||||
{ 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')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
@@ -28,9 +28,8 @@ const ProjectsReports = () => {
|
||||
|
||||
// Memoize the Excel export handler to prevent recreation on every render
|
||||
const handleExcelExport = useCallback(() => {
|
||||
if (currentSession?.team_name) {
|
||||
reportingExportApiService.exportProjects(currentSession.team_name);
|
||||
}
|
||||
const teamName = currentSession?.team_name || 'Team';
|
||||
reportingExportApiService.exportProjects(teamName);
|
||||
}, [currentSession?.team_name]);
|
||||
|
||||
// Memoize the archived checkbox handler to prevent recreation on every render
|
||||
|
||||
Reference in New Issue
Block a user