feat(sw-453): implemented filter from packages
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
.page {
|
||||
min-height: 100dvh;
|
||||
padding-top: var(--Spacing-x6);
|
||||
padding-left: var(--Spacing-x2);
|
||||
padding-right: var(--Spacing-x2);
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x7);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-width: 340px;
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages"
|
||||
|
||||
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||
import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter"
|
||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
||||
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
|
||||
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
@@ -24,7 +22,7 @@ export default async function SelectRatePage({
|
||||
const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms
|
||||
const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms
|
||||
|
||||
const [hotelData, roomsAvailability, user] = await Promise.all([
|
||||
const [hotelData, roomsAvailability, packages, user] = await Promise.all([
|
||||
serverClient().hotel.hotelData.get({
|
||||
hotelId: searchParams.hotel,
|
||||
language: params.lang,
|
||||
@@ -37,6 +35,18 @@ export default async function SelectRatePage({
|
||||
adults,
|
||||
children,
|
||||
}),
|
||||
serverClient().hotel.packages.get({
|
||||
hotelId: searchParams.hotel,
|
||||
startDate: searchParams.fromDate,
|
||||
endDate: searchParams.toDate,
|
||||
adults: adults,
|
||||
children: children,
|
||||
packageCodes: [
|
||||
RoomPackageCode.ACCE,
|
||||
RoomPackageCode.PETR,
|
||||
RoomPackageCode.ALLG,
|
||||
],
|
||||
}),
|
||||
getProfileSafely(),
|
||||
])
|
||||
|
||||
@@ -51,20 +61,14 @@ export default async function SelectRatePage({
|
||||
const roomCategories = hotelData?.included
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<HotelInfoCard hotelData={hotelData} />
|
||||
<div className={styles.content}>
|
||||
<div className={styles.main}>
|
||||
<RoomFilter
|
||||
numberOfRooms={roomsAvailability.roomConfigurations.length}
|
||||
/>
|
||||
<RoomSelection
|
||||
roomsAvailability={roomsAvailability}
|
||||
roomCategories={roomCategories ?? []}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Rooms
|
||||
roomsAvailability={roomsAvailability}
|
||||
roomCategories={roomCategories ?? []}
|
||||
user={user}
|
||||
packages={packages ?? []}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Checkbox from "@/components/TempDesignSystem/Checkbox"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
@@ -1,49 +1,60 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useRef } from "react"
|
||||
import { useCallback, useEffect, useMemo } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
import { z } from "zod"
|
||||
|
||||
import { roomFilterSchema } from "@/server/routers/hotels/schemas/room"
|
||||
import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages"
|
||||
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./roomFilter.module.css"
|
||||
|
||||
import {
|
||||
RoomFilterFormData,
|
||||
RoomFilterProps,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
|
||||
export default function RoomFilter({
|
||||
numberOfRooms,
|
||||
onFilter,
|
||||
filterOptions,
|
||||
}: RoomFilterProps) {
|
||||
const initialFilterValues = useMemo(
|
||||
() =>
|
||||
filterOptions.reduce(
|
||||
(acc, option) => {
|
||||
acc[option.code] = false
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, boolean | undefined>
|
||||
),
|
||||
[filterOptions]
|
||||
)
|
||||
|
||||
function RoomFilter({ numberOfRooms }: RoomFilterProps) {
|
||||
const intl = useIntl()
|
||||
const methods = useForm<RoomFilterFormData>({
|
||||
defaultValues: {
|
||||
allergyFriendly: false,
|
||||
petFriendly: false,
|
||||
accessibility: false,
|
||||
},
|
||||
const methods = useForm<Record<string, boolean | undefined>>({
|
||||
defaultValues: initialFilterValues,
|
||||
mode: "all",
|
||||
reValidateMode: "onChange",
|
||||
resolver: zodResolver(roomFilterSchema),
|
||||
resolver: zodResolver(z.object({})),
|
||||
})
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const { watch, setValue } = methods
|
||||
const petFriendly = watch("petFriendly")
|
||||
const allergyFriendly = watch("allergyFriendly")
|
||||
const { watch, getValues, handleSubmit } = methods
|
||||
const petFriendly = watch(RoomPackageCode.PETR)
|
||||
const allergyFriendly = watch(RoomPackageCode.ALLG)
|
||||
|
||||
const onSubmit = (data: RoomFilterFormData) => {
|
||||
if (data.petFriendly) {
|
||||
setValue("allergyFriendly", false)
|
||||
} else if (data.allergyFriendly) {
|
||||
setValue("petFriendly", false)
|
||||
}
|
||||
console.log("Form submitted with data:", data)
|
||||
}
|
||||
const submitFilter = useCallback(() => {
|
||||
const data = getValues()
|
||||
onFilter(data)
|
||||
}, [onFilter, getValues])
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(submitFilter)())
|
||||
return () => subscription.unsubscribe()
|
||||
}, [handleSubmit, watch, submitFilter])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -51,45 +62,27 @@ function RoomFilter({ numberOfRooms }: RoomFilterProps) {
|
||||
{intl.formatMessage({ id: "Room types available" }, { numberOfRooms })}
|
||||
</Body>
|
||||
<FormProvider {...methods}>
|
||||
<form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
<div className={styles.roomsFilter}>
|
||||
<Checkbox
|
||||
name="accessibility"
|
||||
onChange={() => formRef.current?.requestSubmit()}
|
||||
>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Accessibility room" })}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
name="petFriendly"
|
||||
onChange={() => {
|
||||
setValue("petFriendly", !petFriendly)
|
||||
formRef.current?.requestSubmit()
|
||||
}}
|
||||
registerOptions={{ disabled: allergyFriendly }}
|
||||
>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Pet room" })}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
name="allergyFriendly"
|
||||
onChange={() => {
|
||||
setValue("allergyFriendly", !allergyFriendly)
|
||||
formRef.current?.requestSubmit()
|
||||
}}
|
||||
registerOptions={{ disabled: petFriendly }}
|
||||
>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Allergy room" })}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
{filterOptions.map((option) => (
|
||||
<Checkbox
|
||||
name={option.code}
|
||||
key={option.code}
|
||||
registerOptions={{
|
||||
required: false,
|
||||
disabled:
|
||||
(option.code === RoomPackageCode.PETR && allergyFriendly) ||
|
||||
(option.code === RoomPackageCode.ALLG && petFriendly),
|
||||
}}
|
||||
>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: option.description })}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoomFilter
|
||||
|
||||
53
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
53
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||
|
||||
import RoomFilter from "../RoomFilter"
|
||||
import RoomSelection from "../RoomSelection"
|
||||
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
import { RoomProps } from "@/types/components/hotelReservation/selectRate/room"
|
||||
import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
|
||||
export default function Rooms({
|
||||
roomsAvailability,
|
||||
roomCategories = [],
|
||||
user,
|
||||
packages,
|
||||
}: RoomProps) {
|
||||
const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability)
|
||||
|
||||
function handleFilter(filter: Record<string, boolean | undefined>) {
|
||||
const selectedCodes = Object.keys(filter).filter((key) => filter[key])
|
||||
|
||||
if (selectedCodes.length === 0) {
|
||||
setRooms(roomsAvailability)
|
||||
return
|
||||
}
|
||||
|
||||
const filteredRooms = roomsAvailability.roomConfigurations.filter((room) =>
|
||||
room.features.some((feature) =>
|
||||
selectedCodes.includes(feature.code as RoomPackageCodes)
|
||||
)
|
||||
)
|
||||
setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<RoomFilter
|
||||
numberOfRooms={rooms.roomConfigurations.length}
|
||||
onFilter={handleFilter}
|
||||
filterOptions={packages}
|
||||
/>
|
||||
<RoomSelection
|
||||
roomsAvailability={rooms}
|
||||
roomCategories={roomCategories}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.content {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x7);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "Et foto af værelset",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "About the hotel",
|
||||
"Accessibility room": "Handicapvenligt værelse",
|
||||
"Accessible Room": "Tilgængelighedsrum",
|
||||
"Activities": "Aktiviteter",
|
||||
"Add code": "Tilføj kode",
|
||||
"Add new card": "Tilføj nyt kort",
|
||||
@@ -14,7 +14,7 @@
|
||||
"Adults": "voksne",
|
||||
"Airport": "Lufthavn",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.",
|
||||
"Allergy room": "Allergivenligt værelse",
|
||||
"Allergy Room": "Allergirum",
|
||||
"Already a friend?": "Allerede en ven?",
|
||||
"Amenities": "Faciliteter",
|
||||
"Amusement park": "Forlystelsespark",
|
||||
@@ -217,7 +217,7 @@
|
||||
"Pay later": "Betal senere",
|
||||
"Pay now": "Betal nu",
|
||||
"Payment info": "Betalingsoplysninger",
|
||||
"Pet room": "Kæledyrsrum",
|
||||
"Pet Room": "Kæledyrsrum",
|
||||
"Phone": "Telefon",
|
||||
"Phone is required": "Telefonnummer er påkrævet",
|
||||
"Phone number": "Telefonnummer",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "Ein Foto des Zimmers",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "Über das Hotel",
|
||||
"Accessibility room": "Barrierefreies Zimmer",
|
||||
"Accessible Room": "Barrierefreies Zimmer",
|
||||
"Activities": "Aktivitäten",
|
||||
"Add code": "Code hinzufügen",
|
||||
"Add new card": "Neue Karte hinzufügen",
|
||||
@@ -14,7 +14,7 @@
|
||||
"Adults": "Erwachsene",
|
||||
"Airport": "Flughafen",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.",
|
||||
"Allergy room": "Allergiefreundliches Zimmer",
|
||||
"Allergy Room": "Allergikerzimmer",
|
||||
"Already a friend?": "Sind wir schon Freunde?",
|
||||
"Amenities": "Annehmlichkeiten",
|
||||
"Amusement park": "Vergnügungspark",
|
||||
@@ -217,7 +217,7 @@
|
||||
"Pay later": "Später bezahlen",
|
||||
"Pay now": "Jetzt bezahlen",
|
||||
"Payment info": "Zahlungsinformationen",
|
||||
"Pet room": "Haustierfreundliches Zimmer",
|
||||
"Pet Room": "Haustierzimmer",
|
||||
"Phone": "Telefon",
|
||||
"Phone is required": "Telefon ist erforderlich",
|
||||
"Phone number": "Telefonnummer",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "A photo of the room",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "About the hotel",
|
||||
"Accessibility room": "Accessibility room",
|
||||
"Accessible Room": "Accessibility room",
|
||||
"Activities": "Activities",
|
||||
"Add Room": "Add room",
|
||||
"Add code": "Add code",
|
||||
@@ -17,7 +17,7 @@
|
||||
"Age": "Age",
|
||||
"Airport": "Airport",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||
"Allergy room": "Allergy room",
|
||||
"Allergy Room": "Allergy room",
|
||||
"Already a friend?": "Already a friend?",
|
||||
"Amenities": "Amenities",
|
||||
"Amusement park": "Amusement park",
|
||||
@@ -227,7 +227,7 @@
|
||||
"Pay now": "Pay now",
|
||||
"Payment info": "Payment info",
|
||||
"Payment received": "Payment received",
|
||||
"Pet room": "Pet room",
|
||||
"Pet Room": "Pet room",
|
||||
"Phone": "Phone",
|
||||
"Phone is required": "Phone is required",
|
||||
"Phone number": "Phone number",
|
||||
@@ -258,6 +258,7 @@
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Retype new password",
|
||||
"Room": "Room",
|
||||
"Room & Terms": "Room & Terms",
|
||||
"Room facilities": "Room facilities",
|
||||
"Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "Kuva huoneesta",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "Tietoja hotellista",
|
||||
"Accessibility room": "Esteettömyyshuone",
|
||||
"Accessible Room": "Esteetön huone",
|
||||
"Activities": "Aktiviteetit",
|
||||
"Add code": "Lisää koodi",
|
||||
"Add new card": "Lisää uusi kortti",
|
||||
@@ -14,7 +14,7 @@
|
||||
"Adults": "Aikuista",
|
||||
"Airport": "Lentokenttä",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.",
|
||||
"Allergy room": "Allergiahuone",
|
||||
"Allergy Room": "Allergiahuone",
|
||||
"Already a friend?": "Oletko jo ystävä?",
|
||||
"Amenities": "Mukavuudet",
|
||||
"Amusement park": "Huvipuisto",
|
||||
@@ -217,7 +217,7 @@
|
||||
"Pay later": "Maksa myöhemmin",
|
||||
"Pay now": "Maksa nyt",
|
||||
"Payment info": "Maksutiedot",
|
||||
"Pet room": "Lemmikkihuone",
|
||||
"Pet Room": "Lemmikkihuone",
|
||||
"Phone": "Puhelin",
|
||||
"Phone is required": "Puhelin vaaditaan",
|
||||
"Phone number": "Puhelinnumero",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "Et bilde av rommet",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "Om hotellet",
|
||||
"Accessibility room": "Tilgjengelighetsrom",
|
||||
"Accessible Room": "Tilgjengelighetsrom",
|
||||
"Activities": "Aktiviteter",
|
||||
"Add code": "Legg til kode",
|
||||
"Add new card": "Legg til nytt kort",
|
||||
@@ -14,7 +14,7 @@
|
||||
"Adults": "Voksne",
|
||||
"Airport": "Flyplass",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
||||
"Allergy room": "Allergihus",
|
||||
"Allergy Room": "Allergirom",
|
||||
"Already a friend?": "Allerede Friend?",
|
||||
"Amenities": "Fasiliteter",
|
||||
"Amusement park": "Tivoli",
|
||||
@@ -215,7 +215,7 @@
|
||||
"Pay later": "Betal senere",
|
||||
"Pay now": "Betal nå",
|
||||
"Payment info": "Betalingsinformasjon",
|
||||
"Pet room": "Kjæledyrrom",
|
||||
"Pet Room": "Kjæledyrsrom",
|
||||
"Phone": "Telefon",
|
||||
"Phone is required": "Telefon kreves",
|
||||
"Phone number": "Telefonnummer",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"A photo of the room": "Ett foto av rummet",
|
||||
"About meetings & conferences": "About meetings & conferences",
|
||||
"About the hotel": "Om hotellet",
|
||||
"Accessibility room": "Tillgänglighetsrum",
|
||||
"Accessible Room": "Tillgänglighetsrum",
|
||||
"Activities": "Aktiviteter",
|
||||
"Add code": "Lägg till kod",
|
||||
"Add new card": "Lägg till nytt kort",
|
||||
@@ -14,7 +14,7 @@
|
||||
"Adults": "Vuxna",
|
||||
"Airport": "Flygplats",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.",
|
||||
"Allergy room": "Allergirum",
|
||||
"Allergy Room": "Allergirum",
|
||||
"Already a friend?": "Är du redan en vän?",
|
||||
"Amenities": "Bekvämligheter",
|
||||
"Amusement park": "Nöjespark",
|
||||
@@ -215,7 +215,7 @@
|
||||
"Pay later": "Betala senare",
|
||||
"Pay now": "Betala nu",
|
||||
"Payment info": "Betalningsinformation",
|
||||
"Pet room": "Husdjursrum",
|
||||
"Pet Room": "Husdjursrum",
|
||||
"Phone": "Telefon",
|
||||
"Phone is required": "Telefonnummer är obligatorisk",
|
||||
"Phone number": "Telefonnummer",
|
||||
|
||||
@@ -23,6 +23,7 @@ export namespace endpoints {
|
||||
rewards = `${profile}/reward`,
|
||||
tierRewards = `${profile}/TierRewards`,
|
||||
subscriberId = `${profile}/SubscriberId`,
|
||||
packages = "package/v1/packages/hotel",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function get(
|
||||
const searchParams = new URLSearchParams(params)
|
||||
if (searchParams.size) {
|
||||
searchParams.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
url.searchParams.append(key, value)
|
||||
})
|
||||
url.searchParams.sort()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ import {
|
||||
getHotelPageCounter,
|
||||
validateHotelPageRefs,
|
||||
} from "../contentstack/hotelPage/utils"
|
||||
import {
|
||||
getRoomPackagesInputSchema,
|
||||
getRoomPackagesSchema,
|
||||
} from "./schemas/packages"
|
||||
import {
|
||||
getHotelInputSchema,
|
||||
getHotelsAvailabilityInputSchema,
|
||||
@@ -57,6 +61,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
||||
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
|
||||
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
||||
|
||||
const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
|
||||
const getPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-success"
|
||||
)
|
||||
const getPackagesFailCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-fail"
|
||||
)
|
||||
|
||||
const hotelsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels"
|
||||
)
|
||||
@@ -694,4 +706,89 @@ export const hotelQueryRouter = router({
|
||||
return locations
|
||||
}),
|
||||
}),
|
||||
packages: router({
|
||||
get: serviceProcedure
|
||||
.input(getRoomPackagesInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { hotelId, startDate, endDate, adults, children, packageCodes } =
|
||||
input
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
startDate,
|
||||
endDate,
|
||||
adults: adults.toString(),
|
||||
children: children.toString(),
|
||||
})
|
||||
|
||||
packageCodes.forEach((code) => {
|
||||
searchParams.append("packageCodes", code)
|
||||
})
|
||||
|
||||
const params = searchParams.toString()
|
||||
|
||||
getPackagesCounter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.packages start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.packages}/${hotelId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
getPackagesFailCounter.add(1, {
|
||||
hotelId,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.packages error",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson)
|
||||
|
||||
if (!validatedPackagesData.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedPackagesData.error),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotels.packages validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedPackagesData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
getPackagesSuccessCounter.add(1, {
|
||||
hotelId,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.packages success",
|
||||
JSON.stringify({ query: { hotelId, params: params } })
|
||||
)
|
||||
|
||||
return validatedPackagesData.data
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
59
server/routers/hotels/schemas/packages.ts
Normal file
59
server/routers/hotels/schemas/packages.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export enum RoomPackageCode {
|
||||
PETR = "PETR",
|
||||
ALLG = "ALLG",
|
||||
ACCE = "ACCE",
|
||||
}
|
||||
|
||||
export const getRoomPackagesInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
adults: z.number(),
|
||||
children: z.number().optional().default(0),
|
||||
packageCodes: z.array(z.string()).optional().default([]),
|
||||
})
|
||||
|
||||
const packagesSchema = z.array(
|
||||
z.object({
|
||||
code: z.enum([
|
||||
RoomPackageCode.PETR,
|
||||
RoomPackageCode.ALLG,
|
||||
RoomPackageCode.ACCE,
|
||||
]),
|
||||
itemCode: z.string(),
|
||||
description: z.string(),
|
||||
currency: z.string(),
|
||||
calculatedPrice: z.number(),
|
||||
inventories: z.array(
|
||||
z.object({
|
||||
date: z.string(),
|
||||
total: z.number(),
|
||||
available: z.number(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
export const getRoomPackagesSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
hotelId: z.number(),
|
||||
packages: packagesSchema,
|
||||
}),
|
||||
relationships: z
|
||||
.object({
|
||||
links: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
type: z.string(),
|
||||
}),
|
||||
})
|
||||
.transform((data) => data.data.attributes.packages)
|
||||
@@ -92,9 +92,3 @@ export const roomSchema = z
|
||||
roomFacilities: data.attributes.roomFacilities,
|
||||
}
|
||||
})
|
||||
|
||||
export const roomFilterSchema = z.object({
|
||||
accessibility: z.boolean(),
|
||||
petFriendly: z.boolean(),
|
||||
allergyFriendly: z.boolean(),
|
||||
})
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function getServiceToken() {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
scopes = ["profile"]
|
||||
} else {
|
||||
scopes = ["profile", "hotel", "booking"]
|
||||
scopes = ["profile", "hotel", "booking", "package"]
|
||||
}
|
||||
const tag = generateServiceTokenTag(scopes)
|
||||
const getCachedJwt = unstable_cache(
|
||||
|
||||
6
types/components/hotelReservation/selectRate/room.ts
Normal file
6
types/components/hotelReservation/selectRate/room.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { RoomPackageData } from "./roomFilter"
|
||||
import { RoomSelectionProps } from "./roomSelection"
|
||||
|
||||
export interface RoomProps extends RoomSelectionProps {
|
||||
packages: RoomPackageData
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { roomFilterSchema } from "@/server/routers/hotels/schemas/room"
|
||||
import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages"
|
||||
|
||||
export interface RoomFilterProps {
|
||||
numberOfRooms: number
|
||||
onFilter: (filter: Record<string, boolean | undefined>) => void
|
||||
filterOptions: RoomPackageData
|
||||
}
|
||||
|
||||
export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {}
|
||||
export interface RoomPackageData
|
||||
extends z.output<typeof getRoomPackagesSchema> {}
|
||||
|
||||
export type RoomPackageCodes = RoomPackageData[number]["code"]
|
||||
|
||||
Reference in New Issue
Block a user