From 08904af046a95d93f57052762036acdcb169d5ec Mon Sep 17 00:00:00 2001 From: Motzik20 Date: Mon, 5 Jan 2026 12:10:52 +0100 Subject: [PATCH] fix: make scheduled_objects timezone aware, and reschedule on timezone change --- backend/app/api/routers/settings.py | 8 -- backend/app/services/settings_service.py | 73 ++++++++++++++++++- .../components/schedule/ScheduleItemsList.tsx | 6 +- frontend/src/lib/format-dt.ts | 3 + 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/backend/app/api/routers/settings.py b/backend/app/api/routers/settings.py index 4f6f1cb..1e24cc6 100644 --- a/backend/app/api/routers/settings.py +++ b/backend/app/api/routers/settings.py @@ -3,7 +3,6 @@ from app.core.auth import get_current_user_id from app.core.db import get_db -from app.schemas.availability import WeeklyAvailabilityUpdate from app.schemas.user import AnySettingOut, SettingUpdate from app.services import settings_service @@ -34,11 +33,4 @@ async def update_settings( session: Session = Depends(get_db), ) -> AnySettingOut: """Update a user setting.""" - if setting.type == "schedule": - availability_update = WeeklyAvailabilityUpdate.model_validate( - {"windows": setting.value} - ) - return settings_service.update_availability_setting( - user_id, availability_update, session - ) return settings_service.update_setting(user_id, setting, session) diff --git a/backend/app/services/settings_service.py b/backend/app/services/settings_service.py index e207108..0e52763 100644 --- a/backend/app/services/settings_service.py +++ b/backend/app/services/settings_service.py @@ -14,6 +14,7 @@ BooleanSettingOut, BooleanSettingUpdate, ScheduleSettingOut, + ScheduleSettingUpdate, StringSettingOut, StringSettingUpdate, UserSettingsOut, @@ -87,10 +88,20 @@ def get_all_user_settings(user_id: int, session: Session) -> UserSettingsOut: def update_setting( user_id: int, - setting: StringSettingUpdate | BooleanSettingUpdate, + setting: StringSettingUpdate | BooleanSettingUpdate | ScheduleSettingUpdate, session: Session, ) -> AnySettingOut: """Update a setting and return the updated setting as output schema.""" + if setting.type == "schedule": + availability_update = WeeklyAvailabilityUpdate.model_validate( + {"windows": setting.value} + ) + return update_availability_setting(user_id, availability_update, session) + elif setting.key == "timezone": + old_timezone = setting_crud.get_user_setting(user_id, "timezone", session).value + new_timezone = setting.value + if old_timezone != new_timezone: + _reschedule(user_id, new_timezone, session) updated_model = setting_crud.update_user_setting(user_id, setting, session) return model_to_setting_out(updated_model) @@ -108,6 +119,66 @@ def update_availability_setting( return availability_to_setting_out(av_schema) +def _reschedule(user_id: int, new_timezone: str, session: Session): + from sqlmodel import select + + from app.core.timezone import now_utc + from app.crud import availability_crud, schedule_item_crud, setting_crud, task_crud + from app.models.schedule_item import ScheduleItem + from app.services.greedy_scheduler import GreedyScheduler + from app.services.protocols import ChronoScheduler + from app.services.scheduling_utils import schedule_blocks_to_schedule_items + + # Get all scheduled tasks + scheduled_tasks = task_crud.get_scheduled_tasks(user_id, session) + + if not scheduled_tasks: + return # No tasks to reschedule + + task_ids = [task.id for task in scheduled_tasks if task.id is not None] + + existing_schedule_items = session.exec( + select(ScheduleItem) + .where(ScheduleItem.user_id == user_id) + .where(ScheduleItem.task_id.in_(task_ids)) # type: ignore[attr-defined] + .where(ScheduleItem.source == "task") + ).all() + + for item in existing_schedule_items: + session.delete(item) + + session.flush() + + availability = availability_crud.get_user_availability(user_id, session) + schedule_config = setting_crud.get_schedule_config(user_id, session) + schedule_config.timezone = new_timezone + + all_schedule_items = schedule_item_crud.get_user_schedule_items(user_id, session) + + scheduler: ChronoScheduler = GreedyScheduler() + response = scheduler.schedule_tasks( + scheduled_tasks, + all_schedule_items, + availability, + schedule_config, + ) + + if response.schedule_blocks: + schedule_items_to_create = schedule_blocks_to_schedule_items( + response.schedule_blocks, user_id + ) + schedule_item_crud.create_schedule_items(schedule_items_to_create, session) + + task_crud.update_tasks_scheduled_at( + task_ids, + now_utc(), + user_id, + session, + ) + + session.commit() + + def get_setting_options(key: str) -> list[dict[str, str]] | None: """Get options for a setting key. Returns None if no options available.""" from app.services.option_factory_service import OPTION_FACTORIES diff --git a/frontend/src/components/schedule/ScheduleItemsList.tsx b/frontend/src/components/schedule/ScheduleItemsList.tsx index 5281620..ff1d0ee 100644 --- a/frontend/src/components/schedule/ScheduleItemsList.tsx +++ b/frontend/src/components/schedule/ScheduleItemsList.tsx @@ -17,7 +17,7 @@ import { import { apiDownloadRequest, apiRequest, ApiError } from "@/lib/chrono-client"; import { useSchedule } from "@/context/schedule-context"; import { toast } from "sonner"; -import { formatDuration, formatFloatingTime } from "@/lib/format-dt"; +import { formatDateTime, formatDuration } from "@/lib/format-dt"; export default function ScheduleItemsList() { const [scheduleItems, setScheduleItems] = useState([]); @@ -137,8 +137,8 @@ export default function ScheduleItemsList() {
- {formatFloatingTime(item.start_time)} -{" "} - {formatFloatingTime(item.end_time)} + {formatDateTime(item.start_time, item.user_timezone)} -{" "} + {formatDateTime(item.end_time, item.user_timezone)}
diff --git a/frontend/src/lib/format-dt.ts b/frontend/src/lib/format-dt.ts index 2f54c97..d64deb2 100644 --- a/frontend/src/lib/format-dt.ts +++ b/frontend/src/lib/format-dt.ts @@ -21,6 +21,8 @@ export const formatDuration = (startTime: string, endTime: string) => { return mins > 0 ? `${hours}h ${mins}min` : `${hours}h`; }; +/** +TODO: this is more of a hack, for traveling its better to recalculate the time in the database. export const formatFloatingTime = (dateString: string) => { const literalTime = dateString.slice(0, 19); @@ -31,3 +33,4 @@ export const formatFloatingTime = (dateString: string) => { timeStyle: "short", }); }; +*/