From 2d3aef5b94c60c3389a256ca5dd535365f5cd08c Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Tue, 3 Feb 2026 12:55:57 +0200 Subject: [PATCH 1/3] feat: #86ewfyjet Implement user timezone setting --- frontend/locales/en.json | 5 +- frontend/pages/auth/login.vue | 169 +++++++------- frontend/pages/settings/profile.vue | 321 +++++++++++++------------- frontend/stores/profile.ts | 17 +- frontend/types/database.type.ts | 1 + server/src/modules/auth/router.ts | 40 +++- server/src/modules/profile/db.ts | 1 + server/src/modules/profile/service.ts | 10 +- 8 files changed, 293 insertions(+), 271 deletions(-) diff --git a/frontend/locales/en.json b/frontend/locales/en.json index f968a99..48896b0 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -186,7 +186,10 @@ "reset-password": "Reset Password", "uploading": "Uploading...", "profile-updated": "Profile updated successfully", - "profile-update-failed": "Failed to update profile" + "profile-update-failed": "Failed to update profile", + "timezone": "Timezone", + "select_timezone": "Select Timezone", + "timezone_desc": "Select your local timezone so reviews appear at the correct time." }, "billing": { "billing": "Billing", diff --git a/frontend/pages/auth/login.vue b/frontend/pages/auth/login.vue index 0a84bb7..19a2003 100644 --- a/frontend/pages/auth/login.vue +++ b/frontend/pages/auth/login.vue @@ -3,50 +3,44 @@
background gradient
-
- coming soon object 1 - coming soon object 2 +
+ coming soon object 1 + coming soon object 2 coming soon object 3 polygon object
+ class="relative flex w-full max-w-[1502px] flex-col justify-between overflow-hidden rounded-md bg-white/60 backdrop-blur-lg dark:bg-black/50 lg:min-h-[700px] lg:flex-row lg:gap-10 xl:gap-0">
Cover Image
-
+
-

{{ t('auth.signin') }}

+

{{ + t('auth.signin') }}

Use one of your social accounts to {{ t('auth.signin') }} / {{ t('auth.signup') }}.

