feat(sw-453): implemented filter from packages

This commit is contained in:
Pontus Dreij
2024-10-25 12:55:13 +02:00
parent 260c9096f6
commit 7b36139684
20 changed files with 330 additions and 133 deletions

View File

@@ -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;
}

View File

@@ -1,14 +1,12 @@
import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter" import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs } from "@/types/params" 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 adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms
const children = selectRoomParamsObject.room?.[0].child?.length // 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({ serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel, hotelId: searchParams.hotel,
language: params.lang, language: params.lang,
@@ -37,6 +35,18 @@ export default async function SelectRatePage({
adults, adults,
children, 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(), getProfileSafely(),
]) ])
@@ -51,20 +61,14 @@ export default async function SelectRatePage({
const roomCategories = hotelData?.included const roomCategories = hotelData?.included
return ( return (
<div> <>
<HotelInfoCard hotelData={hotelData} /> <HotelInfoCard hotelData={hotelData} />
<div className={styles.content}> <Rooms
<div className={styles.main}> roomsAvailability={roomsAvailability}
<RoomFilter roomCategories={roomCategories ?? []}
numberOfRooms={roomsAvailability.roomConfigurations.length} user={user}
/> packages={packages ?? []}
<RoomSelection />
roomsAvailability={roomsAvailability} </>
roomCategories={roomCategories ?? []}
user={user}
/>
</div>
</div>
</div>
) )
} }

View File

@@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import Button from "@/components/TempDesignSystem/Button" 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 Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"

View File

@@ -1,49 +1,60 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useRef } from "react" import { useCallback, useEffect, useMemo } from "react"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl" 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 Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./roomFilter.module.css" import styles from "./roomFilter.module.css"
import { import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter"
RoomFilterFormData,
RoomFilterProps, export default function RoomFilter({
} from "@/types/components/hotelReservation/selectRate/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 intl = useIntl()
const methods = useForm<RoomFilterFormData>({ const methods = useForm<Record<string, boolean | undefined>>({
defaultValues: { defaultValues: initialFilterValues,
allergyFriendly: false,
petFriendly: false,
accessibility: false,
},
mode: "all", mode: "all",
reValidateMode: "onChange", reValidateMode: "onChange",
resolver: zodResolver(roomFilterSchema), resolver: zodResolver(z.object({})),
}) })
const formRef = useRef<HTMLFormElement | null>(null) const { watch, getValues, handleSubmit } = methods
const { watch, setValue } = methods const petFriendly = watch(RoomPackageCode.PETR)
const petFriendly = watch("petFriendly") const allergyFriendly = watch(RoomPackageCode.ALLG)
const allergyFriendly = watch("allergyFriendly")
const onSubmit = (data: RoomFilterFormData) => { const submitFilter = useCallback(() => {
if (data.petFriendly) { const data = getValues()
setValue("allergyFriendly", false) onFilter(data)
} else if (data.allergyFriendly) { }, [onFilter, getValues])
setValue("petFriendly", false)
} useEffect(() => {
console.log("Form submitted with data:", data) const subscription = watch(() => handleSubmit(submitFilter)())
} return () => subscription.unsubscribe()
}, [handleSubmit, watch, submitFilter])
return ( return (
<div className={styles.container}> <div className={styles.container}>
@@ -51,45 +62,27 @@ function RoomFilter({ numberOfRooms }: RoomFilterProps) {
{intl.formatMessage({ id: "Room types available" }, { numberOfRooms })} {intl.formatMessage({ id: "Room types available" }, { numberOfRooms })}
</Body> </Body>
<FormProvider {...methods}> <FormProvider {...methods}>
<form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(submitFilter)}>
<div className={styles.roomsFilter}> <div className={styles.roomsFilter}>
<Checkbox {filterOptions.map((option) => (
name="accessibility" <Checkbox
onChange={() => formRef.current?.requestSubmit()} name={option.code}
> key={option.code}
<Caption color="uiTextHighContrast"> registerOptions={{
{intl.formatMessage({ id: "Accessibility room" })} required: false,
</Caption> disabled:
</Checkbox> (option.code === RoomPackageCode.PETR && allergyFriendly) ||
<Checkbox (option.code === RoomPackageCode.ALLG && petFriendly),
name="petFriendly" }}
onChange={() => { >
setValue("petFriendly", !petFriendly) <Caption color="uiTextHighContrast">
formRef.current?.requestSubmit() {intl.formatMessage({ id: option.description })}
}} </Caption>
registerOptions={{ disabled: allergyFriendly }} </Checkbox>
> ))}
<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>
</div> </div>
</form> </form>
</FormProvider> </FormProvider>
</div> </div>
) )
} }
export default RoomFilter

