feat: refactor of my stay

This commit is contained in:
Simon Emanuelsson
2025-04-25 14:08:14 +02:00
committed by Simon.Emanuelsson
parent b5deb84b33
commit ec087a3d15
208 changed files with 5458 additions and 4569 deletions

View File

@@ -2,7 +2,7 @@ import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { clearAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils"
import { clearAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries"
import { AddAncillaryContext } from "@/contexts/AddAncillary"
import type {
@@ -49,18 +49,18 @@ export interface AddAncillaryState {
selectedCategory: string
selectCategory: (category: string) => void
ancillariesBySelectedCategory: Ancillary["ancillaryContent"]
openModal: VoidFunction
closeModal: VoidFunction
prevStep: VoidFunction
openModal: () => void
closeModal: () => void
prevStep: () => void
breakfastData: BreakfastData | null
setBreakfastData: (breakfastData: BreakfastData | null) => void
isBreakfast: boolean
isOpen: boolean
selectedAncillary: SelectedAncillary | null
selectAncillary: (ancillary: SelectedAncillary) => void
selectQuantity: VoidFunction
selectDeliveryTime: VoidFunction
selectQuantityAndDeliveryTime: VoidFunction
selectQuantity: () => void
selectDeliveryTime: () => void
selectQuantityAndDeliveryTime: () => void
}
function findAncillaryByCategory(

View File

@@ -0,0 +1,77 @@
import { formatPrice } from "@/utils/numberFormatting"
import type { IntlShape } from "react-intl"
import { CurrencyEnum } from "@/types/enums/currency"
import type { Room } from "@/types/stores/my-stay"
export function calculateTotalPrice(
rooms: Room[],
currency: CurrencyEnum,
intl: IntlShape,
allRoomsAreCancelled: boolean
) {
const totals = rooms.reduce(
(total, room) => {
if (!allRoomsAreCancelled && room.isCancelled) {
return total
}
if (room.cheques) {
total.cheques = total.cheques + room.cheques
}
if (room.vouchers) {
total.vouchers = total.vouchers + room.vouchers
}
if (room.totalPoints) {
total.points = total.points + room.totalPoints
}
if (room.totalPrice) {
total.cash = total.cash + room.totalPrice
}
return total
},
{
cash: 0,
cheques: 0,
points: 0,
vouchers: 0,
}
)
let totalPrice = ""
if (totals.cheques) {
totalPrice = `${totals.cheques} ${CurrencyEnum.CC}`
}
if (totals.points) {
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
totalPrice = `${appendTotalPrice}${totals.points} ${CurrencyEnum.POINTS}`
}
if (totals.vouchers) {
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
totalPrice = `${appendTotalPrice}${totals.vouchers} ${CurrencyEnum.Voucher}`
}
if (totals.cash) {
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
const cashPrice = formatPrice(intl, totals.cash, currency)
totalPrice = `${appendTotalPrice}${cashPrice}`
}
return totalPrice
}
export function calculateTotalPoints(
rooms: Room[],
allRoomsAreCancelled: boolean
) {
return rooms.reduce((total, room) => {
if (!allRoomsAreCancelled && room.isCancelled) {
return total
}
return total + room.totalPoints
}, 0)
}
export function isAllRoomsCancelled(rooms: Room[]) {
return !rooms.some((room) => room.isCancelled === false)
}

View File

@@ -0,0 +1,106 @@
"use client"
import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { getBookedHotelRoom } from "@/server/routers/booking/utils"
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
import { MyStayContext } from "@/contexts/MyStay"
import {
calculateTotalPoints,
calculateTotalPrice,
isAllRoomsCancelled,
} from "./helpers"
import type { InitialState, MyStayState } from "@/types/stores/my-stay"
export function createMyStayStore({
breakfastPackages,
hotel,
intl,
refId,
roomCategories,
rooms,
savedCreditCards,
}: InitialState) {
const rates = {
change: intl.formatMessage({
defaultMessage: "Free rebooking",
}),
flex: intl.formatMessage({
defaultMessage: "Free cancellation",
}),
save: intl.formatMessage({
defaultMessage: "Non-refundable",
}),
}
const mappedRooms = rooms.map((booking, idx) => {
const room = getBookedHotelRoom(roomCategories, booking.roomTypeCode)
return mapRoomDetails({
booking,
rates,
room,
roomNumber: idx + 1,
})
})
const bookedRoom = mappedRooms[0]
const allRoomsAreCancelled = isAllRoomsCancelled(mappedRooms)
const totalPoints = calculateTotalPoints(mappedRooms, allRoomsAreCancelled)
const totalPrice = calculateTotalPrice(
mappedRooms,
bookedRoom.currencyCode,
intl,
allRoomsAreCancelled
)
const mainRoom = mappedRooms.find((r) => r.mainRoom) ?? bookedRoom
return create<MyStayState>()((set) => {
return {
allRoomsAreCancelled,
bookedRoom,
breakfastPackages,
hotel,
mainRoom,
manageStay: false,
refId,
rooms: mappedRooms,
savedCreditCards,
totalPoints,
totalPrice,
actions: {
closeManageStay() {
return set(
produce((state: MyStayState) => {
state.manageStay = false
})
)
},
openManageStay() {
return set(
produce((state: MyStayState) => {
state.manageStay = true
})
)
},
},
}
})
}
export function useMyStayStore<T>(selector: (store: MyStayState) => T) {
const store = useContext(MyStayContext)
if (!store) {
throw new Error("useMyStayStore must be used within MyStayProvider")
}
return useStore(store, selector)
}

View File

@@ -1,50 +0,0 @@
import { create } from "zustand"
type ActiveView =
| "actionPanel"
| "cancelStay"
| "modifyStay"
| "guaranteeLateArrival"
interface ManageStayState {
isOpen: boolean
activeView: ActiveView
currentStep: number
isLoading: boolean
actions: {
setIsOpen: (isOpen: boolean) => void
setActiveView: (view: ActiveView) => void
setCurrentStep: (step: number) => void
setIsLoading: (isLoading: boolean) => void
handleForward: () => void
handleCloseView: () => void
handleCloseModal: () => void
}
}
export const useManageStayStore = create<ManageStayState>((set) => ({
isOpen: false,
activeView: "actionPanel",
currentStep: 1,
isLoading: false,
actions: {
setIsOpen: (isOpen) => set({ isOpen }),
setActiveView: (activeView) => set({ activeView }),
setCurrentStep: (currentStep) => set({ currentStep }),
setIsLoading: (isLoading) => set({ isLoading }),
handleForward: () =>
set((state) => ({ currentStep: state.currentStep + 1 })),
handleCloseView: () =>
set({
currentStep: 1,
isLoading: false,
activeView: "actionPanel",
}),
handleCloseModal: () =>
set({
currentStep: 1,
isOpen: false,
activeView: "actionPanel",
}),
},
}))

View File

@@ -1,198 +0,0 @@
import { create } from "zustand"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import { CurrencyEnum } from "@/types/enums/currency"
import type { Packages } from "@/types/requests/packages"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export type Room = Pick<
BookingConfirmation["booking"],
| "hotelId"
| "adults"
| "checkInDate"
| "checkOutDate"
| "childrenAges"
| "createDateTime"
| "rateDefinition"
| "guaranteeInfo"
| "linkedReservations"
| "confirmationNumber"
| "cancellationNumber"
| "bookingCode"
| "cheques"
| "vouchers"
| "isCancelable"
| "multiRoom"
| "canChangeDate"
| "guest"
| "roomTypeCode"
| "currencyCode"
| "vatPercentage"
| "roomPoints"
| "totalPrice"
| "totalPriceExVat"
| "vatAmount"
> & {
roomName: string
roomNumber: number | null
isCancelled: boolean
childrenInRoom: Child[]
childrenAsString: string
terms: string | null
packages: Packages | null
bedType: BedTypeSchema
roomPrice: RoomPrice
breakfast: BreakfastPackage | null
mainRoom: boolean
priceType: PriceTypeEnum
}
interface MyStayRoomDetailsState {
bookedRoom: Room
linkedReservationRooms: Room[]
actions: {
addBookedRoom: (room: Room) => void
updateBookedRoom: (room: Room) => void
addLinkedReservationRoom: (room: Room) => void
updateLinkedReservationRoom: (room: Room) => void
}
}
export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
(set) => ({
bookedRoom: {
hotelId: "",
roomTypeCode: "",
adults: 0,
childrenAges: [],
checkInDate: new Date(),
checkOutDate: new Date(),
confirmationNumber: "",
cancellationNumber: null,
bookingCode: null,
cheques: 0,
vouchers: 0,
currencyCode: CurrencyEnum.Unknown,
guest: {
email: "",
firstName: "",
lastName: "",
membershipNumber: "",
phoneNumber: "",
countryCode: "",
},
rateDefinition: {
breakfastIncluded: false,
cancellationRule: null,
cancellationText: null,
generalTerms: [],
isMemberRate: false,
mustBeGuaranteed: false,
rateCode: "",
title: null,
},
roomPoints: 0,
roomPrice: {
perNight: {
requested: {
price: 0,
currency: CurrencyEnum.Unknown,
},
local: {
price: 0,
currency: CurrencyEnum.Unknown,
},
},
perStay: {
requested: {
price: 0,
currency: CurrencyEnum.Unknown,
},
local: {
price: 0,
currency: CurrencyEnum.Unknown,
},
},
},
vatPercentage: 0,
vatAmount: 0,
totalPriceExVat: 0,
totalPrice: 0,
createDateTime: new Date(),
canChangeDate: false,
multiRoom: false,
mainRoom: false,
roomName: "",
roomNumber: null,
isCancelled: false,
childrenInRoom: [],
childrenAsString: "",
terms: null,
packages: null,
bedType: {
description: "",
roomTypeCode: "",
},
breakfast: null,
linkedReservations: [],
isCancelable: false,
priceType: PriceTypeEnum.money,
},
linkedReservationRooms: [],
actions: {
addBookedRoom: (room) => {
set({ bookedRoom: room })
},
updateBookedRoom: (room) => {
set({ bookedRoom: room })
},
addLinkedReservationRoom: (room) => {
set((state) => {
// Check if room exists in bookedRooms
const existsInBookedRoom =
state.bookedRoom.confirmationNumber === room.confirmationNumber
if (existsInBookedRoom) {
return state
}
// Check if room with this ID already exists in linkedReservationRooms
const existingIndex = state.linkedReservationRooms.findIndex(
(r) => r.confirmationNumber === room.confirmationNumber
)
let newRooms = [...state.linkedReservationRooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
return {
linkedReservationRooms: newRooms,
}
})
},
updateLinkedReservationRoom: (room) => {
set((state) => {
const existingIndex = state.linkedReservationRooms.findIndex(
(r) => r.confirmationNumber === room.confirmationNumber
)
let newRooms = [...state.linkedReservationRooms]
if (existingIndex >= 0) {
newRooms[existingIndex] = room
}
return {
linkedReservationRooms: newRooms,
}
})
},
},
})
)

View File

@@ -1,75 +0,0 @@
import { create } from "zustand"
import { CurrencyEnum } from "@/types/enums/currency"
interface RoomPrice {
id: string
totalPrice: number
currencyCode: CurrencyEnum
isMainBooking?: boolean
roomPoints: number
}
interface MyStayTotalPriceState {
currencyCode: CurrencyEnum
rooms: RoomPrice[]
totalCheques: number
totalPoints: number
totalPrice: number | null
totalVouchers: number
actions: {
// Add a single room price
addRoomPrice: (room: RoomPrice) => void
}
}
export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
(set) => ({
rooms: [],
totalPrice: null,
totalPoints: 0,
totalCheques: 0,
totalVouchers: 0,
currencyCode: CurrencyEnum.Unknown,
actions: {
addRoomPrice: (room) => {
set((state) => {
// Check if room with this ID already exists
const existingIndex = state.rooms.findIndex((r) => r.id === room.id)
let newRooms = [...state.rooms]
if (existingIndex >= 0) {
// Update existing room
newRooms[existingIndex] = room
} else {
// Add new room
newRooms.push(room)
}
// Get currency from main booking or first room
const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0]
const currencyCode = mainRoom?.currencyCode ?? CurrencyEnum.Unknown
// Calculate total (only same currency for now)
const total = newRooms.reduce((sum, r) => {
if (r.currencyCode === currencyCode) {
return sum + r.totalPrice
}
return sum
}, 0)
const totalPoints = newRooms.reduce((sum, r) => {
return sum + (r.roomPoints ?? 0)
}, 0)
return {
rooms: newRooms,
totalPrice: total,
currencyCode,
totalPoints,
}
})
},
},
})
)

View File

@@ -3,14 +3,14 @@ import { create } from "zustand"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import type { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { User } from "@/types/user"
import type { SafeUser } from "@/types/user"
interface SidePeekState {
activeSidePeek: SidePeekEnum | null
hotelId: string | null
roomTypeCode: string | null
showCTA: boolean
user: User | null
user: SafeUser
confirmationNumber: string
openSidePeek: ({
key,
@@ -24,7 +24,7 @@ interface SidePeekState {
hotelId: string
roomTypeCode?: string
showCTA?: boolean
user?: User
user?: SafeUser
confirmationNumber?: string
}) => void
closeSidePeek: () => void