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
|
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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user