- 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.
140 lines
5.4 KiB
TypeScript
140 lines
5.4 KiB
TypeScript
import WorklenzControllerBase from "../worklenz-controller-base";
|
|
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
|
import db from "../../config/db";
|
|
import moment from "moment-timezone";
|
|
import { DATE_RANGES } from "../../shared/constants";
|
|
|
|
export default abstract class ReportingControllerBaseWithTimezone extends WorklenzControllerBase {
|
|
|
|
/**
|
|
* Get the user's timezone from the database or request
|
|
* @param userId - The user ID
|
|
* @returns The user's timezone or 'UTC' as default
|
|
*/
|
|
protected static async getUserTimezone(userId: string): Promise<string> {
|
|
const q = `SELECT tz.name as timezone
|
|
FROM users u
|
|
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";
|
|
}
|
|
|
|
/**
|
|
* Generate date range clause with timezone support
|
|
* @param key - Date range key (e.g., YESTERDAY, LAST_WEEK)
|
|
* @param dateRange - Array of date strings
|
|
* @param userTimezone - User's timezone (e.g., 'America/New_York')
|
|
* @returns SQL clause for date filtering
|
|
*/
|
|
protected static getDateRangeClauseWithTimezone(key: string, dateRange: string[], userTimezone: string) {
|
|
// For custom date ranges
|
|
if (dateRange.length === 2) {
|
|
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`;
|
|
}
|
|
}
|
|
|
|
// For predefined ranges, calculate based on user's timezone
|
|
const now = moment.tz(userTimezone);
|
|
let startDate, endDate;
|
|
|
|
switch (key) {
|
|
case DATE_RANGES.YESTERDAY:
|
|
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");
|
|
break;
|
|
case DATE_RANGES.LAST_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");
|
|
break;
|
|
default:
|
|
return "";
|
|
}
|
|
|
|
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 twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Format dates for display in user's timezone
|
|
* @param date - Date to format
|
|
* @param userTimezone - User's timezone
|
|
* @param format - Moment format string
|
|
* @returns Formatted date string
|
|
*/
|
|
protected static formatDateInTimezone(date: string | Date, userTimezone: string, format = "YYYY-MM-DD HH:mm:ss") {
|
|
return moment.tz(date, userTimezone).format(format);
|
|
}
|
|
|
|
/**
|
|
* Get working days count between two dates in user's timezone
|
|
* @param startDate - Start date
|
|
* @param endDate - End date
|
|
* @param userTimezone - User's timezone
|
|
* @returns Number of working days
|
|
*/
|
|
protected static getWorkingDaysInTimezone(startDate: string, endDate: string, userTimezone: string): number {
|
|
const start = moment.tz(startDate, userTimezone);
|
|
const end = moment.tz(endDate, userTimezone);
|
|
let workingDays = 0;
|
|
|
|
const current = start.clone();
|
|
while (current.isSameOrBefore(end, "day")) {
|
|
// Monday = 1, Friday = 5
|
|
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
|
|
workingDays++;
|
|
}
|
|
current.add(1, "day");
|
|
}
|
|
|
|
return workingDays;
|
|
}
|
|
} |