-
@@ -100,125 +82,136 @@ + // Call the store function + await profileStore.updateProfile(profileData); - diff --git a/frontend/stores/profile.ts b/frontend/stores/profile.ts index 7edd7ef..7af6028 100644 --- a/frontend/stores/profile.ts +++ b/frontend/stores/profile.ts @@ -89,8 +89,12 @@ export const useProfileStore = defineStore('profile', () => { .catch((error) => null); } - function updateProfile(profileData: { name?: string; profileImage?: File; preferences?: Record }) { - if (!profileData.name) { + function updateProfile(profileData: { name?: string; profileImage?: File; preferences?: Record; timeZone?: string }) { + const updatePayload: any = {}; + if (profileData.name) updatePayload.name = profileData.name; + if (profileData.timeZone) updatePayload.timeZone = profileData.timeZone; + + if (Object.keys(updatePayload).length === 0) { return Promise.resolve(userDetail.value); } @@ -101,12 +105,13 @@ export const useProfileStore = defineStore('profile', () => { query: { refId: authentication.user?.id, }, - update: { - name: profileData.name, - }, + update: updatePayload, }) .then((res) => { - userDetail.value!.name = profileData.name || ''; + if (userDetail.value) { + if (profileData.name) userDetail.value.name = profileData.name; + if (profileData.timeZone) userDetail.value.timeZone = profileData.timeZone; + } return res; }); } diff --git a/frontend/types/database.type.ts b/frontend/types/database.type.ts index 976c4cb..2045076 100644 --- a/frontend/types/database.type.ts +++ b/frontend/types/database.type.ts @@ -24,6 +24,7 @@ export interface ProfileType { refId: string; name: string; gPicture: string; + timeZone?: string; images: FileDocument[]; } diff --git a/server/src/modules/auth/router.ts b/server/src/modules/auth/router.ts index 5c5b1ad..3de1064 100644 --- a/server/src/modules/auth/router.ts +++ b/server/src/modules/auth/router.ts @@ -38,8 +38,13 @@ auth.get("/google", async (ctx) => { scope: SCOPE, // Pass through the redirect parameter as state to preserve it through OAuth flow state: ctx.query.redirect - ? JSON.stringify({ redirect: ctx.query.redirect }) - : undefined, + ? JSON.stringify({ + redirect: ctx.query.redirect, + timezone: ctx.query.timezone + }) + : JSON.stringify({ + timezone: ctx.query.timezone + }), }); ctx.response.redirect(url); @@ -82,6 +87,16 @@ auth.get("/google/code-login", async (ctx) => { } } + let timeZone: string | null = null; + if (ctx.query.state) { + try { + const state = JSON.parse(ctx.query.state as string); + timeZone = state.timezone; + } catch (error) { + // ignore + } + } + const { tokens } = await oauth2Client.getToken(ctx.query.code as string); if (!tokens) { @@ -124,6 +139,7 @@ auth.get("/google/code-login", async (ctx) => { refId: userId as string, gPicture: picture as string, name: name as string, + timeZone: timeZone || undefined, }, false ); @@ -185,17 +201,17 @@ auth.get("/google/access-token-login", async (ctx) => { // Initialize Leitner System if (registeredUser || (await userManager.getUserByIdentity(googleEmail, "email").catch(() => null))) { - // We need the userId. If registeredUser was null but we just registered, fetch ID? - // userManager.registerUser returns userId. - // Re-fetching user to get ID if we didn't have it (though we just registered it). - const user = await userManager.getUserByIdentity(googleEmail, "email"); - if (user) { - try { - await LeitnerService.ensureInitialized(user.id); - } catch (e) { - console.error("Failed to initialize Leitner System on token login", e); - } + // We need the userId. If registeredUser was null but we just registered, fetch ID? + // userManager.registerUser returns userId. + // Re-fetching user to get ID if we didn't have it (though we just registered it). + const user = await userManager.getUserByIdentity(googleEmail, "email"); + if (user) { + try { + await LeitnerService.ensureInitialized(user.id); + } catch (e) { + console.error("Failed to initialize Leitner System on token login", e); } + } } ctx.body = reply.create("s", { token }); diff --git a/server/src/modules/profile/db.ts b/server/src/modules/profile/db.ts index 6e33885..57190cc 100644 --- a/server/src/modules/profile/db.ts +++ b/server/src/modules/profile/db.ts @@ -15,6 +15,7 @@ const profileCollection = defineCollection({ gPicture: String, name: String, refId: String, + timeZone: String, images: [schemas.file], }, { timestamps: true } diff --git a/server/src/modules/profile/service.ts b/server/src/modules/profile/service.ts index 643e9eb..c64cf10 100644 --- a/server/src/modules/profile/service.ts +++ b/server/src/modules/profile/service.ts @@ -5,10 +5,11 @@ interface UserProfile { refId: string; gPicture?: string; name?: string; + timeZone?: string; } export const updateUserProfile = async ( - { refId, gPicture, name }: UserProfile, + { refId, gPicture, name, timeZone }: UserProfile, rewrite = false ): Promise => { const profileCollection = getCollection(DATABASE, PROFILE_COLLECTION); @@ -16,11 +17,14 @@ export const updateUserProfile = async ( const isExist = await profileCollection.findOne({ refId }); if (!isExist) { - await profileCollection.insertMany({ refId, gPicture, name }); + await profileCollection.insertMany({ refId, gPicture, name, timeZone }); return; } if (rewrite) { - await profileCollection.updateOne({ refId }, { gPicture, name }); + await profileCollection.updateOne({ refId }, { gPicture, name, timeZone }); + } else if (timeZone && !(isExist as any).timeZone) { + // Only set timezone if it's not already set (ignore browser timezone on subsequent logins) + await profileCollection.updateOne({ refId }, { $set: { timeZone } }); } }; \ No newline at end of file From b3f240edeaefe3fd4639e96a497e34fe98a39db8 Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Tue, 3 Feb 2026 12:56:45 +0200 Subject: [PATCH 2/3] feat: #86ewfyjet Implement timezone support for Leitner box review scheduling and display. --- frontend/components/Leitner/LeitnerSettings.vue | 3 ++- server/src/modules/leitner_box/service.ts | 17 ++++++++++++++--- server/src/modules/schedule/service.ts | 7 +++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/components/Leitner/LeitnerSettings.vue b/frontend/components/Leitner/LeitnerSettings.vue index 80c1c6c..1dbd8b6 100644 --- a/frontend/components/Leitner/LeitnerSettings.vue +++ b/frontend/components/Leitner/LeitnerSettings.vue @@ -22,7 +22,8 @@ $t('smart_review.next_session') }}

