diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarDay.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarDay.svelte index f97d1cb..8354d2d 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarDay.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarDay.svelte @@ -127,6 +127,7 @@
{#if isWeekView && items} + {@const isSelected = selectedDate.toString() === day.toString()} {@const isToday = toCalendarDate(day).toString() === today(getLocalTimeZone()).toString()} + + {/if}
diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte index 03ce440..6875ad3 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte @@ -195,9 +195,9 @@ class="mt-2 mb-1" > {#snippet label()} -
+
{name} @@ -244,6 +244,7 @@ changeView("week"); } }} + class="min-h-82" /> diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarLegend.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarLegend.svelte index 3589799..d00f7b5 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarLegend.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarLegend.svelte @@ -12,6 +12,7 @@ today, } from "@internationalized/date"; import Loader from "@lucide/svelte/icons/loader-2"; + import { isAfterOperatingHours, isBeforeOperatingHours } from "./utils"; let { day = $bindable(), @@ -32,6 +33,8 @@ const curTimeIndicator = $derived( today(getLocalTimeZone()).toString() === day.toString() ? $clock : undefined, ); + const isBeforeHours = $derived(isBeforeOperatingHours(curTimeIndicator, earliestStartHour)); + const isAfterHours = $derived(isAfterOperatingHours(curTimeIndicator, latestEndHour));
@@ -50,14 +53,10 @@
{/if} - + {#if !isLoading && curTimeIndicator} {@const isToday = toCalendarDate($clock).toString() === today(getLocalTimeZone()).toString()} - {@const isNotAfterHours = - latestEndHour * hourSize + hourSize / 2 > curTimeIndicator.hour * hourSize} - {@const isNotBeforeHours = - earliestStartHour * hourSize - hourSize * 2 < curTimeIndicator.hour * hourSize} - {#if isToday && isNotAfterHours && isNotBeforeHours} + {#if isToday && !isAfterHours && !isBeforeHours} {@const top = focusAdjustment + curTimeIndicator.hour * hourSize + diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarLines.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarLines.svelte index e858dd6..adb7a9d 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarLines.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarLines.svelte @@ -10,6 +10,7 @@ toCalendarDateTime, today, } from "@internationalized/date"; + import { isAfterOperatingHours, isBeforeOperatingHours } from "./utils"; let { day = $bindable(), @@ -32,10 +33,13 @@ const curTimeIndicator = $derived( today(getLocalTimeZone()).toString() === day.toString() ? $clock : undefined, ); + const isBeforeHours = $derived(isBeforeOperatingHours(curTimeIndicator, earliestStartHour)); + const isAfterHours = $derived(isAfterOperatingHours(curTimeIndicator, latestEndHour)); {#if !isLoading}
+
- + {#if !isLoading && curTimeIndicator} {@const isToday = toCalendarDate($clock).toString() === today(getLocalTimeZone()).toString()} - {@const isNotAfterHours = - latestEndHour * hourSize + hourSize / 2 > curTimeIndicator.hour * hourSize} - {@const isNotBeforeHours = - earliestStartHour * hourSize - hourSize * 2 < curTimeIndicator.hour * hourSize} - {#if isToday && isNotAfterHours && isNotBeforeHours} + {#if isToday && !isAfterHours && !isBeforeHours} {@const top = focusAdjustment + curTimeIndicator.hour * hourSize + diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte index 32b40bf..5a3dec2 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte @@ -19,12 +19,14 @@ import { type ComponentProps } from "svelte"; import type { OnChangeFn } from "vaul-svelte"; import { calendarMonthQuery } from "./queries"; + import { cn } from "$lib/utils"; let { selectedDate = $bindable(), shownAppointments, shownChannels, shownAgents, + class: className, onSelectDay, ref = $bindable(null), }: ComponentProps & { @@ -32,6 +34,7 @@ shownAppointments: TAppointmentFilter; shownChannels: string[]; shownAgents: string[]; + class?: string; onSelectDay?: OnChangeFn; } = $props(); @@ -176,7 +179,7 @@ type="single" locale={getLocale()} calendarLabel={m["calendar.selectDate"]()} - class="bg-transparent p-0 [&_td]:grow [&_td_*]:mx-auto [&_th]:grow" + class={cn("bg-transparent p-0 [&_td]:grow [&_td_*]:mx-auto [&_th]:grow", className)} preventDeselect={true} bind:value={selectedDate} bind:placeholder diff --git a/src/routes/(pages)/dashboard/calendar/(components)/utils.ts b/src/routes/(pages)/dashboard/calendar/(components)/utils.ts index d866479..3dc0222 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/utils.ts +++ b/src/routes/(pages)/dashboard/calendar/(components)/utils.ts @@ -9,10 +9,15 @@ import { staffCrypto } from "$lib/stores/staff-crypto"; import type { TCalendar, TCalendarItem } from "$lib/types/calendar"; import type { TChannel } from "$lib/types/channel"; import { serverAppointmentStatusToUiFilterStatus } from "$lib/utils/appointments"; -import { getWeekStartsOn, localToUTCWithoutDST } from "$lib/utils/datetime"; +import { + getWeekStartsOn, + localToUTCWithoutDST, + timeUTCToLocalWithoutOffset, +} from "$lib/utils/datetime"; import { getLocalTimeZone, parseAbsoluteToLocal, + parseAbsolute, toCalendarDate, type CalendarDate, } from "@internationalized/date"; @@ -174,6 +179,81 @@ export function positionItems(items: TCalendarItem[] | undefined) { return processedItems; } +export const getOperatingHours = (channels: TChannel[], calendar: TCalendar | undefined) => { + const earliestSlotStartHour = channels + .map((c) => c.slotTemplates.map((t) => t.from)) + .flat() + .map((time) => { + const [hourStr] = timeUTCToLocalWithoutOffset(time).split(":"); + return parseInt(hourStr, 10); + }); + const lastSlotEndHour = channels + .map((c) => c.slotTemplates.map((t) => t.to)) + .flat() + .map((time) => { + const [hourStr, minuteStr] = timeUTCToLocalWithoutOffset(time).split(":"); + let hour = parseInt(hourStr, 10); + if (minuteStr !== "00") { + hour += 1; + } + return hour; + }); + const earliestAppointmentStartHour = (calendar?.calendar || []) + .map((day) => + Array.from( + Object.values(day.channels) + .map((it) => + it.appointments.map( + (a) => + // @ts-expect-error appointmentDate is not typed correctly + parseAbsolute(a.appointmentDate, getLocalTimeZone()).hour, + ), + ) + .flat(), + ), + ) + .flat(); + const lastAppointmentEndHour = (calendar?.calendar || []) + .map((day) => + Array.from( + Object.values(day.channels) + .map((it) => + it.appointments.map( + (a) => + // @ts-expect-error appointmentDate is not typed correctly + parseAbsolute(a.appointmentDate, getLocalTimeZone()).add({ + minutes: a.duration, + }).hour, + ), + ) + .flat(), + ), + ) + .flat(); + return { + from: Math.min(...earliestSlotStartHour, ...earliestAppointmentStartHour), + to: Math.max(...lastSlotEndHour, ...lastAppointmentEndHour), + }; +}; + +export const isBeforeOperatingHours = ( + curTimeIndicator: { hour: number; minute: number } | undefined, + earliestStartHour: number, +) => { + if (!curTimeIndicator) return true; + const result = earliestStartHour * 60 - 0.5 > curTimeIndicator.hour * 60; + return result; +}; + +export const isAfterOperatingHours = ( + curTimeIndicator: { hour: number; minute: number } | undefined, + latestEndHour: number, +) => { + if (!curTimeIndicator) return true; + const result = latestEndHour * 60 + 30 < curTimeIndicator.hour * 60 + curTimeIndicator.minute; + return result; +}; + export const moveAppointment = async (opts: { tenant: string; appointment: string; diff --git a/src/routes/(pages)/dashboard/calendar/+page.svelte b/src/routes/(pages)/dashboard/calendar/+page.svelte index d42e2a9..a13c4c8 100644 --- a/src/routes/(pages)/dashboard/calendar/+page.svelte +++ b/src/routes/(pages)/dashboard/calendar/+page.svelte @@ -11,7 +11,6 @@ import { channels as channelsStore } from "$lib/stores/channels"; import { sidebar } from "$lib/stores/sidebar"; import type { TAppointmentFilter, TCalendarMode } from "$lib/types/calendar"; - import { timeUTCToLocalWithoutOffset } from "$lib/utils/datetime"; import { getCurrentTranlslation } from "$lib/utils/localizations"; import { getLocalTimeZone, today, type CalendarDate } from "@internationalized/date"; import { SlidersHorizontal } from "@lucide/svelte"; @@ -25,10 +24,10 @@ import CalendarLegend from "./(components)/CalendarLegend.svelte"; import CalendarLines from "./(components)/CalendarLines.svelte"; import CalendarWeek from "./(components)/CalendarWeek.svelte"; - import { calendarMonthQuery } from "./(components)/queries"; - import { convertDate, openAppointmentById } from "./(components)/utils"; - import type { CalendarView } from "./types"; import MoveAppointment from "./(components)/MoveAppointment.svelte"; + import { calendarMonthQuery } from "./(components)/queries"; + import { convertDate, getOperatingHours, openAppointmentById } from "./(components)/utils"; + import type { CalendarView } from "./types"; const queryClient = useQueryClient(); const tenantId = $derived($auth.user?.tenantId); @@ -49,23 +48,7 @@ mode: "VIEW", }); let view: CalendarView = $state(page.data.calendarView); - let hours = $derived.by(() => { - const from = channels - .map((c) => c.slotTemplates.map((t) => t.from)) - .flat() - .map((time) => { - const [hourStr] = timeUTCToLocalWithoutOffset(time).split(":"); - return parseInt(hourStr, 10); - }); - const to = channels - .map((c) => c.slotTemplates.map((t) => t.to)) - .flat() - .map((time) => { - const [hourStr] = timeUTCToLocalWithoutOffset(time).split(":"); - return parseInt(hourStr, 10); - }); - return { from: Math.min(...from), to: Math.max(...to) }; - }); + let hours = $derived.by(() => getOperatingHours(channels, calendar)); let scale = $state(page.data.calendarZoom); $effect(() => {