mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-27 03:14:48 +02:00
Calendar: Fixed a few display issues
This commit is contained in:
@@ -127,6 +127,7 @@
|
||||
|
||||
<div class="relative flex w-full flex-col">
|
||||
{#if isWeekView && items}
|
||||
<!-- Date -->
|
||||
{@const isSelected = selectedDate.toString() === day.toString()}
|
||||
{@const isToday = toCalendarDate(day).toString() === today(getLocalTimeZone()).toString()}
|
||||
<Text
|
||||
@@ -142,10 +143,25 @@
|
||||
weekday: "short",
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<!-- Vertical line separating days in week view -->
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="bg-muted-foreground absolute top-0 bottom-0 left-0 z-10 transition-all duration-200"
|
||||
style={{ height: `${(latestEndHour * 30 + 30) * scale + 20}px`, marginTop: "-10px" }}
|
||||
style={{
|
||||
height: `${
|
||||
// shown hours in minutes
|
||||
((latestEndHour - earliestStartHour) * 60 +
|
||||
// plus 30 minutes before the day starts
|
||||
30) *
|
||||
scale +
|
||||
// plus 3 pixels for the bottom overflow of the line
|
||||
4 +
|
||||
// plus 10 pixels for the margin
|
||||
10
|
||||
}px`,
|
||||
marginTop: "-10px",
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="absolute top-0 right-0 bottom-0 left-0 z-10">
|
||||
|
||||
@@ -195,9 +195,9 @@
|
||||
class="mt-2 mb-1"
|
||||
>
|
||||
{#snippet label()}
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-start gap-1">
|
||||
<div
|
||||
class="bg-primary h-4 w-2 rounded-sm"
|
||||
class="bg-primary mt-0.5 h-4 w-2 rounded-sm"
|
||||
style:background-color={channel.color}
|
||||
></div>
|
||||
{name}
|
||||
@@ -244,6 +244,7 @@
|
||||
changeView("week");
|
||||
}
|
||||
}}
|
||||
class="min-h-82"
|
||||
/>
|
||||
</HorizontalPagePadding>
|
||||
</Sidebar.Content>
|
||||
|
||||
@@ -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));
|
||||
</script>
|
||||
|
||||
<div class="relative flex w-16 shrink-0 flex-col">
|
||||
@@ -50,14 +53,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Current Time Indicator -->
|
||||
<!-- Current Time Indicator Time Bubble -->
|
||||
{#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 +
|
||||
|
||||
@@ -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));
|
||||
</script>
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="absolute flex w-[calc(100%-2rem)] flex-col">
|
||||
<!-- First half hour line before day starts -->
|
||||
<div
|
||||
class="relative flex w-full items-start justify-between transition-all duration-200"
|
||||
style:height={`${focusAdjustment}px`}
|
||||
@@ -43,14 +47,10 @@
|
||||
<Separator class="bg-secondary absolute top-0 right-0 left-16 h-px w-auto!" />
|
||||
</div>
|
||||
|
||||
<!-- Current Time Indicator -->
|
||||
<!-- Current Time Indicator Line -->
|
||||
{#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 +
|
||||
|
||||
@@ -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<typeof Sidebar.Root> & {
|
||||
@@ -32,6 +34,7 @@
|
||||
shownAppointments: TAppointmentFilter;
|
||||
shownChannels: string[];
|
||||
shownAgents: string[];
|
||||
class?: string;
|
||||
onSelectDay?: OnChangeFn<DateValue | undefined>;
|
||||
} = $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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user