View 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>
)
}

View File

@@ -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);
}

View File

@@ -5,7 +5,7 @@
"A photo of the room": "Et foto af værelset", "A photo of the room": "Et foto af værelset",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "About the hotel", "About the hotel": "About the hotel",
"Accessibility room": "Handicapvenligt værelse", "Accessible Room": "Tilgængelighedsrum",
"Activities": "Aktiviteter", "Activities": "Aktiviteter",
"Add code": "Tilføj kode", "Add code": "Tilføj kode",
"Add new card": "Tilføj nyt kort", "Add new card": "Tilføj nyt kort",
@@ -14,7 +14,7 @@
"Adults": "voksne", "Adults": "voksne",
"Airport": "Lufthavn", "Airport": "Lufthavn",
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", "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?", "Already a friend?": "Allerede en ven?",
"Amenities": "Faciliteter", "Amenities": "Faciliteter",
"Amusement park": "Forlystelsespark", "Amusement park": "Forlystelsespark",
@@ -217,7 +217,7 @@
"Pay later": "Betal senere", "Pay later": "Betal senere",
"Pay now": "Betal nu", "Pay now": "Betal nu",
"Payment info": "Betalingsoplysninger", "Payment info": "Betalingsoplysninger",
"Pet room": "Kæledyrsrum", "Pet Room": "Kæledyrsrum",
"Phone": "Telefon", "Phone": "Telefon",
"Phone is required": "Telefonnummer er påkrævet", "Phone is required": "Telefonnummer er påkrævet",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",

View File

@@ -5,7 +5,7 @@
"A photo of the room": "Ein Foto des Zimmers", "A photo of the room": "Ein Foto des Zimmers",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "Über das Hotel", "About the hotel": "Über das Hotel",
"Accessibility room": "Barrierefreies Zimmer", "Accessible Room": "Barrierefreies Zimmer",
"Activities": "Aktivitäten", "Activities": "Aktivitäten",
"Add code": "Code hinzufügen", "Add code": "Code hinzufügen",
"Add new card": "Neue Karte hinzufügen", "Add new card": "Neue Karte hinzufügen",
@@ -14,7 +14,7 @@
"Adults": "Erwachsene", "Adults": "Erwachsene",
"Airport": "Flughafen", "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.", "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?", "Already a friend?": "Sind wir schon Freunde?",
"Amenities": "Annehmlichkeiten", "Amenities": "Annehmlichkeiten",
"Amusement park": "Vergnügungspark", "Amusement park": "Vergnügungspark",
@@ -217,7 +217,7 @@
"Pay later": "Später bezahlen", "Pay later": "Später bezahlen",
"Pay now": "Jetzt bezahlen", "Pay now": "Jetzt bezahlen",
"Payment info": "Zahlungsinformationen", "Payment info": "Zahlungsinformationen",
"Pet room": "Haustierfreundliches Zimmer", "Pet Room": "Haustierzimmer",
"Phone": "Telefon", "Phone": "Telefon",
"Phone is required": "Telefon ist erforderlich", "Phone is required": "Telefon ist erforderlich",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",

View File

