diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 06f61982..b6f137d5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,9 @@ "Bash(find:*)", "Bash(npm run build:*)", "Bash(npm run type-check:*)", - "Bash(npm run:*)" + "Bash(npm run:*)", + "Bash(grep:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller-updated.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller-updated.ts index d416d0b6..9d23c03d 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller-updated.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller-updated.ts @@ -14,10 +14,10 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR const teamIds = teams.map(id => `'${id}'`).join(","); const projects = (req.body.projects || []) as string[]; const projectIds = projects.map(p => `'${p}'`).join(","); - const billable = req.body.billable; + const {billable} = req.body; // 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) 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) { // Convert user's local dates to their timezone's start/end of day - startDate = moment.tz(date_range[0], userTimezone).startOf('day'); - endDate = moment.tz(date_range[1], userTimezone).endOf('day'); + startDate = moment.tz(date_range[0], userTimezone).startOf("day"); + endDate = moment.tz(date_range[1], userTimezone).endOf("day"); } 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 minDateResult = await db.query(minDateQuery, []); 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); } else { // Calculate ranges based on user's timezone @@ -44,8 +44,8 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR switch (duration) { 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("isoWeek"); @@ -74,11 +74,11 @@ export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzR let workingDays = 0; const current = startDate.clone(); - while (current.isSameOrBefore(endDate, 'day')) { + while (current.isSameOrBefore(endDate, "day")) { if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) { workingDays++; } - current.add(1, 'day'); + current.add(1, "day"); } // 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 { - 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]); - return result.rows[0]?.timezone || 'UTC'; + return result.rows[0]?.timezone || "UTC"; } function buildBillableQuery(billable: { billable: boolean; nonBillable: boolean }): string { 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 614c1671..59fc9a50 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 @@ -12,7 +12,10 @@ export default abstract class ReportingControllerBaseWithTimezone extends Workle * @returns The user's timezone or 'UTC' as default */ protected static async getUserTimezone(userId: string): Promise { - 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]); return result.rows[0]?.timezone || 'UTC'; } diff --git a/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts index 97500437..d2d674eb 100644 --- a/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts +++ b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts @@ -6,10 +6,10 @@ import { IWorkLenzResponse } from "../../interfaces/worklenz-response"; import { ServerResponse } from "../../models/server-response"; import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants"; 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"; -export default class ReportingMembersController extends ReportingControllerBase { +export default class ReportingMembersController extends ReportingControllerBaseWithTimezone { private static async getMembers( teamId: string, searchQuery = "", @@ -487,7 +487,9 @@ export default class ReportingMembersController extends ReportingControllerBase 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 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 { 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 billableQuery = this.buildBillableQuery(billable); diff --git a/worklenz-backend/src/migrations/add_user_timezone.sql b/worklenz-backend/src/migrations/add_user_timezone.sql deleted file mode 100644 index a2eddb6b..00000000 --- a/worklenz-backend/src/migrations/add_user_timezone.sql +++ /dev/null @@ -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)'; \ No newline at end of file