refactor(reporting): enhance timezone handling and clean up migration

- Updated SQL queries in reporting controllers to join with the timezones table for accurate timezone retrieval.
- Refactored ReportingMembersController to extend ReportingControllerBaseWithTimezone for centralized timezone logic.
- Removed obsolete migration file that added a timezone column to the users table, as it is no longer needed.
This commit is contained in:
chamikaJ
2025-07-24 09:25:50 +05:30
parent 69b2fe1a90
commit de26417247
5 changed files with 29 additions and 25 deletions

View File

@@ -4,7 +4,9 @@
"Bash(find:*)", "Bash(find:*)",
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(npm run type-check:*)", "Bash(npm run type-check:*)",
"Bash(npm run:*)" "Bash(npm run:*)",
"Bash(grep:*)",
"Bash(rm:*)"
], ],
"deny": [] "deny": []
} }

View File

@@ -14,10 +14,10 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR
const teamIds = teams.map(id => `'${id}'`).join(","); const teamIds = teams.map(id => `'${id}'`).join(",");
const projects = (req.body.projects || []) as string[]; const projects = (req.body.projects || []) as string[];
const projectIds = projects.map(p => `'${p}'`).join(","); const projectIds = projects.map(p => `'${p}'`).join(",");
const billable = req.body.billable; const {billable} = req.body;
// Get user timezone from request or database // Get user timezone from request or database
const userTimezone = req.body.timezone || await getUserTimezone(req.user?.id || ''); const userTimezone = req.body.timezone || await getUserTimezone(req.user?.id || "");
if (!teamIds || !projectIds.length) if (!teamIds || !projectIds.length)
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] })); return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
@@ -30,13 +30,13 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR
if (date_range && date_range.length === 2) { if (date_range && date_range.length === 2) {
// Convert user's local dates to their timezone's start/end of day // Convert user's local dates to their timezone's start/end of day
startDate = moment.tz(date_range[0], userTimezone).startOf('day'); startDate = moment.tz(date_range[0], userTimezone).startOf("day");
endDate = moment.tz(date_range[1], userTimezone).endOf('day'); endDate = moment.tz(date_range[1], userTimezone).endOf("day");
} else if (duration === DATE_RANGES.ALL_TIME) { } else if (duration === DATE_RANGES.ALL_TIME) {
const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`; const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`;
const minDateResult = await db.query(minDateQuery, []); const minDateResult = await db.query(minDateQuery, []);
const minDate = minDateResult.rows[0]?.min_date; const minDate = minDateResult.rows[0]?.min_date;
startDate = minDate ? moment.tz(minDate, userTimezone) : moment.tz('2000-01-01', userTimezone); startDate = minDate ? moment.tz(minDate, userTimezone) : moment.tz("2000-01-01", userTimezone);
endDate = moment.tz(userTimezone); endDate = moment.tz(userTimezone);
} else { } else {
// Calculate ranges based on user's timezone // Calculate ranges based on user's timezone
@@ -44,8 +44,8 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR
switch (duration) { switch (duration) {
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("isoWeek"); startDate = now.clone().subtract(1, "week").startOf("isoWeek");
@@ -74,11 +74,11 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR
let workingDays = 0; let workingDays = 0;
const current = startDate.clone(); const current = startDate.clone();
while (current.isSameOrBefore(endDate, 'day')) { while (current.isSameOrBefore(endDate, "day")) {
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) { if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
workingDays++; workingDays++;
} }
current.add(1, 'day'); current.add(1, "day");
} }
// Updated SQL query with proper timezone handling // Updated SQL query with proper timezone handling
@@ -154,9 +154,12 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR
} }
async function getUserTimezone(userId: string): Promise<string> { async function getUserTimezone(userId: string): Promise<string> {
const q = `SELECT timezone FROM users WHERE id = $1`; 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]); const result = await db.query(q, [userId]);
return result.rows[0]?.timezone || 'UTC'; return result.rows[0]?.timezone || "UTC";
} }
function buildBillableQuery(billable: { billable: boolean; nonBillable: boolean }): string { function buildBillableQuery(billable: { billable: boolean; nonBillable: boolean }): string {

View File

@@ -12,7 +12,10 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle
* @returns The user's timezone or 'UTC' as default * @returns The user's timezone or 'UTC' as default
*/ */
protected static async getUserTimezone(userId: string): Promise<string> { protected static async getUserTimezone(userId: string): Promise<string> {
const q = `SELECT timezone FROM users WHERE id = $1`; 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]); const result = await db.query(q, [userId]);
return result.rows[0]?.timezone || 'UTC'; return result.rows[0]?.timezone || 'UTC';
} }

View File

@@ -6,10 +6,10 @@ import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
import { ServerResponse } from "../../models/server-response"; import { ServerResponse } from "../../models/server-response";
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants"; import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
import { formatDuration, getColor, int } from "../../shared/utils"; import { formatDuration, getColor, int } from "../../shared/utils";
import ReportingControllerBase from "./reporting-controller-base"; import ReportingControllerBaseWithTimezone from "./reporting-controller-base-with-timezone";
import Excel from "exceljs"; import Excel from "exceljs";
export default class ReportingMembersController extends ReportingControllerBase { export default class ReportingMembersController extends ReportingControllerBaseWithTimezone {
private static async getMembers( private static async getMembers(
teamId: string, searchQuery = "", teamId: string, searchQuery = "",
@@ -487,7 +487,9 @@ export default class ReportingMembersController extends ReportingControllerBase
dateRange = date_range.split(","); dateRange = date_range.split(",");
} }
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "twl"); // Get user timezone for proper date filtering
const userTimezone = await this.getUserTimezone(req.user?.id as string);
const durationClause = this.getDateRangeClauseWithTimezone(duration as string || DATE_RANGES.LAST_WEEK, dateRange, userTimezone);
const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log"); const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log");
const memberName = (req.query.member_name as string)?.trim() || null; const memberName = (req.query.member_name as string)?.trim() || null;
@@ -1038,7 +1040,9 @@ export default class ReportingMembersController extends ReportingControllerBase
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> { public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body; const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl"); // Get user timezone for proper date filtering
const userTimezone = await this.getUserTimezone(req.user?.id as string);
const durationClause = this.getDateRangeClauseWithTimezone(duration || DATE_RANGES.LAST_WEEK, date_range, userTimezone);
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log"); const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
const billableQuery = this.buildBillableQuery(billable); const billableQuery = this.buildBillableQuery(billable);

View File

@@ -1,8 +0,0 @@
-- Add timezone column to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS timezone VARCHAR(50) DEFAULT 'UTC';
-- Add index for better query performance
CREATE INDEX IF NOT EXISTS idx_users_timezone ON users(timezone);
-- Update existing users to use their browser timezone (this would be done via application logic)
COMMENT ON COLUMN users.timezone IS 'IANA timezone identifier (e.g., America/New_York, Asia/Tokyo)';