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 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-11 13:43:31 +00:00
parent df4031d99c
commit b93b8d2e71
10 changed files with 899 additions and 87 deletions

View File

@@ -5,7 +5,7 @@
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="loading && !confirmModal.show" class="loading">Loading calendar...</div>
<FullCalendar v-if="!loading" :options="calendarOptions" />
<FullCalendar v-show="!loading" :options="calendarOptions" />
<!-- Confirmation Modal -->
<div v-if="confirmModal.show" class="modal-overlay" @click.self="cancelReschedule">
@@ -39,7 +39,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed } from 'vue'
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
@@ -47,6 +47,7 @@ import interactionPlugin from '@fullcalendar/interaction'
import type { CalendarOptions, EventInput, DatesSetArg, EventDropArg, EventResizeDoneArg } from '@fullcalendar/core'
import { bookingsApi, adminBookingsApi, handleApiError } from '@/services/api'
import { useAuthStore } from '@/stores/auth'
import { formatDateTime as formatDateTimeUtil } from '@/utils/datetime'
import type { Booking } from '@/types'
interface Props {
@@ -56,8 +57,10 @@ interface Props {
const props = defineProps<Props>()
const authStore = useAuthStore()
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
const bookings = ref<Booking[]>([])
const loading = ref(false)
const loading = ref(true)
const initialLoad = ref(true)
const modalLoading = ref(false)
const error = ref('')
@@ -206,18 +209,17 @@ const cancelReschedule = () => {
// Format datetime for display
const formatDateTime = (date: Date | null) => {
if (!date) return ''
return date.toLocaleString('ro-RO', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
return formatDateTimeUtil(date.toISOString(), userTimezone.value)
}
// Track current date range for refresh
let currentStart: Date | null = null
let currentEnd: Date | null = null
// Load bookings for a date range
const loadBookings = async (start: Date, end: Date) => {
loading.value = true
currentStart = start
currentEnd = end
error.value = ''
try {
@@ -227,7 +229,10 @@ const loadBookings = async (start: Date, end: Date) => {
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
if (initialLoad.value) {
loading.value = false
initialLoad.value = false
}
}
}
@@ -245,6 +250,7 @@ const calendarOptions = computed<CalendarOptions>(() => ({
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
timeZone: userTimezone.value,
events: events.value,
datesSet: handleDatesSet,
editable: isEditable.value, // Enable drag/resize for admins
@@ -283,13 +289,19 @@ const calendarOptions = computed<CalendarOptions>(() => ({
}
}))
// Load initial bookings on mount
onMounted(() => {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
loadBookings(startOfMonth, endOfMonth)
})
// Public refresh method for parent components
const refresh = () => {
if (currentStart && currentEnd) {
loadBookings(currentStart, currentEnd)
} else {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
loadBookings(startOfMonth, endOfMonth)
}
}
defineExpose({ refresh })
</script>
<style scoped>