Skip to content

Commit

Permalink
feat(room-booking): highlight my bookings on timeline; refetch bookin…
Browse files Browse the repository at this point in the history
…gs list via React Query

Closes #160, #159
  • Loading branch information
ArtemSBulgakov committed Oct 26, 2024
1 parent 268ba4f commit 5e70d7b
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 58 deletions.
32 changes: 29 additions & 3 deletions src/components/room-booking/timeline/BookingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
useRole,
useTransitionStyles,
} from "@floating-ui/react";
import { useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import type { Booking, Slot } from "./BookingTimeline.vue";

Expand Down Expand Up @@ -94,6 +96,8 @@ export function BookingModal({
const role = useRole(context);
const { getFloatingProps } = useInteractions([dismiss, role]);

const queryClient = useQueryClient();

const { data: rooms } = $roomBooking.useQuery("get", "/rooms/");
const {
mutate,
Expand Down Expand Up @@ -133,13 +137,23 @@ export function BookingModal({
onSuccess: () => {
setTitle("");
reset();
// Refresh window to update caches in Vue
window.location.reload();
onOpenChange(false);

queryClient.invalidateQueries({
queryKey: $roomBooking.queryOptions("get", "/bookings/my").queryKey,
});

// Refetch bookings after some time
setTimeout(() => {
queryClient.invalidateQueries({
// All /bookings/ queries, with any params
queryKey: ["roomBooking", "get", "/bookings/"],
});
}, 3000);
},
},
);
}, [newSlot, title, mutate, onOpenChange, reset]);
}, [newSlot, title, mutate, reset, queryClient, onOpenChange]);

