diff --git a/docs/datetime.md b/docs/datetime.md index 62e85c7..f479b2d 100644 --- a/docs/datetime.md +++ b/docs/datetime.md @@ -8,12 +8,12 @@ We are following these priciples: - The Back-End only ever handles UTC times. - When working with schedules we are displaying local time without daylight saving adjustments. -- Once we create an appointment, we save it using the respective date and time in UTC. -- Once we display appointments, we convert from UTC to local time incl. daylight saving adjustments. +- Once we select a slot for an appointment, we save it using the respective date and time in UTC incl. daylight saving adjustments. +- When we display appointments, we convert from UTC to local time incl. daylight saving adjustments. ## How it works -> This currently only works for the northern himsphere because we use January as a reference date (see `const jan = new Date`). +> This currently only works for the northern himsphere because we use January as a reference date (see `const jan = new Date`). We will improve this, when needed. 1. Our Back-End will only every take and return dates using UTC (no daylight saving there) 1. When managing slots for channels @@ -21,7 +21,7 @@ We are following these priciples: - we will save times without adjusting for daylight saving 1. When booking appointments - we will show time in local time (with or without daylight saving for the respective date) - - we will save appointments using UTC (effectivly not touching the time during booking process) + - we will save appointments using UTC (converting it to local time and then to utc) 1. When looking a appointments in the calendar we will show local time (with or without daylight saving). 1. Client Dashboard converts to localtime (with or without daylight saving). 1. E-Mails going out about appointments include the timezone of the user, so the proper time can be shown. diff --git a/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte b/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte index b8f6242..f629082 100644 --- a/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/notification-item.svelte @@ -3,7 +3,6 @@ import { resolve } from "$app/paths"; import { page } from "$app/state"; import { m } from "$i18n/messages"; - import { getLocale } from "$i18n/runtime"; import { Button } from "$lib/components/ui/button"; import { Text } from "$lib/components/ui/typography"; import { ROUTES } from "$lib/const/routes"; @@ -11,7 +10,7 @@ import { channels as channelsStore } from "$lib/stores/channels"; import type { TAppointment } from "$lib/types/appointments"; import type { TNotification } from "$lib/types/notification"; - import { utcToLocalWithoutDST } from "$lib/utils/datetime"; + import { toDisplayDateTime } from "$lib/utils/datetime"; import { getCurrentTranlslation } from "$lib/utils/localizations"; import { getLocalTimeZone } from "@internationalized/date"; import { onMount } from "svelte"; @@ -82,15 +81,15 @@ style="xs" class="text-muted-foreground -mb-1 flex pr-8 text-start font-light whitespace-break-spaces" > - {Intl.DateTimeFormat(getLocale(), { + {toDisplayDateTime(new Date(appointment.appointmentDate), { year: "numeric", - month: "short", - day: "numeric", - weekday: "short", + month: "2-digit", + day: "2-digit", hour: "2-digit", minute: "2-digit", - timeZone: getLocalTimeZone().toString(), - }).format(utcToLocalWithoutDST(new Date(appointment.appointmentDate)))} + timeZone: getLocalTimeZone(), + timeZoneName: "short", + })} {/if} diff --git a/src/lib/components/ui/public/appointment-card.svelte b/src/lib/components/ui/public/appointment-card.svelte index 8515fbe..bdf1f15 100644 --- a/src/lib/components/ui/public/appointment-card.svelte +++ b/src/lib/components/ui/public/appointment-card.svelte @@ -10,6 +10,7 @@ import { resest } from "../../../../routes/(pages)/(clients)/book-appointment/[[id]]/(components)/utils"; import { Button } from "../button"; import LocalizedText from "./localized-text.svelte"; + import { toDisplayDateTime } from "$lib/utils/datetime"; let { class: className, @@ -75,14 +76,7 @@
- {new Intl.DateTimeFormat($publicStore.locale, { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - timeZone: "Etc/GMT-1", - }).format(appointment.slot.datetime.toDate("UTC"))} + {toDisplayDateTime(appointment.slot.datetime.toDate("UTC"))}
{/if} diff --git a/src/lib/emails/AppointmentBooked.svelte b/src/lib/emails/AppointmentBooked.svelte index b683f98..93e20fc 100644 --- a/src/lib/emails/AppointmentBooked.svelte +++ b/src/lib/emails/AppointmentBooked.svelte @@ -50,7 +50,6 @@ {appointment.agentName}
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)} - {m["emails.oclock"]()} {tenant.longName} diff --git a/src/lib/emails/AppointmentCancelled.svelte b/src/lib/emails/AppointmentCancelled.svelte index df8ea47..f5e884f 100644 --- a/src/lib/emails/AppointmentCancelled.svelte +++ b/src/lib/emails/AppointmentCancelled.svelte @@ -47,7 +47,6 @@ {appointment.agentName}
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)} - {m["emails.oclock"]()}
{tenant.longName} diff --git a/src/lib/emails/AppointmentRejected.svelte b/src/lib/emails/AppointmentRejected.svelte index a2b496a..106060e 100644 --- a/src/lib/emails/AppointmentRejected.svelte +++ b/src/lib/emails/AppointmentRejected.svelte @@ -47,7 +47,6 @@ {appointment.agentName}
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)} - {m["emails.oclock"]()}
{tenant.longName} diff --git a/src/lib/emails/AppointmentReminder.svelte b/src/lib/emails/AppointmentReminder.svelte index ca17a30..1db9bb8 100644 --- a/src/lib/emails/AppointmentReminder.svelte +++ b/src/lib/emails/AppointmentReminder.svelte @@ -50,7 +50,6 @@ {appointment.agentName}
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)} - {m["emails.oclock"]()}
{tenant.longName} diff --git a/src/lib/emails/AppointmentRequest.svelte b/src/lib/emails/AppointmentRequest.svelte index 404be16..da948ac 100644 --- a/src/lib/emails/AppointmentRequest.svelte +++ b/src/lib/emails/AppointmentRequest.svelte @@ -47,7 +47,6 @@ {appointment.agentName}
{renderAppointmentDate(appointment.appointmentDate, locale, appointment.timezone)}
{renderAppointmentTime(appointment.appointmentDate, locale, appointment.timezone)} - {m["emails.oclock"]()}
{tenant.longName} diff --git a/src/lib/utils/datetime.ts b/src/lib/utils/datetime.ts index d420cc3..2e04464 100644 --- a/src/lib/utils/datetime.ts +++ b/src/lib/utils/datetime.ts @@ -1,16 +1,35 @@ import { getLocale } from "$i18n/runtime"; import type { TCalendarSlot } from "$lib/types/calendar"; -import { parseAbsoluteToLocal, toCalendarDateTime } from "@internationalized/date"; +import { + getLocalTimeZone, + parseAbsoluteToLocal, + toCalendarDateTime, +} from "@internationalized/date"; -export const toDisplayDateTime = (date: Date, opts?: Intl.DateTimeFormatOptions) => { +export const toDisplayDateTime = ( + date: Date, + opts: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZone: getLocalTimeZone(), + timeZoneName: "short", + }, +) => { const hours = date.getHours(); const minutes = date.getMinutes(); const formatter = new Intl.DateTimeFormat( getLocale(), - opts ?? { + opts || { + // TODO: Should be moved outside, in cases where needed dateStyle: "short", - ...(hours !== 0 || minutes !== 0 ? { timeStyle: "short" } : {}), - timeZone: "Etc/GMT-1", + ...(hours !== 0 || minutes !== 0 + ? { timeStyle: "short" } + : { hour: "2-digit", minute: "2-digit" }), + timeZone: getLocalTimeZone(), + timeZoneName: "short", }, ); return formatter.format(date); @@ -155,3 +174,13 @@ export const localToUTCWithoutDST = (localDate: Date): Date => { return new Date(localDate.getTime() + standardOffsetMs - currentOffsetMs); }; + +export const localToUTC = (localDate: Date): Date => { + const currentOffsetMs = localDate.getTimezoneOffset() * 60 * 1000; + return new Date(localDate.getTime() + currentOffsetMs); +}; + +export const utcToLocal = (utcDate: Date): Date => { + const currentOffsetMs = utcDate.getTimezoneOffset() * 60 * 1000; + return new Date(utcDate.getTime() - currentOffsetMs); +}; diff --git a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/select-slot.svelte b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/select-slot.svelte index 3aeb841..96a4b9d 100644 --- a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/select-slot.svelte +++ b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/select-slot.svelte @@ -11,6 +11,7 @@ import type { TPublicAppointment, TPublicSchedule, TPublicSlot } from "$lib/types/public.js"; import { CalendarDate, + fromDate, getLocalTimeZone, parseDate, toCalendarDate, @@ -22,7 +23,11 @@ import type { DateMatcher } from "bits-ui"; import type { OnChangeFn } from "vaul-svelte"; import { fetchSchedule } from "./utils"; - import { timeUTCToLocalWithoutOffset } from "$lib/utils/datetime"; + import { + localToUTC, + timeUTCToLocalWithoutOffset, + utcToLocalWithoutDST, + } from "$lib/utils/datetime"; const { channel, @@ -137,6 +142,8 @@ const selectSlot = (slot: TPublicSlot) => { if (selectedDate && slot.availableAgents.length > 0) { + const localTime = utcToLocalWithoutDST(new Date(slot.from)); + const utc = localToUTC(localTime); proceed({ ...appointment, agent: { @@ -145,10 +152,7 @@ image: slot.availableAgents[0].image || null, }, slot: { - datetime: toCalendarDateTime(selectedDate).set({ - hour: new Date(slot.from).getUTCHours(), - minute: new Date(slot.from).getUTCMinutes(), - }), + datetime: toCalendarDateTime(fromDate(utc, getLocalTimeZone())), duration: slot.duration, }, }); diff --git a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/summary.svelte b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/summary.svelte index 4cd2985..24b2ba4 100644 --- a/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/summary.svelte +++ b/src/routes/(pages)/(clients)/book-appointment/[[id]]/(components)/summary.svelte @@ -7,7 +7,6 @@ import { Text } from "$lib/components/ui/typography"; import { ROUTES } from "$lib/const/routes"; import { publicStore } from "$lib/stores/public.js"; - import { toZoned } from "@internationalized/date"; import { toast } from "svelte-sonner"; const tenant = $derived($publicStore.tenant); @@ -30,7 +29,7 @@ $publicStore.crypto .createAppointment( appointment.data, - toZoned(appointment.slot.datetime, "UTC").toAbsoluteString(), + appointment.slot.datetime.toDate("UTC").toISOString(), appointment.agent.id, channel.id, appointment.slot.duration, diff --git a/src/routes/(pages)/(clients)/clients/(components)/appointment-list.svelte b/src/routes/(pages)/(clients)/clients/(components)/appointment-list.svelte index 41a7da0..4564a28 100644 --- a/src/routes/(pages)/(clients)/clients/(components)/appointment-list.svelte +++ b/src/routes/(pages)/(clients)/clients/(components)/appointment-list.svelte @@ -82,15 +82,7 @@ {/if} - {toDisplayDateTime(utcToLocalWithoutDST(new Date(item.appointmentDate)), { - year: "numeric", - month: "long", - day: "numeric", - weekday: "short", - hour: "2-digit", - minute: "2-digit", - timeZone: getLocalTimeZone().toString(), - })} + {toDisplayDateTime(new Date(item.appointmentDate))} diff --git a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte index 0cc0c92..71df577 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/AppointmentDetail.svelte @@ -1,13 +1,12 @@