From b915de2b93377ae600408bb3ec3cc823c122b0a8 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Tue, 29 Jul 2025 13:05:55 +0530 Subject: [PATCH] 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. --- ...reporting-controller-base-with-timezone.ts | 75 ++++++++++++------- .../ProjectReportsDrawer.tsx | 62 ++++++++++++++- .../projects-reports/projects-reports.tsx | 5 +- 3 files changed, 111 insertions(+), 31 deletions(-) diff --git a/worklenz-backend/src/controllers/reporting/reporting-controller-base-with-timezone.ts b/worklenz-backend/src/controllers/reporting/reporting-controller-base-with-timezone.ts index 59fc9a50..1dae9147 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-controller-base-with-timezone.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-controller-base-with-timezone.ts @@ -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'); - - // 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 task_work_log.created_at >= '${startUtc}'::TIMESTAMP AND task_work_log.created_at <= '${endUtc}'::TIMESTAMP`; + try { + // Handle different date formats that might come from frontend + let startDate, endDate; + + // 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]); + } + + // 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; diff --git a/worklenz-frontend/src/features/reporting/projectReports/projectReportsDrawer/ProjectReportsDrawer.tsx b/worklenz-frontend/src/features/reporting/projectReports/projectReportsDrawer/ProjectReportsDrawer.tsx index 2355dd5e..3ab86ee2 100644 --- a/worklenz-frontend/src/features/reporting/projectReports/projectReportsDrawer/ProjectReportsDrawer.tsx +++ b/worklenz-frontend/src/features/reporting/projectReports/projectReportsDrawer/ProjectReportsDrawer.tsx @@ -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(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 ( { key: '1', label: t('membersButton') }, { key: '2', label: t('tasksButton') }, ], + onClick: ({ key }) => handleExportClick(key), }} > - diff --git a/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx b/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx index acc0541b..028e7bc1 100644 --- a/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx +++ b/worklenz-frontend/src/pages/reporting/projects-reports/projects-reports.tsx @@ -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