if (!isMounted) {
return null;
Expand Down Expand Up @@ -223,6 +237,17 @@ export function BookingModal({
</div>
);

const MyBookingButtons = (
<div className="flex flex-row gap-2">
<Link
to="/room-booking/list"
className="flex w-full items-center justify-center gap-2 rounded-2xl border-2 border-purple-400 bg-purple-200 px-4 py-2 text-lg font-medium text-purple-900 hover:bg-purple-300 dark:border-purple-600 dark:bg-purple-900 dark:text-purple-300 dark:hover:bg-purple-950"
>
Manage my booking
</Link>
</div>
);

return (
<FloatingPortal>
<FloatingOverlay
Expand Down Expand Up @@ -298,6 +323,7 @@ export function BookingModal({
{BookingLocation}
{BookingDate}
{BookingTime}
{detailsBooking?.myBookingId && MyBookingButtons}
</div>
)}
</div>
Expand Down
116 changes: 61 additions & 55 deletions src/components/room-booking/timeline/BookingTimeline.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { roomBookingFetch, type roomBookingTypes } from "@/api/room-booking";
import { type roomBookingTypes } from "@/api/room-booking";
import {
clockTime,
durationFormatted,
Expand All @@ -8,7 +8,14 @@ import {
} from "@/lib/utils/dates.ts";
import { useMediaQuery, useNow } from "@vueuse/core";
import type { MaybeRef } from "vue";
import { computed, onMounted, ref, shallowRef, unref, watch } from "vue";
import {
computed,
onMounted,
ref,
shallowRef,
unref,
watch,
} from "vue"; /* ========================================================================== */
/* ========================================================================== */
/* ================================ Options ================================= */
Expand Down Expand Up @@ -79,6 +86,7 @@ export type Booking = Omit<roomBookingTypes.SchemaBooking, "start" | "end"> & {
id: string;
startsAt: Date;
endsAt: Date;
myBookingId: number | undefined;
};
export type Slot = {
Expand Down Expand Up @@ -156,12 +164,8 @@ function timeGridNeighbors(
/* ============================= Initialization ============================= */
/* ========================================================================== */
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(startDate.getTime() + 7 * T.Day);
const timelineStart = shallowRef(startDate);
const timelineEnd = shallowRef(endDate);
const timelineStart = shallowRef(props.startDate);
const timelineEnd = shallowRef(props.endDate);
const timelineDates = computed(() => {
const dates = [];
Expand All @@ -183,61 +187,42 @@ onMounted(() => {
});
/* ========================================================================== */
/* ============================= Data Fetching ============================== */
/* ============================= Data Preparing ============================= */
/* ========================================================================== */
const actualRooms = shallowRef<Room[]>([]);
const roomsLoading = shallowRef(true);
roomBookingFetch
.GET("/rooms/")
.then(({ data }) => {
if (!data) throw new Error("no data");
roomsLoading.value = false;
actualRooms.value = data.map((room, idx) => ({ ...room, idx }));
})
.catch((err) => {
console.error("Failed to load rooms:", err);
// TODO: show error message to the user.
});
const actualRooms = computed(
() => props.rooms?.map((room, idx) => ({ ...room, idx })) ?? [],
);
const roomsLoading = computed(() => props.isRoomsPending);
// TODO: remove this, when backend will return booking UIDs.
let bookingIdCounter = 0;
const actualBookings = shallowRef<Map<Booking["id"], Booking>>();
const bookingsLoading = shallowRef(true);
roomBookingFetch
.GET("/bookings/", {
params: {
query: { start: startDate.toISOString(), end: endDate.toISOString() },
},
})
.then(({ data, error }) => {
if (error?.detail)
throw new Error(`validation error: ${JSON.stringify(error.detail)}`);
const actualBookings = computed<Map<Booking["id"], Booking>>(() => {
const map = new Map<Booking["id"], Booking>();
if (!data) throw new Error("no data");
const map = new Map<Booking["id"], Booking>();
for (const booking of data) {
const mappedBooking = {
...booking,
id: (++bookingIdCounter).toString(),
startsAt: new Date(booking.start),
endsAt: new Date(booking.end),
};
for (const booking of props.bookings ?? []) {
const myBooking = props.myBookings?.find(
(myBooking) =>
myBooking.room_id === booking.room_id &&
myBooking.start === booking.start &&
myBooking.end === booking.end,
);
const mappedBooking: Booking = {
...booking,
id: (++bookingIdCounter).toString(),
startsAt: new Date(booking.start),
endsAt: new Date(booking.end),
myBookingId: myBooking?.id,
title: myBooking?.title ?? booking.title,
};
map.set(mappedBooking.id, mappedBooking);
}
map.set(mappedBooking.id, mappedBooking);
}
bookingsLoading.value = false;
actualBookings.value = map;
})
.catch((err) => {
console.error("Failed to load bookings:", err);
// TODO: show error message to the user.
});
return map;
});
const bookingsLoading = computed(() => props.isBookingsPending);
const actualBookingsByRoomSorted = computed(() => {
const map = new Map<Room["id"], Booking[]>();
Expand Down Expand Up @@ -1142,7 +1127,14 @@ function scrollToNow(options?: Omit<ScrollToOptions, "to">) {

<!-- Body of the timeline (rooms and bookings). -->
<div
v-memo="[roomsLoading, bookingsLoading, compactModeEnabled]"
v-memo="[
roomsLoading,
actualRooms,
bookingsLoading,
actualBookingsByRoomSorted,
myBookings,
compactModeEnabled,
]"
:class="$style.body"
>
<div
Expand Down Expand Up @@ -1176,6 +1168,8 @@ function scrollToNow(options?: Omit<ScrollToOptions, "to">) {
:key="booking === 'placeholder' ? j : booking.id"
:class="{
[$style.booking]: true,
[$style.myBooking]:
typeof booking !== 'string' && !!booking.myBookingId,
[$style.placeholder]: booking === 'placeholder',
}"
:style="
Expand Down Expand Up @@ -1306,6 +1300,9 @@ $button-height: 50px;
--c-textbox-bg-purple: #{colors.$purple-400};
--c-textbox-text-purple: #{colors.$purple-900};
--c-textbox-borders-purple: #{colors.$purple-600};
--c-textbox-bg-green: #{colors.$green-400};
--c-textbox-text-green: #{colors.$green-900};
--c-textbox-borders-green: #{colors.$green-600};
--c-ruler-now: #{colors.$red-600};
--c-ruler-new: #{colors.$purple-600};
--c-skeleton-bg: #{colors.$gray-300};
Expand All @@ -1325,6 +1322,9 @@ $button-height: 50px;
--c-textbox-bg-purple: #{colors.$purple-900};
--c-textbox-text-purple: #{colors.$purple-500};
--c-textbox-borders-purple: #{colors.$purple-700};
--c-textbox-bg-green: #{colors.$green-900};
--c-textbox-text-green: #{colors.$green-500};
--c-textbox-borders-green: #{colors.$green-700};
--c-ruler-now: #{colors.$red-800};
--c-ruler-new: #{colors.$purple-800};
--c-skeleton-bg: #{colors.$gray-800};
Expand Down Expand Up @@ -1566,6 +1566,12 @@ $button-height: 50px;
left: var(--left);
width: var(--width);
&.myBooking > div {
border: 1px solid var(--c-textbox-borders-green);
background: var(--c-textbox-bg-green);
color: var(--c-textbox-text-green);
}
& > div {
background: var(--c-bg-items);
color: var(--c-text);
Expand Down
33 changes: 33 additions & 0 deletions src/components/room-booking/timeline/RoomBookingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useMe } from "@/api/accounts/user.ts";
import { $roomBooking } from "@/api/room-booking";
import { SignInButton } from "@/components/common/SignInButton.tsx";
import { BookingModal } from "@/components/room-booking/timeline/BookingModal.tsx";
import { T } from "@/lib/utils/dates.ts";
import { lazy, Suspense, useState } from "react";
import type { Booking, Slot } from "./BookingTimeline.vue";

Expand All @@ -15,6 +17,29 @@ export function RoomBookingPage() {

const { me } = useMe();

const [startDate] = useState(new Date(new Date().setHours(0, 0, 0, 0)));
const [endDate] = useState(new Date(startDate.getTime() + 7 * T.Day));

const { data: rooms, isPending: isRoomsPending } = $roomBooking.useQuery(
"get",
"/rooms/",
);
const { data: bookings, isPending: isBookingsPending } =
$roomBooking.useQuery(
"get",
"/bookings/",
{
params: {
query: { start: startDate.toISOString(), end: endDate.toISOString() },
},
},
{
refetchInterval: 5 * T.Min,
},
);
const { data: myBookings, isPending: isMyBookingsPending } =
$roomBooking.useQuery("get", "/bookings/my");

if (!me) {
return (
<>
Expand All @@ -33,6 +58,14 @@ export function RoomBookingPage() {
<Suspense>
<BookingTimeline
className="h-full"
startDate={startDate}
endDate={endDate}
rooms={rooms}
isRoomsPending={isRoomsPending}
bookings={bookings}
isBookingsPending={isBookingsPending}
myBookings={myBookings}
isMyBookingsPending={isMyBookingsPending}
onBook={(newBooking: Slot) => {
setNewBookingSlot(newBooking);
setBookingDetails(undefined);
Expand Down

0 comments on commit 5e70d7b

Please sign in to comment.