@@ -5,7 +5,7 @@
"A photo of the room": "A photo of the room", "A photo of the room": "A photo of the room",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "About the hotel", "About the hotel": "About the hotel",
"Accessibility room": "Accessibility room", "Accessible Room": "Accessibility room",
"Activities": "Activities", "Activities": "Activities",
"Add Room": "Add room", "Add Room": "Add room",
"Add code": "Add code", "Add code": "Add code",
@@ -17,7 +17,7 @@
"Age": "Age", "Age": "Age",
"Airport": "Airport", "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.", "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?", "Already a friend?": "Already a friend?",
"Amenities": "Amenities", "Amenities": "Amenities",
"Amusement park": "Amusement park", "Amusement park": "Amusement park",
@@ -227,7 +227,7 @@
"Pay now": "Pay now", "Pay now": "Pay now",
"Payment info": "Payment info", "Payment info": "Payment info",
"Payment received": "Payment received", "Payment received": "Payment received",
"Pet room": "Pet room", "Pet Room": "Pet room",
"Phone": "Phone", "Phone": "Phone",
"Phone is required": "Phone is required", "Phone is required": "Phone is required",
"Phone number": "Phone number", "Phone number": "Phone number",
@@ -258,6 +258,7 @@
"Restaurant & Bar": "Restaurant & Bar", "Restaurant & Bar": "Restaurant & Bar",
"Restaurants & Bars": "Restaurants & Bars", "Restaurants & Bars": "Restaurants & Bars",
"Retype new password": "Retype new password", "Retype new password": "Retype new password",
"Room": "Room",
"Room & Terms": "Room & Terms", "Room & Terms": "Room & Terms",
"Room facilities": "Room facilities", "Room facilities": "Room facilities",
"Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", "Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",

View File

@@ -5,7 +5,7 @@
"A photo of the room": "Kuva huoneesta", "A photo of the room": "Kuva huoneesta",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "Tietoja hotellista", "About the hotel": "Tietoja hotellista",
"Accessibility room": "Esteettömyyshuone", "Accessible Room": "Esteetön huone",
"Activities": "Aktiviteetit", "Activities": "Aktiviteetit",
"Add code": "Lisää koodi", "Add code": "Lisää koodi",
"Add new card": "Lisää uusi kortti", "Add new card": "Lisää uusi kortti",
@@ -14,7 +14,7 @@
"Adults": "Aikuista", "Adults": "Aikuista",
"Airport": "Lentokenttä", "Airport": "Lentokenttä",
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", "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ä?", "Already a friend?": "Oletko jo ystävä?",
"Amenities": "Mukavuudet", "Amenities": "Mukavuudet",
"Amusement park": "Huvipuisto", "Amusement park": "Huvipuisto",
@@ -217,7 +217,7 @@
"Pay later": "Maksa myöhemmin", "Pay later": "Maksa myöhemmin",
"Pay now": "Maksa nyt", "Pay now": "Maksa nyt",
"Payment info": "Maksutiedot", "Payment info": "Maksutiedot",
"Pet room": "Lemmikkihuone", "Pet Room": "Lemmikkihuone",
"Phone": "Puhelin", "Phone": "Puhelin",
"Phone is required": "Puhelin vaaditaan", "Phone is required": "Puhelin vaaditaan",
"Phone number": "Puhelinnumero", "Phone number": "Puhelinnumero",

View File

@@ -5,7 +5,7 @@
"A photo of the room": "Et bilde av rommet", "A photo of the room": "Et bilde av rommet",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "Om hotellet", "About the hotel": "Om hotellet",
"Accessibility room": "Tilgjengelighetsrom", "Accessible Room": "Tilgjengelighetsrom",
"Activities": "Aktiviteter", "Activities": "Aktiviteter",
"Add code": "Legg til kode", "Add code": "Legg til kode",
"Add new card": "Legg til nytt kort", "Add new card": "Legg til nytt kort",
@@ -14,7 +14,7 @@
"Adults": "Voksne", "Adults": "Voksne",
"Airport": "Flyplass", "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.", "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?", "Already a friend?": "Allerede Friend?",
"Amenities": "Fasiliteter", "Amenities": "Fasiliteter",
"Amusement park": "Tivoli", "Amusement park": "Tivoli",
@@ -215,7 +215,7 @@
"Pay later": "Betal senere", "Pay later": "Betal senere",
"Pay now": "Betal nå", "Pay now": "Betal nå",
"Payment info": "Betalingsinformasjon", "Payment info": "Betalingsinformasjon",
"Pet room": "Kjæledyrrom", "Pet Room": "Kjæledyrsrom",
"Phone": "Telefon", "Phone": "Telefon",
"Phone is required": "Telefon kreves", "Phone is required": "Telefon kreves",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",

View File

