From b93b8d2e71b8111a6d475dfec966d5e5f538572b Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 11 Feb 2026 13:43:31 +0000 Subject: [PATCH] fix: implement timezone-aware datetime display and editing All datetime values are stored in UTC but were displaying raw UTC times to users, causing confusion (e.g., 10:00 Bucharest showing as 08:00). This implements proper timezone conversion throughout the app using each user's profile timezone setting. Changes: - Frontend: Replace local formatters with timezone-aware utilities - Backend: Add timezone conversion to PUT /bookings endpoint - FullCalendar: Configure to display events in user timezone - Fix edit modal to preserve times when editing bookings Co-Authored-By: Claude Sonnet 4.5 --- backend/app/api/bookings.py | 19 +- frontend/src/components/AttachmentsList.vue | 9 +- frontend/src/components/BookingForm.vue | 8 +- frontend/src/components/SpaceCalendar.vue | 50 +- frontend/src/utils/datetime.ts | 2 +- frontend/src/views/AdminPending.vue | 25 +- frontend/src/views/AuditLog.vue | 14 +- frontend/src/views/Dashboard.vue | 823 +++++++++++++++++++- frontend/src/views/MyBookings.vue | 29 +- frontend/src/views/UserProfile.vue | 7 +- 10 files changed, 899 insertions(+), 87 deletions(-) diff --git a/backend/app/api/bookings.py b/backend/app/api/bookings.py index c665557..da018c5 100644 --- a/backend/app/api/bookings.py +++ b/backend/app/api/bookings.py @@ -467,9 +467,20 @@ def update_booking( detail="Can only edit pending bookings", ) + # Convert input times from user timezone to UTC + user_timezone = current_user.timezone or "UTC" # type: ignore[attr-defined] + # Prepare updated values (don't update model yet - validate first) - updated_start = data.start_datetime if data.start_datetime is not None else booking.start_datetime # type: ignore[assignment] - updated_end = data.end_datetime if data.end_datetime is not None else booking.end_datetime # type: ignore[assignment] + # Convert datetimes to UTC if provided + if data.start_datetime is not None: + updated_start = convert_to_utc(data.start_datetime, user_timezone) # type: ignore[assignment] + else: + updated_start = booking.start_datetime # type: ignore[assignment] + + if data.end_datetime is not None: + updated_end = convert_to_utc(data.end_datetime, user_timezone) # type: ignore[assignment] + else: + updated_end = booking.end_datetime # type: ignore[assignment] # Re-validate booking rules BEFORE updating the model user_id = int(current_user.id) # type: ignore[arg-type] @@ -494,9 +505,9 @@ def update_booking( if data.description is not None: booking.description = data.description # type: ignore[assignment] if data.start_datetime is not None: - booking.start_datetime = data.start_datetime # type: ignore[assignment] + booking.start_datetime = convert_to_utc(data.start_datetime, user_timezone) # type: ignore[assignment] if data.end_datetime is not None: - booking.end_datetime = data.end_datetime # type: ignore[assignment] + booking.end_datetime = convert_to_utc(data.end_datetime, user_timezone) # type: ignore[assignment] db.commit() db.refresh(booking) diff --git a/frontend/src/components/AttachmentsList.vue b/frontend/src/components/AttachmentsList.vue index 3a42a0d..4499a54 100644 --- a/frontend/src/components/AttachmentsList.vue +++ b/frontend/src/components/AttachmentsList.vue @@ -42,8 +42,10 @@ diff --git a/frontend/src/views/MyBookings.vue b/frontend/src/views/MyBookings.vue index 8e7b403..00e2bad 100644 --- a/frontend/src/views/MyBookings.vue +++ b/frontend/src/views/MyBookings.vue @@ -191,7 +191,7 @@ import { ref, computed, onMounted } from 'vue' import { bookingsApi, handleApiError } from '@/services/api' import { useAuthStore } from '@/stores/auth' -import { formatDate as formatDateTZ, formatTime as formatTimeTZ } from '@/utils/datetime' +import { formatDate as formatDateTZ, formatTime as formatTimeTZ, isoToLocalDateTime, localDateTimeToISO } from '@/utils/datetime' import type { Booking } from '@/types' const authStore = useAuthStore() @@ -237,18 +237,6 @@ const formatTime = (start: string, end: string): string => { return `${startTime} - ${endTime}` } -const formatTimeOld = (start: string, end: string): string => { - const startDate = new Date(start) - const endDate = new Date(end) - const formatTimeOnly = (date: Date) => - date.toLocaleTimeString('en-GB', { - hour: '2-digit', - minute: '2-digit', - hour12: false - }) - return `${formatTimeOnly(startDate)} - ${formatTimeOnly(endDate)}` -} - const formatType = (type: string): string => { const typeMap: Record = { sala: 'Sala', @@ -263,15 +251,12 @@ const formatStatus = (status: string): string => { const openEditModal = (booking: Booking) => { editingBooking.value = booking - // Convert ISO datetime to datetime-local format (YYYY-MM-DDTHH:MM) - const start = new Date(booking.start_datetime) - const end = new Date(booking.end_datetime) - + // Convert ISO datetime to datetime-local format in user's timezone editForm.value = { title: booking.title, description: booking.description || '', - start_datetime: start.toISOString().slice(0, 16), - end_datetime: end.toISOString().slice(0, 16) + start_datetime: isoToLocalDateTime(booking.start_datetime, userTimezone.value), + end_datetime: isoToLocalDateTime(booking.end_datetime, userTimezone.value) } editError.value = '' showEditModal.value = true @@ -290,12 +275,12 @@ const saveEdit = async () => { editError.value = '' try { - // Convert datetime-local format back to ISO + // Convert datetime-local format to ISO without Z (backend will handle timezone conversion) const updateData = { title: editForm.value.title, description: editForm.value.description, - start_datetime: new Date(editForm.value.start_datetime).toISOString(), - end_datetime: new Date(editForm.value.end_datetime).toISOString() + start_datetime: localDateTimeToISO(editForm.value.start_datetime), + end_datetime: localDateTimeToISO(editForm.value.end_datetime) } await bookingsApi.update(editingBooking.value.id, updateData) diff --git a/frontend/src/views/UserProfile.vue b/frontend/src/views/UserProfile.vue index e7249d8..40539a9 100644 --- a/frontend/src/views/UserProfile.vue +++ b/frontend/src/views/UserProfile.vue @@ -127,12 +127,14 @@