{{ localSettings.reviewHour }}:00

{{ localSettings.reviewInterval === 1 ? $t('smart_review.daily') : - $t('smart_review.every_days_reminder', { days: localSettings.reviewInterval }) }}

+ $t('smart_review.every_days_reminder', { days: localSettings.reviewInterval }) }} ({{ + profileStore.userDetail?.timeZone || 'UTC' }})

diff --git a/server/src/modules/leitner_box/service.ts b/server/src/modules/leitner_box/service.ts index 9be6739..1fb163d 100644 --- a/server/src/modules/leitner_box/service.ts +++ b/server/src/modules/leitner_box/service.ts @@ -1,5 +1,5 @@ import { LeitnerItem } from "./db"; -import { DATABASE, PHRASE_COLLECTION, DATABASE_LEITNER, LEITNER_SYSTEM_COLLECTION, BUNDLE_COLLECTION } from "../../config"; +import { DATABASE, PHRASE_COLLECTION, DATABASE_LEITNER, LEITNER_SYSTEM_COLLECTION, BUNDLE_COLLECTION, PROFILE_COLLECTION } from "../../config"; import { getCollection } from "@modular-rest/server"; import { Document } from "mongoose"; import { BoardService } from "../board/service"; @@ -51,7 +51,13 @@ export class LeitnerService { this.syncScheduledJob(userId).catch(e => console.error(`[LeitnerService] Async job sync failed for ${userId}:`, e)); } - return settings; + const profileCol = await getCollection(DATABASE, PROFILE_COLLECTION); + const profile = await profileCol.findOne({ refId: userId }) as any; + + return { + ...settings, + timeZone: profile?.timeZone + }; } static async ensureInitialized(userId: string) { @@ -462,6 +468,10 @@ export class LeitnerService { // Cron: 0 minute, reviewHour hour, every X days, *, * const cron = `0 ${reviewHour} */${reviewInterval} * *`; + const profileCol = await getCollection(DATABASE, PROFILE_COLLECTION); + const profile = await profileCol.findOne({ refId: userId }) as any; + const timeZone = profile?.timeZone; + await ScheduleService.createJob( `leitner-review-${userId}`, 'leitner-review-job', @@ -469,7 +479,8 @@ export class LeitnerService { cronExpression: cron, args: { userId }, jobType: 'recurrent', - catchUp: true + catchUp: true, + timeZone } ); diff --git a/server/src/modules/schedule/service.ts b/server/src/modules/schedule/service.ts index e110cf6..650faeb 100644 --- a/server/src/modules/schedule/service.ts +++ b/server/src/modules/schedule/service.ts @@ -38,6 +38,7 @@ export class ScheduleService { executionType?: "Immediate" | "normal"; jobType?: "recurrent" | "once"; catchUp?: boolean; + timeZone?: string; } ) { const { @@ -46,7 +47,8 @@ export class ScheduleService { args = {}, executionType = "normal", jobType = "recurrent", - catchUp = false + catchUp = false, + timeZone } = options; const collection = await getCollection(DATABASE_SCHEDULE, SCHEDULE_JOB_COLLECTION); @@ -62,6 +64,7 @@ export class ScheduleService { jobType, state: "scheduled" as const, catchUp, + timeZone, }; if (existing) { @@ -98,7 +101,7 @@ export class ScheduleService { schedule.scheduledJobs[jobData.name].cancel(); } - const scheduleParam = jobData.jobType === "once" ? jobData.runAt : jobData.cronExpression; + const scheduleParam = jobData.jobType === "once" ? jobData.runAt : (jobData.timeZone ? { rule: jobData.cronExpression, tz: jobData.timeZone } : jobData.cronExpression); if (!scheduleParam) { console.warn(`[ScheduleService] No schedule parameter for job: ${jobData.name}`); return; From bb7d15526a4573f9000a5286a15f13ac060ddf2e Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Tue, 3 Feb 2026 13:40:19 +0200 Subject: [PATCH 3/3] fix(schedule): #86ewfyjet add timezone support testing and fix catch-up logic --- .../leitner_box/__tests__/service.test.ts | 92 +++++++++++++++++- .../modules/profile/__tests__/service.test.ts | 96 +++++++++++++++++++ .../schedule/__tests__/service.test.ts | 90 +++++++++++++++++ server/src/modules/schedule/db.ts | 2 + server/src/modules/schedule/service.ts | 2 +- 5 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 server/src/modules/profile/__tests__/service.test.ts diff --git a/server/src/modules/leitner_box/__tests__/service.test.ts b/server/src/modules/leitner_box/__tests__/service.test.ts index 1134ff7..bc075be 100644 --- a/server/src/modules/leitner_box/__tests__/service.test.ts +++ b/server/src/modules/leitner_box/__tests__/service.test.ts @@ -2,6 +2,9 @@ import { describe, it, expect, jest, beforeEach } from "@jest/globals"; import { LeitnerService } from "../service"; import { getCollection } from "@modular-rest/server"; +import { ScheduleService } from "../../schedule/service"; +import { DATABASE, PROFILE_COLLECTION } from "../../../config"; + // Mock modular-rest/server jest.mock("@modular-rest/server", () => ({ getCollection: jest.fn(), @@ -11,8 +14,26 @@ jest.mock("@modular-rest/server", () => ({ schemas: { file: {} }, })); +// Mock ScheduleService +jest.mock("../../schedule/service", () => ({ + ScheduleService: { + createJob: jest.fn(), + register: jest.fn(), + }, +})); + +jest.mock("../../../config", () => ({ + DATABASE: "db", + PROFILE_COLLECTION: "profile", + DATABASE_LEITNER: "leitner_db", + LEITNER_SYSTEM_COLLECTION: "leitner_col", + PHRASE_COLLECTION: "phrase", + BUNDLE_COLLECTION: "bundle", +})); + describe("LeitnerService", () => { let mockCollection: any; + let mockProfileCollection: any; beforeEach(() => { jest.clearAllMocks(); @@ -21,7 +42,18 @@ describe("LeitnerService", () => { create: jest.fn(), updateOne: jest.fn(), }; - (getCollection as any).mockResolvedValue(mockCollection); + // Default mock for getCollection to return generic collection + (getCollection as any).mockImplementation((db: string, col: string) => { + if (col === PROFILE_COLLECTION) { + if (!mockProfileCollection) { + mockProfileCollection = { + findOne: jest.fn(), + }; + } + return Promise.resolve(mockProfileCollection); + } + return Promise.resolve(mockCollection); + }); }); describe("getSettings", () => { @@ -70,4 +102,62 @@ describe("LeitnerService", () => { expect(mockCollection.create).not.toHaveBeenCalled(); }); }); + + describe("syncScheduledJob (via updateSettings)", () => { + it("should schedule job with user timezone from profile", async () => { + const userId = "user1"; + const timeZone = "Asia/Tokyo"; + const settings = { reviewInterval: 1, reviewHour: 9 }; + + // Mock existing system + // We need to simulate the DB update: + // 1. First call (updateSettings -> getSystem) returns old settings. + // 2. Second call (syncScheduledJob -> getSettings -> getSystem) should return NEW settings (reviewHour: 10). + mockCollection.findOne + .mockResolvedValueOnce({ _id: "sys1", userId, settings }) // First call + .mockResolvedValueOnce({ _id: "sys1", userId, settings: { ...settings, reviewHour: 10 } }); // Second call + + // Mock profile with timezone + mockProfileCollection = { + findOne: jest.fn().mockResolvedValue({ refId: userId, timeZone }), + }; + + // We use updateSettings to trigger syncScheduledJob since it's private + await LeitnerService.updateSettings(userId, { reviewHour: 10 }); + + expect(ScheduleService.createJob).toHaveBeenCalledWith( + `leitner-review-${userId}`, + "leitner-review-job", + expect.objectContaining({ + timeZone: timeZone, + cronExpression: expect.stringContaining("10"), // 10 from update + }) + ); + }); + + it("should use default setting if no timezone set", async () => { + const userId = "user2"; + // Mock existing system + mockCollection.findOne.mockResolvedValue({ + _id: "sys2", + userId, + settings: { reviewInterval: 1, reviewHour: 9 } + }); + + // Mock profile WITHOUT timezone + mockProfileCollection = { + findOne: jest.fn().mockResolvedValue({ refId: userId }), + }; + + await LeitnerService.updateSettings(userId, { reviewHour: 10 }); + + expect(ScheduleService.createJob).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + timeZone: undefined, + }) + ); + }); + }); }); diff --git a/server/src/modules/profile/__tests__/service.test.ts b/server/src/modules/profile/__tests__/service.test.ts new file mode 100644 index 0000000..5f090ad --- /dev/null +++ b/server/src/modules/profile/__tests__/service.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, jest, beforeEach } from "@jest/globals"; +import { updateUserProfile } from "../service"; +import { getCollection } from "@modular-rest/server"; + +// Mock modular-rest/server +jest.mock("@modular-rest/server", () => ({ + getCollection: jest.fn(), + Schema: class { }, + defineCollection: jest.fn(), +})); + +jest.mock("../../../config", () => ({ + DATABASE: "test_db", + PROFILE_COLLECTION: "profile_col", +})); + +describe("ProfileService", () => { + let mockCollection: any; + + beforeEach(() => { + jest.clearAllMocks(); + mockCollection = { + findOne: jest.fn(), + insertMany: jest.fn(), + updateOne: jest.fn(), + }; + (getCollection as any).mockReturnValue(mockCollection); + }); + + describe("updateUserProfile", () => { + const refId = "user123"; + const gPicture = "pic.jpg"; + const name = "Test User"; + + it("should create a new profile with timezone if it does not exist", async () => { + mockCollection.findOne.mockResolvedValue(null); + + await updateUserProfile({ refId, gPicture, name, timeZone: "UTC" }); + + expect(mockCollection.insertMany).toHaveBeenCalledWith({ + refId, + gPicture, + name, + timeZone: "UTC", + }); + }); + + it("should update profile with new timezone if full rewrite is requested", async () => { + mockCollection.findOne.mockResolvedValue({ refId, timeZone: "Old/Zone" }); + + await updateUserProfile({ refId, gPicture, name, timeZone: "New/Zone" }, true); + + expect(mockCollection.updateOne).toHaveBeenCalledWith( + { refId }, + { gPicture, name, timeZone: "New/Zone" } + ); + }); + + it("should set timezone if user exists but has no timezone set (login flow)", async () => { + // simulate existing user with no timezone + mockCollection.findOne.mockResolvedValue({ refId }); + + await updateUserProfile({ refId, gPicture, name, timeZone: "New/Zone" }, false); + + expect(mockCollection.updateOne).toHaveBeenCalledWith( + { refId }, + { $set: { timeZone: "New/Zone" } } + ); + }); + + it("should NOT update timezone if user already has one (login flow)", async () => { + // simulate valid existing timezone + mockCollection.findOne.mockResolvedValue({ refId, timeZone: "Existing/Zone" }); + + await updateUserProfile({ refId, gPicture, name, timeZone: "New/Zone" }, false); + + // Should NOT call updateOne for timezone + expect(mockCollection.updateOne).not.toHaveBeenCalledWith( + { refId }, + { $set: { timeZone: "New/Zone" } } + ); + // It might call other updates if we had passed them differently, but based on current logic: + // if rewrite=false, it only checks 'else if (timeZone...)'. + // Since it has existing timezone, it enters NO block. + expect(mockCollection.updateOne).not.toHaveBeenCalled(); + }); + + it("should not update timezone if none provided", async () => { + mockCollection.findOne.mockResolvedValue({ refId, timeZone: "Existing/Zone" }); + + await updateUserProfile({ refId, gPicture, name }, false); + + expect(mockCollection.updateOne).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/modules/schedule/__tests__/service.test.ts b/server/src/modules/schedule/__tests__/service.test.ts index d282e9f..8fdf73e 100644 --- a/server/src/modules/schedule/__tests__/service.test.ts +++ b/server/src/modules/schedule/__tests__/service.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, jest, beforeEach, afterEach } from "@jest/globals import { ScheduleService } from "../service"; import schedule from "node-schedule"; import { getCollection } from "@modular-rest/server"; +import parser from "cron-parser"; // Mock modular-rest/server jest.mock("@modular-rest/server", () => ({ @@ -11,6 +12,15 @@ jest.mock("@modular-rest/server", () => ({ Permission: class { }, })); +// Mock cron-parser +jest.mock("cron-parser", () => ({ + parseExpression: jest.fn(() => ({ + prev: () => ({ + toDate: () => new Date(Date.now() - 1000) // Default to 1 second ago for tests + }) + })) +})); + // Mock node-schedule jest.mock("node-schedule", () => ({ scheduleJob: jest.fn(), @@ -96,6 +106,22 @@ describe("ScheduleService", () => { ); expect(schedule.scheduleJob).toHaveBeenCalled(); }); + + it("should save timezone to database", async () => { + mockCollection.findOne.mockResolvedValue(null); + const options = { + cronExpression: "* * * * *", + functionId: "test-fn", + timeZone: "Asia/Tokyo" + }; + + await ScheduleService.createJob("tz-create-job", "test-fn", options); + + expect(mockCollection.create).toHaveBeenCalledWith(expect.objectContaining({ + name: "tz-create-job", + timeZone: "Asia/Tokyo" + })); + }); }); describe("deleteJob", () => { @@ -350,5 +376,69 @@ describe("ScheduleService", () => { executedTime: expect.any(Date) })); }); + + it("should use timezone when calculating past occurrences", async () => { + const pastDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); + const mockJobs = [ + { + name: "catchup-tz-job", + cronExpression: "0 9 */1 * *", + functionId: "fn", + jobType: "recurrent", + catchUp: true, + lastRun: pastDate, + createdAt: pastDate, + timeZone: "America/New_York" + } + ]; + mockCollection.find.mockResolvedValue(mockJobs); + mockCollection.findOneAndUpdate.mockResolvedValue(null); + + await ScheduleService.init(); + + expect(parser.parseExpression).toHaveBeenCalledWith( + "0 9 */1 * *", + expect.objectContaining({ tz: "America/New_York" }) + ); + }); + }); + + describe("TimeZone Support", () => { + it("should schedule job with timezone if provided", async () => { + const jobData = { + name: "tz-job", + functionId: "fn", + cronExpression: "0 0 12 * * *", + timeZone: "America/New_York", + state: "scheduled", + jobType: "recurrent", + }; + + await ScheduleService.scheduleJobInternal(jobData); + + expect(schedule.scheduleJob).toHaveBeenCalledWith( + "tz-job", + { rule: "0 0 12 * * *", tz: "America/New_York" }, + expect.any(Function) + ); + }); + + it("should schedule normal cron string if no timezone provided", async () => { + const jobData = { + name: "no-tz-job", + functionId: "fn", + cronExpression: "0 0 12 * * *", + state: "scheduled", + jobType: "recurrent", + }; + + await ScheduleService.scheduleJobInternal(jobData); + + expect(schedule.scheduleJob).toHaveBeenCalledWith( + "no-tz-job", + "0 0 12 * * *", + expect.any(Function) + ); + }); }); }); diff --git a/server/src/modules/schedule/db.ts b/server/src/modules/schedule/db.ts index d237a98..5d84a52 100644 --- a/server/src/modules/schedule/db.ts +++ b/server/src/modules/schedule/db.ts @@ -12,6 +12,7 @@ interface ScheduleJobSchema { lastRun?: Date; state: "scheduled" | "queued" | "executing" | "executed" | "failed"; catchUp?: boolean; + timeZone?: string; } const scheduleJobSchema = new Schema( @@ -38,6 +39,7 @@ const scheduleJobSchema = new Schema( default: "scheduled", }, catchUp: { type: Boolean, default: false }, + timeZone: { type: String }, }, { timestamps: true } ); diff --git a/server/src/modules/schedule/service.ts b/server/src/modules/schedule/service.ts index 650faeb..75940e0 100644 --- a/server/src/modules/schedule/service.ts +++ b/server/src/modules/schedule/service.ts @@ -202,7 +202,7 @@ export class ScheduleService { if (job.jobType === "recurrent" && shouldCatchUp && job.cronExpression) { try { - const interval = parser.parseExpression(job.cronExpression); + const interval = parser.parseExpression(job.cronExpression, job.timeZone ? { tz: job.timeZone } : undefined); const lastOccurrence = interval.prev().toDate(); const lastRun = job.lastRun ? new Date(job.lastRun) : new Date(job.createdAt);