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:
@@ -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": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)';
|
|
||||||
Reference in New Issue
Block a user