diff --git a/package-lock.json b/package-lock.json index 7ee3ffc..800b56e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@noble/post-quantum": "^0.4.1", "@simplewebauthn/server": "^11.0.0", "@sveltejs/adapter-node": "^5.5.1", + "@tanstack/svelte-query": "^6.1.34", "argon2": "^0.43.0", "argon2-browser": "^1.18.0", "cropperjs": "^2.0.1", @@ -2887,6 +2888,32 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/svelte-query": { + "version": "6.1.34", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.1.34.tgz", + "integrity": "sha512-b/j6OXHd285NLPTH31SqJ8+XfHKoDPV8+ZGxXjg4pSAWW2aVo/f5gbu5m32QjfdTSumib3+d1dJBv5Yupv7HKw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "svelte": "^5.25.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/package.json b/package.json index 0381391..0fe615a 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@noble/post-quantum": "^0.4.1", "@simplewebauthn/server": "^11.0.0", "@sveltejs/adapter-node": "^5.5.1", + "@tanstack/svelte-query": "^6.1.34", "argon2": "^0.43.0", "argon2-browser": "^1.18.0", "cropperjs": "^2.0.1", diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index 06085af..5e4bcec 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -357,6 +357,7 @@ "weekdays": "Wochentage" } }, + "cancel": "Abbrechen", "actions": "Aktionen", "edit": "Editieren", "pause": "Deaktivieren", @@ -941,7 +942,12 @@ }, "calendar": { "title": "Kalender", + "selectDate": "Datum auswählen", "today": "Heute", + "thisMonth": "Aktueller Monat", + "addThreeMonths": "+3", + "addSixMonths": "+6", + "addOneYear": "+12", "loading": "Kalender wird geladen", "decrypting": "wird entschlüsselt...", "decryptingError": "Entschlüsselung fehlgeschlagen. Bitte neu laden/anmelden.", diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index 9ac6538..6e87e59 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -950,7 +950,12 @@ }, "calendar": { "title": "Calendar", + "selectDate": "Select a date", "today": "Today", + "thisMonth": "Current month", + "addThreeMonths": "+3", + "addSixMonths": "+6", + "addOneYear": "+12", "loading": "Loading calendar", "decrypting": "decrypting...", "decryptingError": "Unable to decrypt data. Please reload/log-in again", diff --git a/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte index 472faeb..7b39aef 100644 --- a/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte +++ b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte @@ -1,5 +1,6 @@ @@ -95,27 +103,32 @@ {m["nav.tenants"]()} - {#each $tenants?.tenants.slice(0, maxTenantsToShow) as tenant (tenant.id)} + {#each tenantList.slice(0, maxTenantsToShow) as tenant (tenant.id)} tenants.setCurrentTenant(tenant.id)} - class="gap-2 p-2" + class="justify-between gap-2 p-2" > -
- {#if tenant.logo} - {tenant.shortName} - {:else} - - {/if} +
+
+ {#if tenant.logo} + {tenant.shortName} + {:else} + + {/if} +
+ {tenant.shortName}
- {tenant.shortName} + {#if tenant.id === activeTenantId} + + {/if} {/each} - {#if $tenants?.tenants.length > maxTenantsToShow} + {#if tenantList.length > maxTenantsToShow} + import * as Calendar from "$lib/components/ui/calendar"; + import * as Sidebar from "$lib/components/ui/sidebar"; + import { cn } from "$lib/utils"; + import { type DateValue } from "@internationalized/date"; + import { type ComponentProps } from "svelte"; + + let { + day, + outsideMonth, + isDateEmpty, + isDateHighlighted, + }: ComponentProps & { + day: DateValue; + outsideMonth: boolean; + isDateEmpty?: (day: DateValue) => boolean; + isDateHighlighted?: (day: DateValue) => boolean; + } = $props(); + + + + {#if !outsideMonth} +
+
+ {day.day} +
+ {#if isDateHighlighted && isDateHighlighted(day)} + + {/if} +
+ {/if} +
diff --git a/src/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte index 0f842b0..d281ac9 100644 --- a/src/lib/components/ui/calendar/calendar.svelte +++ b/src/lib/components/ui/calendar/calendar.svelte @@ -1,3 +1,9 @@ + -{@render children()} + + {@render children()} + diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte index afea0ef..ede8489 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarFilters.svelte @@ -14,14 +14,17 @@ import { sidebar as sidebarStore } from "$lib/stores/sidebar"; import type { TAppointmentFilter } from "$lib/types/calendar"; import { cn } from "$lib/utils"; - import { PanelRightClose, ZoomIn, ZoomOut } from "@lucide/svelte"; + import { CalendarDate } from "@internationalized/date"; + import { FunnelX, PanelRightClose, ZoomIn, ZoomOut } from "@lucide/svelte"; import { type ComponentProps } from "svelte"; + import CalendarMonth from "./CalendarMonth.svelte"; let { shownAppointments = $bindable(), shownChannels = $bindable(), shownAgents = $bindable(), scale = $bindable(), + selectedDate = $bindable(), ref = $bindable(null), ...restProps }: ComponentProps & { @@ -29,6 +32,7 @@ shownChannels: string[]; shownAgents: string[]; scale: number; + selectedDate: CalendarDate; } = $props(); const appointmentStates: { value: TAppointmentFilter; label: string }[] = [ @@ -49,6 +53,12 @@ const nextIndex = currentIndex + direction; scale = zoomSteps[nextIndex]; }; + + const clearFilters = () => { + shownAppointments = "all"; + shownChannels = []; + shownAgents = []; + }; - {m["calendar.shownAppointments.title"]()} - - {#each appointmentStates as state (state.value)} -
- - +
+
+
+ {m["calendar.shownAppointments.title"]()} +
- {/each} - + + {#each appointmentStates as state (state.value)} +
+ + +
+ {/each} +
+
+ {#if channels.length > 1} +
+ {m["channels.title"]()} + {#each channels as channel (channel.id)} + {@const locale = getLocale()} + {@const name = channel.names[locale] || Object.values(channel.names)[0]} +
+ { + if (v) { + shownChannels = [...shownChannels, channel.id]; + } else { + shownChannels = shownChannels.filter((id) => id !== channel.id); + } + }} + class="mt-2 mb-1" + /> +
+ {/each} +
+ {/if} + {#if agents.length > 1} +
+ {m["agents.title"]()} + {#each agents as agent (agent.id)} +
+ { + if (v) { + shownAgents = [...shownAgents, agent.id]; + } else { + shownAgents = shownAgents.filter((id) => id !== agent.id); + } + }} + class="mt-2 mb-1" + /> +
+ {/each} +
+ {/if} +
- - - {#if channels.length > 1} -
- {m["channels.title"]()} - {#each channels as channel (channel.id)} - {@const locale = getLocale()} - {@const name = channel.names[locale] || Object.values(channel.names)[0]} -
- { - if (v) { - shownChannels = [...shownChannels, channel.id]; - } else { - shownChannels = shownChannels.filter((id) => id !== channel.id); - } - }} - class="mt-2 mb-1" - /> -
- {/each} -
- {/if} - {#if agents.length > 1} -
- {m["agents.title"]()} - {#each agents as agent (agent.id)} -
- { - if (v) { - shownAgents = [...shownAgents, agent.id]; - } else { - shownAgents = shownAgents.filter((id) => id !== agent.id); - } - }} - class="mt-2 mb-1" - /> -
- {/each} -
- {/if} + + + sidebarStore.setCalendarExpanded(!sidebar.isCalendarExpanded)} + /> diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte index 356f336..eb717d1 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarHeader.svelte @@ -1,32 +1,53 @@ @@ -37,15 +58,28 @@ -
- {Intl.DateTimeFormat(getLocale(), { - year: "numeric", - month: "long", - day: "numeric", - weekday: "short", - timeZone: getLocalTimeZone().toString(), - }).format(startDate.toDate(getLocalTimeZone()))} -
+ + + {Intl.DateTimeFormat(getLocale(), { + year: "numeric", + month: "long", + day: "numeric", + weekday: "short", + timeZone: getLocalTimeZone().toString(), + }).format(selectedDate.toDate(getLocalTimeZone()))} + + + + + @@ -54,7 +88,7 @@ size="sm" variant="outline" onclick={setToToday} - disabled={startDate.toString() === today(getLocalTimeZone()).toString()} + disabled={selectedDate.toString() === today(getLocalTimeZone()).toString()} class="-order-1 ml-auto min-[500px]:order-0" > {m["calendar.today"]()} diff --git a/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte b/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte new file mode 100644 index 0000000..32b40bf --- /dev/null +++ b/src/routes/(pages)/dashboard/calendar/(components)/CalendarMonth.svelte @@ -0,0 +1,189 @@ + + +
+
+ +
+ + + +
+
+ + {#snippet day({ day, outsideMonth })} + + {/snippet} + +
diff --git a/src/routes/(pages)/dashboard/calendar/(components)/queries.ts b/src/routes/(pages)/dashboard/calendar/(components)/queries.ts new file mode 100644 index 0000000..73710da --- /dev/null +++ b/src/routes/(pages)/dashboard/calendar/(components)/queries.ts @@ -0,0 +1,9 @@ +import type { CalendarDate } from "@internationalized/date"; +import { fetchCalendar } from "./utils"; + +export const calendarMonthQuery = (tenant: string | undefined | null, date: CalendarDate) => ({ + queryKey: ["calendar", `${date.year}-${date.month}`], + queryFn: () => fetchCalendar({ tenant: tenant as string, selectedDate: date }), + enabled: Boolean(tenant), + staleTime: 5000, +}); diff --git a/src/routes/(pages)/dashboard/calendar/(components)/utils.ts b/src/routes/(pages)/dashboard/calendar/(components)/utils.ts index 0e7c44d..00b24c6 100644 --- a/src/routes/(pages)/dashboard/calendar/(components)/utils.ts +++ b/src/routes/(pages)/dashboard/calendar/(components)/utils.ts @@ -10,14 +10,26 @@ import { localToUTCWithoutDST } from "$lib/utils/datetime"; import { getLocalTimeZone, type CalendarDate } from "@internationalized/date"; import { get } from "svelte/store"; -export const fetchCalendar = async (opts: { tenant: string; startDate: CalendarDate }) => { +export const fetchCalendar = async (opts: { tenant: string; selectedDate: CalendarDate }) => { if (!browser) return; const localStartDate = new Date( - Date.UTC(opts.startDate.year, opts.startDate.month - 1, opts.startDate.day, 0, 0, 0, 0), + opts.selectedDate.year, + opts.selectedDate.month - 1, + 1, + 0, + 0, + 0, + 0, ); const localEndDate = new Date( - Date.UTC(opts.startDate.year, opts.startDate.month - 1, opts.startDate.day, 23, 59, 59, 999), + opts.selectedDate.year, + opts.selectedDate.month, + 0, + 23, + 59, + 59, + 999, ); const params = new URLSearchParams({ diff --git a/src/routes/(pages)/dashboard/calendar/+page.svelte b/src/routes/(pages)/dashboard/calendar/+page.svelte index 1e540ee..82c00cc 100644 --- a/src/routes/(pages)/dashboard/calendar/+page.svelte +++ b/src/routes/(pages)/dashboard/calendar/+page.svelte @@ -12,7 +12,7 @@ import { calendarStore } from "$lib/stores/calendar"; import { channels as channelsStore } from "$lib/stores/channels"; import { sidebar } from "$lib/stores/sidebar"; - import type { TAppointmentFilter, TCalendar, TCalendarItem } from "$lib/types/calendar"; + import type { TAppointmentFilter, TCalendarItem } from "$lib/types/calendar"; import { timeUTCToLocalWithoutOffset, utcToLocalWithoutDST } from "$lib/utils/datetime"; import { getCurrentTranlslation } from "$lib/utils/localizations"; import { @@ -22,31 +22,35 @@ today, type CalendarDate, } from "@internationalized/date"; - import { Funnel } from "@lucide/svelte"; + import { SlidersHorizontal } from "@lucide/svelte"; import { onMount } from "svelte"; import AddAppointment from "./(components)/add-appointment/AddAppointment.svelte"; import Appointment from "./(components)/Appointment.svelte"; import CalendarDay from "./(components)/CalendarDay.svelte"; import CalendarFilters from "./(components)/CalendarFilters.svelte"; import CalendarHeader from "./(components)/CalendarHeader.svelte"; - import { fetchCalendar, openAppointmentById } from "./(components)/utils"; + import { openAppointmentById } from "./(components)/utils"; + import { createQuery, useQueryClient } from "@tanstack/svelte-query"; + import { calendarMonthQuery } from "./(components)/queries"; const convertDate = (dateStr: string) => { const zonedDateTime = parseAbsoluteToLocal(dateStr); return toCalendarDate(zonedDateTime); }; + const queryClient = useQueryClient(); const tenantId = $derived($auth.user?.tenantId); const curEmptySlot = $derived($calendarStore.curEmptySlot); const curItem = $derived($calendarStore.curItem); const channels = $derived($channelsStore.channels); const agents = $derived($agentsStore.agents); - let startDate: CalendarDate = $state( + let selectedDate: CalendarDate = $state( "date" in page.state ? convertDate(history?.state["sveltekit:states"].date) : today(getLocalTimeZone()), ); - let calender: TCalendar | undefined = $state(); + const query = createQuery(() => calendarMonthQuery(tenantId as string, selectedDate)); + const calendar = $derived(query.data); let shownAppointments: TAppointmentFilter = $state("all"); let shownChannels: string[] = $state([]); let shownAgents: string[] = $state([]); @@ -70,7 +74,10 @@ let scale = $state(1); $effect(() => { - updateCalendar(); + const getMonth = (x: string | undefined) => (x ? new Date(x).getMonth() + 1 : undefined); + if (!calendar || getMonth(calendar?.period.startDate) !== getMonth(selectedDate.toString())) { + updateCalendar(); + } }); $effect(() => { @@ -100,8 +107,8 @@ "appointmentId" in page.state ) { const date = convertDate(page.state.date as string); - if (date.toString() !== startDate.toString()) { - startDate = date; + if (date.toString() !== selectedDate.toString()) { + selectedDate = date; replaceState("", { appointmentId: page.state.appointmentId }); } else { if (items) { @@ -115,8 +122,9 @@ const updateCalendar = async () => { if (tenantId) { - calender = undefined; - calender = await fetchCalendar({ startDate, tenant: tenantId }); + queryClient.invalidateQueries({ + queryKey: ["calendar"], + }); } }; @@ -125,8 +133,8 @@ }; let items: TCalendarItem[] | undefined = $derived.by(() => { - if (!calender) return undefined; - const dayEntry = calender.calendar.find((d) => d.date === startDate.toString()); + if (!calendar) return undefined; + const dayEntry = calendar.calendar.find((d) => d.date === selectedDate.toString()); if (!dayEntry) return []; return Object.keys(dayEntry.channels).reduce((allItems, channelId) => { const channelData = dayEntry.channels[channelId]; @@ -203,10 +211,10 @@
- +
sidebar.setCalendarExpanded(!$sidebar.isCalendarExpanded)} class="lg:hidden" > - + {/snippet} {#snippet sidebarRight()} - + {/snippet}