@@ -5,7 +5,7 @@
"A photo of the room": "Ett foto av rummet", "A photo of the room": "Ett foto av rummet",
"About meetings & conferences": "About meetings & conferences", "About meetings & conferences": "About meetings & conferences",
"About the hotel": "Om hotellet", "About the hotel": "Om hotellet",
"Accessibility room": "Tillgänglighetsrum", "Accessible Room": "Tillgänglighetsrum",
"Activities": "Aktiviteter", "Activities": "Aktiviteter",
"Add code": "Lägg till kod", "Add code": "Lägg till kod",
"Add new card": "Lägg till nytt kort", "Add new card": "Lägg till nytt kort",
@@ -14,7 +14,7 @@
"Adults": "Vuxna", "Adults": "Vuxna",
"Airport": "Flygplats", "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.", "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?", "Already a friend?": "Är du redan en vän?",
"Amenities": "Bekvämligheter", "Amenities": "Bekvämligheter",
"Amusement park": "Nöjespark", "Amusement park": "Nöjespark",
@@ -215,7 +215,7 @@
"Pay later": "Betala senare", "Pay later": "Betala senare",
"Pay now": "Betala nu", "Pay now": "Betala nu",
"Payment info": "Betalningsinformation", "Payment info": "Betalningsinformation",
"Pet room": "Husdjursrum", "Pet Room": "Husdjursrum",
"Phone": "Telefon", "Phone": "Telefon",
"Phone is required": "Telefonnummer är obligatorisk", "Phone is required": "Telefonnummer är obligatorisk",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",

View File

@@ -23,6 +23,7 @@ export namespace endpoints {
rewards = `${profile}/reward`, rewards = `${profile}/reward`,
tierRewards = `${profile}/TierRewards`, tierRewards = `${profile}/TierRewards`,
subscriberId = `${profile}/SubscriberId`, subscriberId = `${profile}/SubscriberId`,
packages = "package/v1/packages/hotel",
} }
} }

View File

@@ -37,7 +37,7 @@ export async function get(
const searchParams = new URLSearchParams(params) const searchParams = new URLSearchParams(params)
if (searchParams.size) { if (searchParams.size) {
searchParams.forEach((value, key) => { searchParams.forEach((value, key) => {
url.searchParams.set(key, value) url.searchParams.append(key, value)
}) })
url.searchParams.sort() url.searchParams.sort()
} }

View File

@@ -25,6 +25,10 @@ import {
getHotelPageCounter, getHotelPageCounter,
validateHotelPageRefs, validateHotelPageRefs,
} from "../contentstack/hotelPage/utils" } from "../contentstack/hotelPage/utils"
import {
getRoomPackagesInputSchema,
getRoomPackagesSchema,
} from "./schemas/packages"
import { import {
getHotelInputSchema, getHotelInputSchema,
getHotelsAvailabilityInputSchema, getHotelsAvailabilityInputSchema,
@@ -57,6 +61,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success") const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail") 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( const hotelsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.hotels" "trpc.hotel.availability.hotels"
) )
@@ -694,4 +706,89 @@ export const hotelQueryRouter = router({
return locations 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
}),
}),
}) })

View 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)

View File

@@ -92,9 +92,3 @@ export const roomSchema = z
roomFacilities: data.attributes.roomFacilities, roomFacilities: data.attributes.roomFacilities,
} }
}) })
export const roomFilterSchema = z.object({
accessibility: z.boolean(),
petFriendly: z.boolean(),
allergyFriendly: z.boolean(),
})

View File

@@ -74,7 +74,7 @@ export async function getServiceToken() {
if (env.HIDE_FOR_NEXT_RELEASE) { if (env.HIDE_FOR_NEXT_RELEASE) {
scopes = ["profile"] scopes = ["profile"]
} else { } else {
scopes = ["profile", "hotel", "booking"] scopes = ["profile", "hotel", "booking", "package"]
} }
const tag = generateServiceTokenTag(scopes) const tag = generateServiceTokenTag(scopes)
const getCachedJwt = unstable_cache( const getCachedJwt = unstable_cache(

View File

@@ -0,0 +1,6 @@
import { RoomPackageData } from "./roomFilter"
import { RoomSelectionProps } from "./roomSelection"
export interface RoomProps extends RoomSelectionProps {
packages: RoomPackageData
}

View File

@@ -1,9 +1,14 @@
import { z } from "zod" import { z } from "zod"
import { roomFilterSchema } from "@/server/routers/hotels/schemas/room" import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages"
export interface RoomFilterProps { export interface RoomFilterProps {
numberOfRooms: number 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"]