From ec087a3d15fafb38f9a8313f89e6cfc0517a9713 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Fri, 25 Apr 2025 14:08:14 +0200 Subject: [PATCH] feat: refactor of my stay --- .../gla-payment-callback/page.tsx | 2 +- .../hotelreservation/my-stay/layout.tsx | 14 - .../hotelreservation/my-stay/page.module.css | 75 ++++ .../hotelreservation/my-stay/page.tsx | 233 ++++++++++- .../hotelreservation/my-stay/layout.tsx | 14 - .../hotelreservation/my-stay/page.module.css | 75 ++++ .../webview/hotelreservation/my-stay/page.tsx | 233 ++++++++++- .../Rewards/Redeem/ConfirmClose.tsx | 2 +- .../components/DatePicker/Range/Desktop.tsx | 3 +- .../components/DatePicker/Range/Mobile.tsx | 3 +- .../components/DatePicker/Single/Desktop.tsx | 3 +- .../components/DatePicker/Single/Mobile.tsx | 3 +- .../components/DatePicker/index.tsx | 13 - .../components/DatePicker/locales.ts | 11 + .../PriceDetails/index.tsx | 2 - .../EnterDetails/Confirm/index.tsx | 4 +- .../EnterDetails/Payment/PaymentClient.tsx | 4 +- .../Payment => }/MySavedCards/index.tsx | 2 +- .../MySavedCards/mySavedCards.module.css | 0 .../Steps/ConfirmationStep/index.tsx | 8 +- .../Steps/DeliveryDetailsStep/index.tsx | 2 +- .../AddAncillaryFlowModal/index.tsx | 19 +- .../Ancillaries/GuaranteeCallback/index.tsx | 11 +- .../MyStay/GuaranteeLateArrival/index.tsx | 239 ----------- .../MyStay/GuestDetails/Details.tsx | 315 ++++++++++++++ .../MyStay/GuestDetails/index.tsx | 324 +-------------- .../HotelReservation/MyStay/Header/index.tsx | 11 +- .../Actions/AddToCalendarButton.tsx | 40 -- .../CancelStayPriceContainer/index.tsx | 40 -- .../Actions/CancelStay/Confirmation/index.tsx | 119 ------ .../CancelStay/FinalConfirmation/index.tsx | 36 -- .../Actions/CancelStay/hooks/useCancelStay.ts | 178 -------- .../ActionPanel/Actions/CancelStay/index.tsx | 136 ------ .../ActionPanel/Actions/CancelStay/utils.ts | 162 -------- .../Confirmation/confirmation.module.css | 32 -- .../Actions/ModifyStay/Confirmation/index.tsx | 165 -------- .../Actions/ModifyStay/hooks/useModifyStay.ts | 176 -------- .../ActionPanel/Actions/ModifyStay/index.tsx | 202 --------- .../ActionPanel/actionPanel.module.css | 79 ---- .../MyStay/ManageStay/ActionPanel/index.tsx | 255 ------------ .../MyStay/ManageStay/ActionPanel/utils.ts | 42 -- .../MyStay/ManageStay/index.tsx | 102 ----- .../MyStay/ManageStay/manangeStay.module.css | 3 - .../MyStay/MultiRoom/PriceType.tsx | 62 --- .../HotelReservation/MyStay/Points/index.tsx | 38 -- .../HotelReservation/MyStay/Price/index.tsx | 39 -- .../MyStay/PriceDetails/index.tsx | 26 +- .../MyStay/PriceDetails/mapToPrice.ts | 2 +- .../index.tsx => PriceType/Cheques.tsx} | 14 +- .../MyStay/PriceType/Points.tsx | 42 ++ .../MyStay/PriceType/Price/index.tsx | 21 + .../{ => PriceType}/Price/price.module.css | 0 .../MyStay/PriceType/Vouchers.tsx | 42 ++ .../{PriceType.tsx => PriceType/index.tsx} | 43 +- .../Actions/Cancelled/CustomerSupport.tsx | 19 + .../Actions/Cancelled/cancelled.module.css | 18 + .../ReferenceCard/Actions/Cancelled/index.tsx | 21 + .../customerSupport.module.css | 30 ++ .../Actions/CustomerSupportModal/index.tsx | 78 ++++ .../AddToCalendar/AddToCalendarButton.tsx | 43 ++ .../Actions/AddToCalendar/button.module.css | 18 + .../Actions/AddToCalendar/index.tsx | 57 +++ .../ManageStay/Actions/CancelStay/Alerts.tsx | 46 +++ .../Steps/CancelStayPriceContainer.tsx | 78 ++++ .../Steps/Confirmation/Multiroom/index.tsx | 111 +++++ .../Multiroom/multiroom.module.css | 74 ++++ .../Confirmation/confirmation.module.css | 9 + .../CancelStay/Steps/Confirmation/index.tsx | 127 ++++++ .../finalConfirmation.module.css | 9 + .../Steps/FinalConfirmation/index.tsx | 170 ++++++++ .../Actions/CancelStay/Steps/index.tsx | 60 +++ .../Actions/CancelStay/cancelStay.module.css | 0 .../ManageStay/Actions/CancelStay/index.tsx | 28 ++ .../ChangeDates/Alerts/CannotChangeDate.tsx | 42 ++ .../ChangeDates/Alerts/MultiRoomBooking.tsx | 42 ++ .../ChangeDates/Alerts/NotMainRoom.tsx | 42 ++ .../Actions/ChangeDates/Alerts/index.tsx | 31 ++ .../Steps/Confirmation/PriceAndDate/index.tsx | 64 +++ .../PriceAndDate/priceAndDate.module.css | 18 + .../Confirmation/confirmation.module.css | 11 + .../ChangeDates/Steps/Confirmation/index.tsx | 183 ++++++++ .../ChangeDates/Steps/Form/Alerts/Error.tsx | 21 + .../Steps/Form/Alerts/NoAvailability.tsx | 21 + .../CalendarButton/calendarButton.module.css | 0 .../Form}/NewDates/CalendarButton/index.tsx | 0 .../Steps/Form}/NewDates/index.tsx | 108 ++--- .../Steps/Form}/NewDates/newDates.module.css | 0 .../Actions/ChangeDates/Steps/Form/index.tsx | 85 ++++ .../Actions/ChangeDates/Steps/index.tsx | 136 ++++++ .../ManageStay/Actions/ChangeDates/index.tsx | 49 +++ .../Actions/CustomerSupport/index.tsx | 18 + .../Form/form.module.css} | 88 ++-- .../GuaranteeLateArrival/Form/index.tsx | 194 +++++++++ .../GuaranteeLateArrival/Form}/schema.ts | 0 .../Actions/GuaranteeLateArrival/index.tsx | 69 ++++ .../Actions/ViewAndPrintReceipt/index.tsx | 56 +++ .../ViewAndPrintReceipt/view.module.css | 7 + .../ManageStay/Actions/actions.module.css | 5 + .../NotCancelled/ManageStay/Actions/index.tsx | 21 + .../NotCancelled/ManageStay/Actions/utils.ts | 7 + .../NotCancelled/ManageStay/Info/index.tsx | 44 ++ .../ManageStay/Info/info.module.css | 29 ++ .../Actions/NotCancelled/ManageStay/index.tsx | 65 +++ .../ManageStay/manageStay.module.css | 52 +++ .../Actions/NotCancelled/index.tsx | 34 ++ .../NotCancelled/notCancelled.module.css | 9 + .../ReferenceCard/Actions/actions.module.css | 12 + .../MyStay/ReferenceCard/Actions/index.tsx | 16 + .../BookingCode/bookingCode.module.css} | 6 +- .../ReferenceCard/BookingCode/index.tsx | 41 ++ .../Cancellations/cancellations.module.css | 16 + .../ReferenceCard/Cancellations/index.tsx | 43 ++ .../ReferenceCard/Dates/dates.module.css | 15 + .../MyStay/ReferenceCard/Dates/index.tsx | 53 +++ .../GuaranteeInfo/guaranteeInfo.module.css | 15 + .../ReferenceCard/GuaranteeInfo/index.tsx | 43 ++ .../ReferenceCard/Guests/guests.module.css | 20 + .../MyStay/ReferenceCard/Guests/index.tsx | 61 +++ .../Modal/Button/button.module.css | 14 + .../ReferenceCard/Modal/Button/index.tsx | 31 ++ .../Modal/ModalContent/Body/body.module.css | 15 + .../Modal/ModalContent/Body/index.tsx | 5 + .../ModalContent/Footer/footer.module.css | 7 + .../Modal/ModalContent/Footer/index.tsx | 64 +++ .../ModalContent/Header/header.module.css | 15 + .../Modal/ModalContent/Header/index.tsx | 24 ++ .../Modal/ModalContent/index.tsx | 15 + .../ModalContent/modalContent.module.css | 4 + .../MyStay/ReferenceCard/Modal/index.tsx | 17 + .../ReferenceCard/Modal/modal.module.css | 70 ++++ .../PriceContainer/index.tsx | 16 +- .../PriceContainer/priceContainer.module.css | 0 .../MyStay/ReferenceCard/Reference/index.tsx | 49 +++ .../Reference/reference.module.css | 10 + .../ReferenceCard/ReferenceCardSkeleton.tsx | 35 -- .../MyStay/ReferenceCard/Room/index.tsx | 43 ++ .../MyStay/ReferenceCard/Room/room.module.css | 15 + .../MyStay/ReferenceCard/index.tsx | 391 +----------------- .../ReferenceCard/referenceCard.module.css | 75 +--- .../MultiRoom/MultiRoomSkeleton.tsx | 0 .../{ => Rooms}/MultiRoom/ToggleSidePeek.tsx | 0 .../MyStay/{ => Rooms}/MultiRoom/index.tsx | 245 ++++------- .../MultiRoom/multiRoom.module.css | 0 .../MultiRoom/toggleSidePeek.module.css | 0 .../{ => Rooms}/SingleRoom/ToggleSidePeek.tsx | 0 .../MyStay/{ => Rooms}/SingleRoom/index.tsx | 221 +++++----- .../{ => Rooms}/SingleRoom/room.module.css | 18 + .../MyStay/Rooms/TotalPrice.tsx | 26 ++ .../MyStay/Rooms/TotalPrice/index.tsx | 80 ---- .../HotelReservation/MyStay/Rooms/index.tsx | 74 +--- .../MyStay/SingleRoom/PriceType.tsx | 63 --- .../index.tsx => TrackGuarantee.tsx} | 20 +- .../HotelReservation/MyStay/index.tsx | 192 --------- .../HotelReservation/MyStay/myStay.module.css | 75 +--- .../utils.ts => utils/ancillaries.ts} | 2 +- .../MyStay/utils/mapRoomDetails.ts | 186 +++++---- .../Payment => }/PaymentOption/index.tsx | 0 .../PaymentOption/paymentOption.module.css | 0 .../PaymentOption/paymentOption.ts | 0 .../PriceDetailsTable/Breakfast.tsx | 2 +- .../PriceDetailsTable/index.tsx | 2 +- .../RoomPackageFilter/Form/index.tsx | 2 +- .../HotelReservation/SidePeek/index.tsx | 23 +- .../components/ImageGallery/index.tsx | 2 +- apps/scandic-web/components/Modal/modal.ts | 2 +- .../ParkingPrices/index.tsx | 1 + .../SidePeeks/BookedRoomSidePeek/index.tsx | 102 +++-- .../TempDesignSystem/Form/Checkbox/index.tsx | 9 +- apps/scandic-web/contexts/MyStay.ts | 5 + .../hooks/booking/useGuaranteeBooking.ts | 24 +- apps/scandic-web/lib/dt.ts | 2 + .../lib/trpc/memoizedRequests/index.ts | 7 + .../providers/AddAncillaryProvider.tsx | 2 +- apps/scandic-web/providers/MyStay.tsx | 110 +++++ .../material-symbols/rounded-112272ae.woff2 | Bin 34088 -> 0 bytes .../material-symbols/rounded-a03ed056.woff2 | Bin 0 -> 34644 bytes .../server/routers/booking/input.ts | 15 + .../server/routers/booking/mutation.ts | 70 ++-- .../server/routers/booking/output.ts | 4 +- .../server/routers/booking/query.ts | 117 ++++-- .../server/routers/booking/utils.ts | 101 +++++ .../server/routers/hotels/utils.ts | 1 + .../stores/my-stay/add-ancillary-flow.ts | 14 +- apps/scandic-web/stores/my-stay/helpers.ts | 77 ++++ apps/scandic-web/stores/my-stay/index.ts | 106 +++++ .../stores/my-stay/manageStayStore.ts | 50 --- .../stores/my-stay/myStayRoomDetailsStore.ts | 198 --------- .../stores/my-stay/myStayTotalPrice.ts | 75 ---- apps/scandic-web/stores/sidepeek.ts | 6 +- .../types/components/blocks/surprises.ts | 4 +- .../types/components/datepicker.ts | 6 - .../types/components/header/headerLink.ts | 2 +- .../bookingConfirmation.ts | 3 +- .../hotelReservation/myStay/cancelStay.ts | 21 +- .../myStay/{modifyDate.ts => changeDates.ts} | 14 +- .../hotelReservation/toggleSidePeekProps.ts | 4 +- .../components/myPages/myStay/ancillaries.ts | 2 +- .../sidePeeks/bookedRoomSidePeek.ts | 4 +- apps/scandic-web/types/contexts/my-stay.ts | 3 + apps/scandic-web/types/hotel.ts | 2 + apps/scandic-web/types/stores/my-stay.ts | 89 ++++ apps/scandic-web/types/stores/rates.ts | 6 +- .../trpc/routers/booking/confirmation.ts | 5 +- .../lib/components/RateCard/Modal/modal.ts | 2 +- packages/design-system/lib/fonts.css | 2 +- .../material-symbols/rounded-112272ae.woff2 | Bin 34088 -> 0 bytes .../material-symbols/rounded-a03ed056.woff2 | Bin 0 -> 34644 bytes scripts/material-symbols-update.mjs | 3 + 208 files changed, 5458 insertions(+), 4569 deletions(-) delete mode 100644 apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx create mode 100644 apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css delete mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css create mode 100644 apps/scandic-web/components/DatePicker/locales.ts rename apps/scandic-web/components/HotelReservation/{EnterDetails/Payment => }/MySavedCards/index.tsx (93%) rename apps/scandic-web/components/HotelReservation/{EnterDetails/Payment => }/MySavedCards/mySavedCards.module.css (100%) delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{Cheques/index.tsx => PriceType/Cheques.tsx} (63%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{ => PriceType}/Price/price.module.css (100%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{PriceType.tsx => PriceType/index.tsx} (59%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel => ReferenceCard/Actions/NotCancelled/ManageStay}/Actions/CancelStay/cancelStay.module.css (100%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel/Actions/ModifyStay => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form}/NewDates/CalendarButton/calendarButton.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel/Actions/ModifyStay => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form}/NewDates/CalendarButton/index.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel/Actions/ModifyStay => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form}/NewDates/index.tsx (64%) rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel/Actions/ModifyStay => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form}/NewDates/newDates.module.css (100%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{GuaranteeLateArrival/guaranteeLateArrival.module.css => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css} (54%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{GuaranteeLateArrival => ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form}/schema.ts (100%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{Rooms/TotalPrice/totalPrice.module.css => ReferenceCard/BookingCode/bookingCode.module.css} (50%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel => ReferenceCard}/PriceContainer/index.tsx (92%) rename apps/scandic-web/components/HotelReservation/MyStay/{ManageStay/ActionPanel => ReferenceCard}/PriceContainer/priceContainer.module.css (100%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/MultiRoom/MultiRoomSkeleton.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/MultiRoom/ToggleSidePeek.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/MultiRoom/index.tsx (55%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/MultiRoom/multiRoom.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/MultiRoom/toggleSidePeek.module.css (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/SingleRoom/ToggleSidePeek.tsx (100%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/SingleRoom/index.tsx (74%) rename apps/scandic-web/components/HotelReservation/MyStay/{ => Rooms}/SingleRoom/room.module.css (99%) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx => TrackGuarantee.tsx} (90%) delete mode 100644 apps/scandic-web/components/HotelReservation/MyStay/index.tsx rename apps/scandic-web/components/HotelReservation/MyStay/{Ancillaries/utils.ts => utils/ancillaries.ts} (94%) rename apps/scandic-web/components/HotelReservation/{EnterDetails/Payment => }/PaymentOption/index.tsx (100%) rename apps/scandic-web/components/HotelReservation/{EnterDetails/Payment => }/PaymentOption/paymentOption.module.css (100%) rename apps/scandic-web/components/HotelReservation/{EnterDetails/Payment => }/PaymentOption/paymentOption.ts (100%) create mode 100644 apps/scandic-web/contexts/MyStay.ts create mode 100644 apps/scandic-web/providers/MyStay.tsx delete mode 100644 apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 create mode 100644 apps/scandic-web/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 create mode 100644 apps/scandic-web/stores/my-stay/helpers.ts create mode 100644 apps/scandic-web/stores/my-stay/index.ts delete mode 100644 apps/scandic-web/stores/my-stay/manageStayStore.ts delete mode 100644 apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts delete mode 100644 apps/scandic-web/stores/my-stay/myStayTotalPrice.ts rename apps/scandic-web/types/components/hotelReservation/myStay/{modifyDate.ts => changeDates.ts} (62%) create mode 100644 apps/scandic-web/types/contexts/my-stay.ts create mode 100644 apps/scandic-web/types/stores/my-stay.ts delete mode 100644 packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 create mode 100644 packages/design-system/public/_static/fonts/material-symbols/rounded-a03ed056.woff2 diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx index 5c4a83629..571a44d89 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/gla-payment-callback/page.tsx @@ -8,7 +8,7 @@ import { myStay } from "@/constants/routes/myStay" import { serverClient } from "@/lib/trpc/server" import GuaranteeCallback from "@/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback" -import TrackGuarantee from "@/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback" +import TrackGuarantee from "@/components/HotelReservation/MyStay/TrackGuarantee" import LoadingSpinner from "@/components/LoadingSpinner" import type { LangParams, PageArgs } from "@/types/params" diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx deleted file mode 100644 index c7bc50ba5..000000000 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function HotelReservationLayout({ - children, -}: React.PropsWithChildren>) { - return ( -
- {children} - -
- ) -} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css new file mode 100644 index 000000000..774114c7d --- /dev/null +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.module.css @@ -0,0 +1,75 @@ +.main { + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.imageContainer { + position: absolute; + width: 100%; + height: 480px; +} + +.blurOverlay { + position: absolute; + inset: 0; + backdrop-filter: blur(12px); + pointer-events: none; + z-index: 1; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.5) 0%, + transparent 100% + ); +} + +.image { + object-fit: cover; + object-position: center; +} + +.headerContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + gap: 80px; + margin: 0 auto; + position: relative; + z-index: 2; + padding-bottom: var(--Spacing-x3); +} + +.form { + max-width: 640px; + margin-left: auto; + margin-right: auto; + padding: var(--Spacing-x5) 0; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x2); +} + +.logIn { + padding: var(--Spacing-x9) var(--Spacing-x2); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + align-items: center; + color: var(--Scandic-Grey-100); +} + +@media (min-width: 768px) { + .content { + width: var(--max-width-content); + padding-bottom: 160px; + } +} diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx index aa1ec16aa..3308028c0 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/my-stay/page.tsx @@ -1,21 +1,234 @@ +import { cookies } from "next/headers" import { notFound } from "next/navigation" -import { Suspense } from "react" -import { MyStay } from "@/components/HotelReservation/MyStay" -import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { env } from "@/env/server" +import { dt } from "@/lib/dt" +import { + getAncillaryPackages, + getBookingConfirmation, + getLinkedReservations, + getPackages, + getProfileSafely, + getSavedPaymentCardsSafely, +} from "@/lib/trpc/memoizedRequests" +import { decrypt } from "@/server/routers/utils/encryption" + +import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_UNAUTHORIZED, +} from "@/components/HotelReservation/MyStay/accessBooking" +import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries" +import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary" +import { Header } from "@/components/HotelReservation/MyStay/Header" +import Promo from "@/components/HotelReservation/MyStay/Promo" +import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" +import Rooms from "@/components/HotelReservation/MyStay/Rooms" +import SidePeek from "@/components/HotelReservation/SidePeek" +import Image from "@/components/Image" +import { getIntl } from "@/i18n" +import { setLang } from "@/i18n/serverContext" +import MyStayProvider from "@/providers/MyStay" +import { getCurrentWebUrl } from "@/utils/url" + +import styles from "./page.module.css" + +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { LangParams, PageArgs } from "@/types/params" -export default function MyStayPage({ +export default async function MyStay({ + params, searchParams, }: PageArgs) { - if (!searchParams.RefId) { + setLang(params.lang) + const refId = searchParams.RefId + + if (!refId) { notFound() } - return ( - }> - - - ) + const value = decrypt(refId) + if (!value) { + return notFound() + } + + const [confirmationNumber, lastName] = value.split(",") + const bookingConfirmation = await getBookingConfirmation(confirmationNumber) + if (!bookingConfirmation) { + return notFound() + } + + const { additionalData, booking, hotel, roomCategories } = bookingConfirmation + + const user = await getProfileSafely() + const bv = cookies().get("bv")?.value + const intl = await getIntl() + + const access = accessBooking(booking.guest, lastName, user, bv) + + if (access === ACCESS_GRANTED) { + const lang = params.lang + const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") + const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") + + const linkedReservationsPromise = getLinkedReservations({ + rooms: booking.linkedReservations, + }) + + const packagesInput = { + adults: booking.adults, + children: booking.childrenAges.length, + endDate: toDate, + hotelId: hotel.operaId, + lang, + startDate: fromDate, + packageCodes: [ + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + ], + } + const supportedCards = hotel.merchantInformationData.cards + const savedPaymentCardsInput = { supportedCards } + + const hasBreakfastPackage = booking.packages.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const breakfastIncluded = booking.rateDefinition.breakfastIncluded + const shouldFetchBreakfastPackages = + !hasBreakfastPackage && !breakfastIncluded + if (shouldFetchBreakfastPackages) { + void getPackages(packagesInput) + } + void getSavedPaymentCardsSafely(savedPaymentCardsInput) + + const ancillaryPackages = await getAncillaryPackages({ + fromDate, + hotelId: hotel.operaId, + toDate, + }) + + let breakfastPackages = null + if (shouldFetchBreakfastPackages) { + breakfastPackages = await getPackages(packagesInput) + } + const savedCreditCards = await getSavedPaymentCardsSafely( + savedPaymentCardsInput + ) + + const imageSrc = + hotel.hotelContent.images.imageSizes.large ?? + additionalData.gallery?.heroImages[0]?.imageSizes.large ?? + hotel.galleryImages[0]?.imageSizes.large + + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + const promoUrl = env.HIDE_FOR_NEXT_RELEASE + ? new URL(getCurrentWebUrl({ path: "/", lang })) + : new URL(`${baseUrl}/${lang}/`) + + promoUrl.searchParams.set("hotel", hotel.operaId) + + return ( + +
+
+
+ {imageSrc && ( + {hotel.name} + )} +
+
+
+
+ +
+ {booking.showAncillaries && ( + + )} + + + + + +
+
+ +
+ ) + } + + if (access === ERROR_BAD_REQUEST) { + return ( +
+
+ +
+
+ ) + } + + if (access === ERROR_UNAUTHORIZED) { + return ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "You need to be logged in to view your booking", + })} +

+
+ +

+ {intl.formatMessage({ + defaultMessage: + "And you need to be logged in with the same member account that made the booking.", + })} +

+
+
+
+ ) + } + + return notFound() } diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx deleted file mode 100644 index 97481c9bf..000000000 --- a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default function HotelReservationLayout({ - children, -}: React.PropsWithChildren>) { - return ( - <> - {children} - - - ) -} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css new file mode 100644 index 000000000..774114c7d --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.module.css @@ -0,0 +1,75 @@ +.main { + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.imageContainer { + position: absolute; + width: 100%; + height: 480px; +} + +.blurOverlay { + position: absolute; + inset: 0; + backdrop-filter: blur(12px); + pointer-events: none; + z-index: 1; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.5) 0%, + transparent 100% + ); +} + +.image { + object-fit: cover; + object-position: center; +} + +.headerContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} + +.content { + width: 100%; + display: flex; + flex-direction: column; + gap: 80px; + margin: 0 auto; + position: relative; + z-index: 2; + padding-bottom: var(--Spacing-x3); +} + +.form { + max-width: 640px; + margin-left: auto; + margin-right: auto; + padding: var(--Spacing-x5) 0; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: 0 var(--Spacing-x2); +} + +.logIn { + padding: var(--Spacing-x9) var(--Spacing-x2); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + align-items: center; + color: var(--Scandic-Grey-100); +} + +@media (min-width: 768px) { + .content { + width: var(--max-width-content); + padding-bottom: 160px; + } +} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx index aa1ec16aa..e083c1cde 100644 --- a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx @@ -1,21 +1,234 @@ +import { cookies } from "next/headers" import { notFound } from "next/navigation" -import { Suspense } from "react" -import { MyStay } from "@/components/HotelReservation/MyStay" -import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { env } from "@/env/server" +import { dt } from "@/lib/dt" +import { + getAncillaryPackages, + getBookingConfirmation, + getLinkedReservations, + getPackages, + getProfileSafely, + getSavedPaymentCardsSafely, +} from "@/lib/trpc/memoizedRequests" +import { decrypt } from "@/server/routers/utils/encryption" + +import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm" +import accessBooking, { + ACCESS_GRANTED, + ERROR_BAD_REQUEST, + ERROR_UNAUTHORIZED, +} from "@/components/HotelReservation/MyStay/accessBooking" +import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries" +import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary" +import { Header } from "@/components/HotelReservation/MyStay/Header" +import Promo from "@/components/HotelReservation/MyStay/Promo" +import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard" +import Rooms from "@/components/HotelReservation/MyStay/Rooms" +import SidePeek from "@/components/HotelReservation/SidePeek" +import Image from "@/components/Image" +import { getIntl } from "@/i18n" +import { setLang } from "@/i18n/serverContext" +import MyStayProvider from "@/providers/MyStay" +import { getCurrentWebUrl } from "@/utils/url" + +import styles from "./page.module.css" + +import { BreakfastPackageEnum } from "@/types/enums/breakfast" import type { LangParams, PageArgs } from "@/types/params" -export default function MyStayPage({ +export default async function MyStay({ + params, searchParams, }: PageArgs) { - if (!searchParams.RefId) { + setLang(params.lang) + const refId = searchParams.RefId + + if (!refId) { notFound() } - return ( - }> - - - ) + const value = decrypt(refId) + if (!value) { + return notFound() + } + + const [confirmationNumber, lastName] = value.split(",") + const bookingConfirmation = await getBookingConfirmation(confirmationNumber) + if (!bookingConfirmation) { + return notFound() + } + + const { additionalData, booking, hotel, roomCategories } = bookingConfirmation + + const user = await getProfileSafely() + const bv = cookies().get("bv")?.value + const intl = await getIntl() + + const access = accessBooking(booking.guest, lastName, user, bv) + + if (access === ACCESS_GRANTED) { + const lang = params.lang + const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD") + const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD") + + const linkedReservationsPromise = getLinkedReservations({ + rooms: booking.linkedReservations, + }) + + const packagesInput = { + adults: booking.adults, + children: booking.childrenAges.length, + endDate: toDate, + hotelId: hotel.operaId, + lang, + startDate: fromDate, + packageCodes: [ + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + ], + } + const supportedCards = hotel.merchantInformationData.cards + const savedPaymentCardsInput = { supportedCards } + + const hasBreakfastPackage = booking.packages.find( + (pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST + ) + const breakfastIncluded = booking.rateDefinition.breakfastIncluded + const alreadyHasABreakfastSelection = + !hasBreakfastPackage && !breakfastIncluded + if (alreadyHasABreakfastSelection) { + void getPackages(packagesInput) + } + void getSavedPaymentCardsSafely(savedPaymentCardsInput) + + const ancillaryPackages = await getAncillaryPackages({ + fromDate, + hotelId: hotel.operaId, + toDate, + }) + + let breakfastPackages = null + if (alreadyHasABreakfastSelection) { + breakfastPackages = await getPackages(packagesInput) + } + const savedCreditCards = await getSavedPaymentCardsSafely( + savedPaymentCardsInput + ) + + const imageSrc = + hotel.hotelContent.images.imageSizes.large ?? + additionalData.gallery?.heroImages[0]?.imageSizes.large ?? + hotel.galleryImages[0]?.imageSizes.large + + const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" + const promoUrl = env.HIDE_FOR_NEXT_RELEASE + ? new URL(getCurrentWebUrl({ path: "/", lang })) + : new URL(`${baseUrl}/${lang}/`) + + promoUrl.searchParams.set("hotel", hotel.operaId) + + return ( + +
+
+
+ {imageSrc && ( + {hotel.name} + )} +
+
+
+
+ +
+ {booking.showAncillaries && ( + + )} + + + + + +
+
+ +
+ ) + } + + if (access === ERROR_BAD_REQUEST) { + return ( +
+
+ +
+
+ ) + } + + if (access === ERROR_UNAUTHORIZED) { + return ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "You need to be logged in to view your booking", + })} +

+
+ +

+ {intl.formatMessage({ + defaultMessage: + "And you need to be logged in with the same member account that made the booking.", + })} +

+
+
+
+ ) + } + + return notFound() } diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx index b1f9ad451..9417ead55 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/ConfirmClose.tsx @@ -8,7 +8,7 @@ import useRedeemFlow from "./useRedeemFlow" import styles from "./redeem.module.css" -export function ConfirmClose({ close }: { close: VoidFunction }) { +export function ConfirmClose({ close }: { close: () => void }) { const intl = useIntl() const { setRedeemStep } = useRedeemFlow() diff --git a/apps/scandic-web/components/DatePicker/Range/Desktop.tsx b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx index 4941dd3c4..ff1b235c8 100644 --- a/apps/scandic-web/components/DatePicker/Range/Desktop.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Desktop.tsx @@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" @@ -23,7 +25,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker" export default function DatePickerRangeDesktop({ close, handleOnSelect, - locales, selectedRange, }: DatePickerRangeProps) { const lang = useLang() diff --git a/apps/scandic-web/components/DatePicker/Range/Mobile.tsx b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx index 2b678c64c..0079b095d 100644 --- a/apps/scandic-web/components/DatePicker/Range/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Range/Mobile.tsx @@ -12,6 +12,8 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./mobile.module.css" import classNames from "react-day-picker/style.module.css" @@ -20,7 +22,6 @@ import type { DatePickerRangeProps } from "@/types/components/datepicker" export default function DatePickerRangeMobile({ close, handleOnSelect, - locales, selectedRange, }: DatePickerRangeProps) { const lang = useLang() diff --git a/apps/scandic-web/components/DatePicker/Single/Desktop.tsx b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx index a249ed8c2..3f1f7e74f 100644 --- a/apps/scandic-web/components/DatePicker/Single/Desktop.tsx +++ b/apps/scandic-web/components/DatePicker/Single/Desktop.tsx @@ -15,6 +15,8 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./desktop.module.css" import classNames from "react-day-picker/style.module.css" @@ -23,7 +25,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker" export default function DatePickerSingleDesktop({ close, handleOnSelect, - locales, selectedDate, startMonth, }: DatePickerSingleProps) { diff --git a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx index aad227952..f224f7a7c 100644 --- a/apps/scandic-web/components/DatePicker/Single/Mobile.tsx +++ b/apps/scandic-web/components/DatePicker/Single/Mobile.tsx @@ -10,6 +10,8 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" +import { locales } from "../locales" + import styles from "./mobile.module.css" import classNames from "react-day-picker/style.module.css" @@ -18,7 +20,6 @@ import type { DatePickerSingleProps } from "@/types/components/datepicker" export default function DatePickerSingleMobile({ close, handleOnSelect, - locales, selectedDate, hideHeader, }: DatePickerSingleProps) { diff --git a/apps/scandic-web/components/DatePicker/index.tsx b/apps/scandic-web/components/DatePicker/index.tsx index 0bd89e004..95fe4c72b 100644 --- a/apps/scandic-web/components/DatePicker/index.tsx +++ b/apps/scandic-web/components/DatePicker/index.tsx @@ -1,11 +1,8 @@ "use client" - -import { da, de, fi, nb, sv } from "date-fns/locale" import { useCallback, useEffect, useRef, useState } from "react" import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" @@ -20,14 +17,6 @@ import type { DateRange } from "react-day-picker" import type { DatePickerFormProps } from "@/types/components/datepicker" -const locales = { - [Lang.da]: da, - [Lang.de]: de, - [Lang.fi]: fi, - [Lang.no]: nb, - [Lang.sv]: sv, -} - export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const lang = useLang() const intl = useIntl() @@ -163,7 +152,6 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { ( (total, room) => { if (!room) { diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx index 9897f8cd8..9ee4bdc3d 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Confirm/index.tsx @@ -10,13 +10,13 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { PaymentMethodEnum } from "@/constants/booking" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import Modal from "@/components/Modal" import Divider from "@/components/TempDesignSystem/Divider" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import { trackPaymentSectionOpen } from "@/utils/tracking/booking" -import MySavedCards from "../Payment/MySavedCards" -import PaymentOption from "../Payment/PaymentOption" import PaymentOptionsGroup from "../Payment/PaymentOptionsGroup" import TermsAndConditions from "../Payment/TermsAndConditions" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 924ab2e85..769745f2e 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -22,6 +22,8 @@ import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" import { useEnterDetailsStore } from "@/stores/enter-details" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" @@ -40,9 +42,7 @@ import { writeGlaToSessionStorage } from "./PaymentCallback/helpers" import GuaranteeDetails from "./GuaranteeDetails" import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers" import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown" -import MySavedCards from "./MySavedCards" import PaymentAlert from "./PaymentAlert" -import PaymentOption from "./PaymentOption" import PaymentOptionsGroup from "./PaymentOptionsGroup" import { type PaymentFormData, paymentSchema } from "./schema" import TermsAndConditions from "./TermsAndConditions" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx b/apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx similarity index 93% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx rename to apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx index a90330637..8d2bca089 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MySavedCards/index.tsx @@ -5,8 +5,8 @@ import { type PaymentMethodEnum, } from "@/constants/booking" +import PaymentOptionsGroup from "../EnterDetails/Payment/PaymentOptionsGroup" import PaymentOption from "../PaymentOption" -import PaymentOptionsGroup from "../PaymentOptionsGroup" import styles from "./mySavedCards.module.css" diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/mySavedCards.module.css b/apps/scandic-web/components/HotelReservation/MySavedCards/mySavedCards.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/MySavedCards/mySavedCards.module.css rename to apps/scandic-web/components/HotelReservation/MySavedCards/mySavedCards.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx index 7eebb833d..e52f18e24 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx @@ -12,9 +12,9 @@ import { import { dt } from "@/lib/dt" import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow" -import MySavedCards from "@/components/HotelReservation/EnterDetails/Payment/MySavedCards" -import PaymentOption from "@/components/HotelReservation/EnterDetails/Payment/PaymentOption" import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" import Alert from "@/components/TempDesignSystem/Alert" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Link from "@/components/TempDesignSystem/Link" @@ -144,8 +144,8 @@ export default function ConfirmationStep({ label={ savedCreditCards?.length ? intl.formatMessage({ - defaultMessage: "OTHER", - }) + defaultMessage: "OTHER", + }) : undefined } > diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx index b3a134832..78b1a0c28 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/DeliveryDetailsStep/index.tsx @@ -1,6 +1,6 @@ import { useIntl } from "react-intl" -import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/Ancillaries/utils" +import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries" import Input from "@/components/TempDesignSystem/Form/Input" import Select from "@/components/TempDesignSystem/Form/Select" import Body from "@/components/TempDesignSystem/Text/Body" diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index c4314d3c9..c8236285b 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -19,6 +19,13 @@ import { useAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" +import { + buildAncillaryPackages, + clearAncillarySessionData, + generateDeliveryOptions, + getAncillarySessionData, + setAncillarySessionData, +} from "@/components/HotelReservation/MyStay/utils/ancillaries" import Image from "@/components/Image" import LoadingSpinner from "@/components/LoadingSpinner" import Modal from "@/components/Modal" @@ -33,13 +40,6 @@ import { trackGlaAncillaryAttempt, } from "@/utils/tracking/myStay" -import { - buildAncillaryPackages, - clearAncillarySessionData, - generateDeliveryOptions, - getAncillarySessionData, - setAncillarySessionData, -} from "../../utils" import { type AncillaryFormData, ancillaryFormSchema } from "../schema" import ActionButtons from "./ActionButtons" import PriceDetails from "./PriceDetails" @@ -124,10 +124,7 @@ export default function AddAncillaryFlowModal({ const addAncillary = trpc.booking.packages.useMutation() const { guaranteeBooking, isLoading, handleGuaranteeError } = - useGuaranteeBooking({ - confirmationNumber: booking.confirmationNumber, - isAncillaryFlow: true, - }) + useGuaranteeBooking(booking.confirmationNumber, true) function validateTermsAndConditions(data: AncillaryFormData): boolean { if (!data.termsAndConditions) { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx index e82f7e339..4c2c1677c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/GuaranteeCallback/index.tsx @@ -5,18 +5,17 @@ import { useEffect } from "react" import { trpc } from "@/lib/trpc/client" +import { + buildAncillaryPackages, + clearAncillarySessionData, + getAncillarySessionData, +} from "@/components/HotelReservation/MyStay/utils/ancillaries" import LoadingSpinner from "@/components/LoadingSpinner" import { trackAncillaryFailed, trackAncillarySuccess, } from "@/utils/tracking/myStay" -import { - buildAncillaryPackages, - clearAncillarySessionData, - getAncillarySessionData, -} from "../utils" - import type { Lang } from "@/constants/languages" export default function GuaranteeAncillaryHandler({ diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx deleted file mode 100644 index bd37a9f77..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx +++ /dev/null @@ -1,239 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { PaymentMethodEnum } from "@/constants/booking" -import { - bookingTermsAndConditions, - privacyPolicy, -} from "@/constants/currentWebHrefs" -import { guaranteeCallback } from "@/constants/routes/hotelReservation" -import { env } from "@/env/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import LoadingSpinner from "@/components/LoadingSpinner" -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Divider from "@/components/TempDesignSystem/Divider" -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" -import { toast } from "@/components/TempDesignSystem/Toasts" -import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" -import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" -import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay" - -import MySavedCards from "../../EnterDetails/Payment/MySavedCards" -import PaymentOption from "../../EnterDetails/Payment/PaymentOption" -import PaymentOptionsGroup from "../../EnterDetails/Payment/PaymentOptionsGroup" -import { type GuaranteeFormData, paymentSchema } from "./schema" - -import styles from "./guaranteeLateArrival.module.css" - -import type { CreditCard } from "@/types/user" - -export interface GuaranteeLateArrivalProps { - savedCreditCards: CreditCard[] | null - refId: string -} - -export default function GuaranteeLateArrival({ - savedCreditCards, - refId, -}: GuaranteeLateArrivalProps) { - const intl = useIntl() - const lang = useLang() - const router = useRouter() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const { - actions: { handleCloseView, handleCloseModal }, - } = useManageStayStore() - - const methods = useForm({ - defaultValues: { - paymentMethod: savedCreditCards?.length - ? savedCreditCards[0].id - : PaymentMethodEnum.card, - termsAndConditions: false, - }, - mode: "all", - reValidateMode: "onChange", - resolver: zodResolver(paymentSchema), - }) - const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` - - const { guaranteeBooking, isLoading, handleGuaranteeError } = - useGuaranteeBooking({ - confirmationNumber: bookedRoom.confirmationNumber, - handleBookingCompleted: router.refresh, - }) - - if (isLoading) { - return ( -
- -
- ) - } - - const handleGuaranteeLateArrival = (data: GuaranteeFormData) => { - const savedCreditCard = savedCreditCards?.find( - (card) => card.id === data.paymentMethod - ) - trackGlaSaveCardAttempt(bookedRoom.hotelId, savedCreditCard, "yes") - if (bookedRoom.confirmationNumber) { - const card = savedCreditCard - ? { - alias: savedCreditCard.alias, - expiryDate: savedCreditCard.expirationDate, - cardType: savedCreditCard.cardType, - } - : undefined - guaranteeBooking.mutate({ - confirmationNumber: bookedRoom.confirmationNumber, - language: lang, - ...(card !== undefined && { card }), - success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, - error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`, - cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, - }) - } else { - handleGuaranteeError("No confirmation number") - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong!", - }) - ) - } - } - - return ( - - - - {intl.formatMessage({ - defaultMessage: - "Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.", - })} - - - {intl.formatMessage({ - defaultMessage: - "In case of no-show you will be charged for the first night.", - })} - - {savedCreditCards?.length ? ( - - ) : null} - - - -
- -

- {intl.formatMessage( - { - defaultMessage: - "By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic's Privacy Policy. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.", - }, - { - termsAndConditionsLink: (str) => ( - - {str} - - ), - privacyPolicyLink: (str) => ( - - {str} - - ), - } - )} -

-
- - - - {intl.formatMessage({ - defaultMessage: "I accept the terms and conditions", - })} - - - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Guarantee cost", - })} - - - {intl.formatMessage({ - defaultMessage: - "Your card will only be used for authorisation", - })} - -
- - - {formatPrice(intl, 0, bookedRoom.currencyCode)} - -
- - } - primaryAction={{ - label: intl.formatMessage({ - defaultMessage: "Guarantee", - }), - onClick: methods.handleSubmit(handleGuaranteeLateArrival), - intent: "primary", - }} - secondaryAction={{ - label: intl.formatMessage({ - defaultMessage: "Back", - }), - onClick: handleCloseView, - intent: "text", - }} - /> -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx new file mode 100644 index 000000000..ef6eebe81 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/Details.tsx @@ -0,0 +1,315 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" +import { useState } from "react" +import { Dialog } from "react-aria-components" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trpc } from "@/lib/trpc/client" + +import MembershipLevelIcon from "@/components/Levels/Icon" +import Modal from "@/components/Modal" +import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import ModifyContact from "../ModifyContact" + +import styles from "./guestDetails.module.css" + +import { + type ModifyContactSchema, + modifyContactSchema, +} from "@/types/components/hotelReservation/myStay/modifyContact" +import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" + +interface DetailsProps { + booking: Room + user: SafeUser +} + +export default function Details({ booking, user }: DetailsProps) { + const intl = useIntl() + const lang = useLang() + const router = useRouter() + const utils = trpc.useUtils() + const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL) + const [isLoading, setIsLoading] = useState(false) + + const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] = + useState(false) + + const form = useForm({ + resolver: zodResolver(modifyContactSchema), + defaultValues: { + firstName: booking.guest.firstName, + lastName: booking.guest.lastName, + email: booking.guest.email, + phoneNumber: booking.guest.phoneNumber, + countryCode: booking.guest.countryCode, + }, + }) + + const isFirstStep = currentStep === MODAL_STEPS.INITIAL + + const isMemberBooking = + booking.guest.membershipNumber === user?.membership?.membershipNumber + + const updateGuest = trpc.booking.update.useMutation({ + onMutate: () => setIsLoading(true), + onSuccess: (data) => { + if (data) { + utils.booking.get.invalidate({ + confirmationNumber: data.confirmationNumber, + }) + + toast.success( + intl.formatMessage({ + defaultMessage: "Guest details updated", + }) + ) + setIsModifyGuestDetailsOpen(false) + setCurrentStep(MODAL_STEPS.INITIAL) + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update guest details", + }) + ) + } + }, + onError: () => { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update guest details", + }) + ) + }, + onSettled: () => { + setIsLoading(false) + }, + }) + + async function onSubmit(data: ModifyContactSchema) { + updateGuest.mutate({ + confirmationNumber: booking.confirmationNumber, + guest: { + email: data.email, + phoneNumber: data.phoneNumber, + countryCode: data.countryCode, + }, + }) + } + + function handleModifyMemberDetails() { + const expirationTime = Date.now() + 10 * 60 * 1000 + sessionStorage.setItem( + "myStayReturnRoute", + JSON.stringify({ + path: window.location.href, + expiry: expirationTime, + }) + ) + router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) + } + + return ( +
+ {isMemberBooking && user.membership && ( +
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Your member tier", + })} +

+
+
+
+ +
+
+
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Total points", + })} +

+
+
+ + +

{user.membership.currentPoints}

+
+
+
+ )} +
+ +

+ {booking.guest.firstName} {booking.guest.lastName} +

+
+ {isMemberBooking && user.membership && ( + +

+ {intl.formatMessage( + { + defaultMessage: "Member no. {nr}", + }, + { + nr: user.membership.membershipNumber, + } + )} +

+
+ )} +
+ +

{booking.guest.email}

+
+ +

{booking.guest.phoneNumber}

+
+
+
+ +

{booking.guest.email}

+
+ +

{booking.guest.phoneNumber}

+
+
+
+ {isMemberBooking ? ( + + ) : ( + <> + + {isModifyGuestDetailsOpen && ( + + + {({ close }) => ( + + setIsModifyGuestDetailsOpen(false)} + content={ + booking.guest && ( + + ) + } + primaryAction={{ + label: isFirstStep + ? intl.formatMessage({ + defaultMessage: "Save updates", + }) + : intl.formatMessage({ + defaultMessage: "Confirm", + }), + onClick: isFirstStep + ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) + : () => form.handleSubmit(onSubmit)(), + disabled: !form.formState.isValid || isLoading, + intent: isFirstStep ? "secondary" : "primary", + }} + secondaryAction={{ + label: isFirstStep + ? intl.formatMessage({ + defaultMessage: "Back", + }) + : intl.formatMessage({ + defaultMessage: "Cancel", + }), + onClick: () => { + close() + setCurrentStep(MODAL_STEPS.INITIAL) + }, + }} + /> + + )} + + + )} + + )} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx index 10d2fee9b..7f786c59f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx @@ -1,326 +1,22 @@ "use client" -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { useState } from "react" -import { Dialog } from "react-aria-components" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" +import { useMyStayStore } from "@/stores/my-stay" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" +import Details from "./Details" -import { trpc } from "@/lib/trpc/client" -import { type Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -import MembershipLevelIcon from "@/components/Levels/Icon" -import Modal from "@/components/Modal" -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Button from "@/components/TempDesignSystem/Button" -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" - -import ModifyContact from "../ModifyContact" - -import styles from "./guestDetails.module.css" - -import { - type ModifyContactSchema, - modifyContactSchema, -} from "@/types/components/hotelReservation/myStay/modifyContact" -import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" -import type { User } from "@/types/user" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" interface GuestDetailsProps { - user: User | null - booking: Room - updateRoom: (room: Room) => void + selectedRoom?: Room + user: SafeUser } export default function GuestDetails({ + selectedRoom, user, - booking, - updateRoom, }: GuestDetailsProps) { - const intl = useIntl() - const lang = useLang() - const router = useRouter() - const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL) - const [isLoading, setIsLoading] = useState(false) + const booking = useMyStayStore((state) => state.bookedRoom) + const room = selectedRoom ? selectedRoom : booking - const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] = - useState(false) - - const form = useForm({ - resolver: zodResolver(modifyContactSchema), - defaultValues: { - firstName: booking.guest.firstName, - lastName: booking.guest.lastName, - email: booking.guest.email, - phoneNumber: booking.guest.phoneNumber, - countryCode: booking.guest.countryCode, - }, - }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - - const isMemberBooking = - booking.guest.membershipNumber === user?.membership?.membershipNumber - - const updateGuest = trpc.booking.update.useMutation({ - onMutate: () => setIsLoading(true), - onSuccess: (data) => { - if (!data) { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update guest details", - }) - ) - - return - } - updateRoom({ - ...booking, - guest: { - ...booking.guest, - email: data.guest.email, - phoneNumber: data.guest.phoneNumber, - countryCode: data.guest.countryCode, - }, - }) - - toast.success( - intl.formatMessage({ - defaultMessage: "Guest details updated", - }) - ) - setIsModifyGuestDetailsOpen(false) - setCurrentStep(MODAL_STEPS.INITIAL) - }, - onError: () => { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update guest details", - }) - ) - }, - onSettled: () => { - setIsLoading(false) - }, - }) - - async function onSubmit(data: ModifyContactSchema) { - updateGuest.mutate({ - confirmationNumber: booking.confirmationNumber, - guest: { - email: data.email, - phoneNumber: data.phoneNumber, - countryCode: data.countryCode, - }, - }) - } - - function handleModifyMemberDetails() { - const expirationTime = Date.now() + 10 * 60 * 1000 - sessionStorage.setItem( - "myStayReturnRoute", - JSON.stringify({ - path: window.location.href, - expiry: expirationTime, - }) - ) - router.push(`/${lang}/scandic-friends/my-pages/profile/edit`) - } - - return ( -
- {isMemberBooking && user.membership && ( -
-
- -

- {intl.formatMessage({ - defaultMessage: "Your member tier", - })} -

-
-
-
- -
-
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Total points", - })} -

-
-
- - -

{user.membership.currentPoints}

-
-
-
- )} -
- -

- {booking.guest.firstName} {booking.guest.lastName} -

-
- {isMemberBooking && user.membership && ( - -

- {intl.formatMessage( - { - defaultMessage: "Member no. {nr}", - }, - { - nr: user.membership.membershipNumber, - } - )} -

-
- )} -
- -

{booking.guest.email}

-
- -

{booking.guest.phoneNumber}

-
-
-
- -

{booking.guest.email}

-
- -

{booking.guest.phoneNumber}

-
-
-
- {isMemberBooking ? ( - - ) : ( - <> - - {isModifyGuestDetailsOpen && ( - - - {({ close }) => ( - - setIsModifyGuestDetailsOpen(false)} - content={ - booking.guest && ( - - ) - } - primaryAction={{ - label: isFirstStep - ? intl.formatMessage({ - defaultMessage: "Save updates", - }) - : intl.formatMessage({ - defaultMessage: "Confirm", - }), - onClick: isFirstStep - ? () => setCurrentStep(MODAL_STEPS.CONFIRMATION) - : () => form.handleSubmit(onSubmit)(), - disabled: !form.formState.isValid || isLoading, - intent: isFirstStep ? "secondary" : "primary", - }} - secondaryAction={{ - label: isFirstStep - ? intl.formatMessage({ - defaultMessage: "Back", - }) - : intl.formatMessage({ - defaultMessage: "Cancel", - }), - onClick: () => { - close() - setCurrentStep(MODAL_STEPS.INITIAL) - }, - }} - /> - - )} - - - )} - - )} -
- ) + return
} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx index cfbf7f8b5..6b940b9c6 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Header/index.tsx @@ -4,9 +4,12 @@ import { getIntl } from "@/i18n" import styles from "./header.module.css" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Hotel } from "@/types/hotel" -export async function Header({ hotel }: Pick) { +export async function Header({ + cityName, + name, +}: Pick) { const intl = await getIntl() return (
@@ -20,8 +23,8 @@ export async function Header({ hotel }: Pick) { " " } - {hotel.name} - {hotel.cityName} + {name} + {cityName}
) diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx deleted file mode 100644 index 7574b7591..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/AddToCalendarButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import Button from "@/components/TempDesignSystem/Button" -import { trackMyStayPageLink } from "@/utils/tracking" - -import styles from "../actionPanel.module.css" - -export default function AddToCalendarButton({ - onPress, - disabled, -}: { - onPress: () => void - disabled?: boolean -}) { - const intl = useIntl() - - const handleAddToCalendar = () => { - trackMyStayPageLink("add to calendar") - onPress() - } - - return ( - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx deleted file mode 100644 index 9dcc58892..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/CancelStayPriceContainer/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import PriceContainer from "../../../PriceContainer" -import { useCheckedRoomsCounts } from "../utils" - -import type { - CancelStayFormValues, - PriceContainerProps, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -export default function CancelStayPriceContainer({ - roomDetails, - stayDetails, -}: PriceContainerProps) { - const intl = useIntl() - - const { getValues } = useFormContext() - const formRooms = getValues("rooms") - - const checkedRoomsDetails = useCheckedRoomsCounts( - roomDetails, - formRooms, - intl - ) - - return ( - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx deleted file mode 100644 index 6e4804b84..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/Confirmation/index.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client" - -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" - -import CancelStayPriceContainer from "../CancelStayPriceContainer" - -import styles from "../cancelStay.module.css" - -import type { - CancelStayConfirmationProps, - CancelStayFormValues, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -export function CancelStayConfirmation({ - hotel, - stayDetails, -}: CancelStayConfirmationProps) { - const intl = useIntl() - const { watch } = useFormContext() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const { multiRoom } = bookedRoom - - return ( - <> -
- - {intl.formatMessage( - { - defaultMessage: - "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.", - }, - { - hotel: hotel.name, - checkInDate: stayDetails.checkInDate, - checkOutDate: stayDetails.checkOutDate, - } - )} - - - {intl.formatMessage({ - defaultMessage: "No charges were made.", - })} - -
- {multiRoom && ( - <> - - {intl.formatMessage({ - defaultMessage: "Select rooms", - })} - - -
- {watch("rooms").map((room, index) => { - // Find room details from store by confirmationNumber - const roomDetail = - linkedReservationRooms.find( - (detail) => - detail.confirmationNumber === room.confirmationNumber - ) ?? bookedRoom - - return ( -
- -
- - {intl.formatMessage( - { - defaultMessage: "Room {roomIndex}", - }, - { - roomIndex: index + 1, - } - )} - - {roomDetail && ( - <> - - {roomDetail.roomName} - - - )} -
-
-
- ) - })} -
- - )} - {watch("rooms").some((room) => room.checked) && ( - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx deleted file mode 100644 index 5995c88f5..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/FinalConfirmation/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useIntl } from "react-intl" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Body from "@/components/TempDesignSystem/Text/Body" - -import CancelStayPriceContainer from "../CancelStayPriceContainer" - -import styles from "../cancelStay.module.css" - -import type { FinalConfirmationProps } from "@/types/components/hotelReservation/myStay/cancelStay" - -export function FinalConfirmation({ stayDetails }: FinalConfirmationProps) { - const intl = useIntl() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - return ( - <> -
- - {intl.formatMessage({ - defaultMessage: - "Are you sure you want to continue with the cancellation?", - })} - -
- {bookedRoom && ( - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts deleted file mode 100644 index 110ab453d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/hooks/useCancelStay.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { useIntl } from "react-intl" - -import { trpc } from "@/lib/trpc/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { - type Room, - useMyStayRoomDetailsStore, -} from "@/stores/my-stay/myStayRoomDetailsStore" - -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" -import { trackCancelStay } from "@/utils/tracking" - -import type { - CancelStayFormValues, - CancelStayProps, -} from "@/types/components/hotelReservation/myStay/cancelStay" - -interface UseCancelStayProps extends Omit { - checkedRooms: CancelStayFormValues["rooms"] -} - -export default function useCancelStay({ - handleCloseModal, - checkedRooms, -}: UseCancelStayProps) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setIsLoading }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - - const updateLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateLinkedReservationRoom - ) - - const cancelStay = trpc.booking.cancel.useMutation({ - onMutate: () => setIsLoading(true), - }) - - async function handleCancelStay() { - if (!bookedRoom.confirmationNumber) { - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - return - } - - setIsLoading(true) - - try { - const results = [] - const errors = [] - - for (const room of checkedRooms) { - let targetRoom: Room | undefined - - // Check if this is the main booked room - if (room.confirmationNumber === bookedRoom.confirmationNumber) { - targetRoom = bookedRoom - } - // Check if this is a linked reservation room - else { - targetRoom = linkedReservationRooms.find( - (r) => r.confirmationNumber === room.confirmationNumber - ) - } - - if (!targetRoom?.confirmationNumber) { - errors.push(room.confirmationNumber) - continue - } - - try { - const response = await cancelStay.mutateAsync({ - confirmationNumber: targetRoom.confirmationNumber, - language: lang, - }) - - if (response) { - results.push(room.confirmationNumber) - const cancelledRoom = response.rooms.find( - (r) => r.confirmationNumber === targetRoom?.confirmationNumber - ) - - if (cancelledRoom) { - if ( - targetRoom.confirmationNumber === bookedRoom.confirmationNumber - ) { - // Update main booked room - updateBookedRoom({ - ...bookedRoom, - isCancelled: true, - cancellationNumber: cancelledRoom.cancellationNumber, - }) - } else { - // Update linked reservation room - updateLinkedReservationRoom({ - ...targetRoom, - isCancelled: true, - cancellationNumber: cancelledRoom.cancellationNumber, - }) - } - - trackCancelStay( - bookedRoom.hotelId, - cancelledRoom.confirmationNumber - ) - } - } else { - errors.push(room.confirmationNumber) - } - } catch (error) { - console.error( - `Error cancelling room ${targetRoom.confirmationNumber}:`, - error - ) - errors.push(room.confirmationNumber) - } - } - - // Show appropriate toast based on results - if (results.length > 0 && errors.length === 0) { - // All selected rooms cancelled successfully - toast.success( - intl.formatMessage( - { - defaultMessage: - "Your stay was cancelled. Cancellation cost: 0 {currency}. We're sorry to see that the plans didn't work out", - }, - { currency: bookedRoom.currencyCode } - ) - ) - } else if (results.length > 0 && errors.length > 0) { - // Some rooms cancelled, some failed - toast.warning( - intl.formatMessage({ - defaultMessage: - "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.", - }) - ) - } else { - // No rooms cancelled successfully - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - } - - handleCloseModal() - } catch (error) { - console.error("Error in handleCancelStay:", error) - toast.error( - intl.formatMessage({ - defaultMessage: "Something went wrong. Please try again later.", - }) - ) - } finally { - setIsLoading(false) - } - } - - return { - handleCancelStay, - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx deleted file mode 100644 index 5cc5808ad..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Alert from "@/components/TempDesignSystem/Alert" -import useLang from "@/hooks/useLang" - -import useCancelStay from "./hooks/useCancelStay" -import { CancelStayConfirmation } from "./Confirmation" -import { FinalConfirmation } from "./FinalConfirmation" -import { formatStayDetails, getDefaultRooms } from "./utils" - -import { - type CancelStayFormValues, - cancelStaySchema, -} from "@/types/components/hotelReservation/myStay/cancelStay" -import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" -import { AlertTypeEnum } from "@/types/enums/alert" -import type { Hotel } from "@/types/hotel" - -interface CancelStayProps { - hotel: Hotel -} - -export default function CancelStay({ hotel }: CancelStayProps) { - const intl = useIntl() - const lang = useLang() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const form = useForm({ - resolver: zodResolver(cancelStaySchema), - defaultValues: { - rooms: getDefaultRooms(bookedRoom), - }, - }) - const { - currentStep, - isLoading, - actions: { handleForward, handleCloseView, handleCloseModal }, - } = useManageStayStore() - - const { rooms } = form.watch() - - const { handleCancelStay } = useCancelStay({ - handleCloseModal, - checkedRooms: rooms.filter((room) => room.checked), - }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) - - function getModalCopy() { - if (isFirstStep) { - return { - title: intl.formatMessage({ - defaultMessage: "Cancel stay", - }), - primaryLabel: intl.formatMessage({ - defaultMessage: "Cancel stay", - }), - secondaryLabel: intl.formatMessage({ - defaultMessage: "Back", - }), - } - } else { - return { - title: intl.formatMessage({ - defaultMessage: "Confirm cancellation", - }), - primaryLabel: intl.formatMessage({ - defaultMessage: "Confirm cancellation", - }), - secondaryLabel: intl.formatMessage({ - defaultMessage: "Don't cancel", - }), - } - } - } - - function getModalContent() { - if (bookedRoom && isFirstStep) - return - - if (bookedRoom && !isFirstStep) - return - - if (!bookedRoom && isFirstStep) - return ( - - ) - } - - const isFormValid = rooms?.some((room) => room.checked) - - return ( - - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts deleted file mode 100644 index ce05d8ff7..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/utils.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { dt } from "@/lib/dt" - -import type { IntlShape } from "react-intl" - -import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -export function getDefaultRooms(room: Room) { - const { multiRoom, confirmationNumber, linkedReservations = [] } = room - - if (!multiRoom) { - return [{ id: "1", checked: true, confirmationNumber }] - } - - const mainRoom = { id: "1", checked: false, confirmationNumber } - const linkedRooms = linkedReservations.map((reservation, index) => ({ - id: `${index + 2}`, - checked: false, - confirmationNumber: reservation.confirmationNumber, - })) - - return [mainRoom, ...linkedRooms] -} - -export function formatStayDetails({ - bookedRoom, - lang, - intl, -}: { - bookedRoom: Room - lang: string - intl: IntlShape -}) { - const { - multiRoom, - adults, - childrenAges, - linkedReservations, - checkInDate, - checkOutDate, - } = bookedRoom - - const totalAdults = multiRoom - ? linkedReservations.reduce((acc, reservation) => { - return acc + reservation.adults - }, adults) - : adults - const totalChildren = multiRoom - ? linkedReservations.reduce((acc, reservation) => { - return acc + reservation.children - }, childrenAges.length) - : childrenAges.length - - const checkInDateFormatted = dt(checkInDate) - .locale(lang) - .format("dddd D MMM YYYY") - const checkOutDateFormatted = dt(checkOutDate) - .locale(lang) - .format("dddd D MMM YYYY") - const diff = dt(checkOutDate) - .startOf("day") - .diff(dt(checkInDate).startOf("day"), "days") - - const nightsText = intl.formatMessage( - { - defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", - }, - { totalNights: diff } - ) - const adultsText = intl.formatMessage( - { - defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", - }, - { totalAdults: totalAdults } - ) - const childrenText = intl.formatMessage( - { - defaultMessage: - "{totalChildren, plural, one {# child} other {# children}}", - }, - { totalChildren: totalChildren } - ) - - return { - checkInDate: checkInDateFormatted, - checkOutDate: checkOutDateFormatted, - nightsText, - adultsText, - childrenText, - totalChildren, - } -} - -function getMatchedRooms( - roomDetails: Room, - checkedConfirmationNumbers: string[] -) { - let matchedRooms = [] - - // Main booking - if (checkedConfirmationNumbers.includes(roomDetails.confirmationNumber)) { - matchedRooms.push({ - adults: roomDetails.adults, - children: roomDetails.childrenAges.length, - }) - } - - // Linked reservations - if (roomDetails.linkedReservations) { - roomDetails.linkedReservations.forEach((reservation) => { - if (checkedConfirmationNumbers.includes(reservation.confirmationNumber)) - matchedRooms.push({ - adults: reservation.adults, - children: reservation.children, - }) - }) - } - - return matchedRooms -} - -function calculateTotals(matchedRooms: { adults: number; children: number }[]) { - const totalAdults = matchedRooms.reduce((sum, room) => sum + room.adults, 0) - const totalChildren = matchedRooms.reduce( - (sum, room) => sum + room.children, - 0 - ) - return { totalAdults, totalChildren } -} - -export const useCheckedRoomsCounts = ( - roomDetails: Room, - formRooms: CancelStayFormValues["rooms"], - intl: IntlShape -) => { - const checkedFormRooms = formRooms.filter((room) => room.checked) - const checkedConfirmationNumbers = checkedFormRooms - .map((room) => room.confirmationNumber) - .filter( - (confirmationNumber): confirmationNumber is string => - confirmationNumber !== null && confirmationNumber !== undefined - ) - - const matchedRooms = getMatchedRooms(roomDetails, checkedConfirmationNumbers) - const { totalAdults, totalChildren } = calculateTotals(matchedRooms) - - const adultsText = intl.formatMessage( - { - defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", - }, - { totalAdults: totalAdults } - ) - const childrenText = intl.formatMessage( - { - defaultMessage: - "{totalChildren, plural, one {# child} other {# children}}", - }, - { totalChildren: totalChildren } - ) - - return { adultsText, childrenText, totalChildren } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css deleted file mode 100644 index e17771a89..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/confirmation.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.container { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.dateComparison { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.dateGroup { - display: flex; - flex-direction: column; -} - -.dateHeader { - display: flex; - flex-direction: row; - justify-content: space-between; -} - -.dates { - display: flex; - flex-direction: column; -} - -.date { - display: flex; - justify-content: space-between; -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx deleted file mode 100644 index 64c73b90f..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/Confirmation/index.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useFormContext } from "react-hook-form" -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import PriceContainer from "@/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer" -import Divider from "@/components/TempDesignSystem/Divider" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import useLang from "@/hooks/useLang" - -import styles from "./confirmation.module.css" - -interface ConfirmationProps { - oldPrice: number - newPrice: number - stayDetails: { - checkInDate: string - checkOutDate: string - nightsText: string - adultsText: string - childrenText: string - totalChildren?: number - } -} - -export default function Confirmation({ - oldPrice, - newPrice, - stayDetails, -}: ConfirmationProps) { - const intl = useIntl() - const lang = useLang() - const { getValues } = useFormContext() - const { currencyCode } = useMyStayTotalPriceStore() - - const formValues = getValues() - - const originalCheckIn = dt(stayDetails.checkInDate) - .locale(lang) - .format("dddd, DD MMM, YYYY") - const originalCheckOut = dt(stayDetails.checkOutDate) - .locale(lang) - .format("dddd, DD MMM, YYYY") - const newCheckIn = dt(formValues.checkInDate) - .locale(lang) - .format("dddd, DD MMM, YYYY") - const newCheckOut = dt(formValues.checkOutDate) - .locale(lang) - .format("dddd, DD MMM, YYYY") - - const diff = dt(newCheckOut) - .startOf("day") - .diff(dt(newCheckIn).startOf("day"), "days") - - const nightsText = intl.formatMessage( - { - defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", - }, - { totalNights: diff } - ) - - return ( -
-
-
-
- - {intl.formatMessage({ - defaultMessage: "Old dates", - })} - - - {oldPrice} {currencyCode} - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Check-in", - })} - - {originalCheckIn} -
-
- - {intl.formatMessage({ - defaultMessage: "Check-out", - })} - - {originalCheckOut} -
-
-
- - - -
-
- - {intl.formatMessage({ - defaultMessage: "New dates", - })} - - - {newPrice} {currencyCode} - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Check-in", - })} - - {newCheckIn} -
-
- - {intl.formatMessage({ - defaultMessage: "Check-out", - })} - - {newCheckOut} -
-
-
-
- - -
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts deleted file mode 100644 index aa9563f77..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/hooks/useModifyStay.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { useIntl } from "react-intl" - -import { trpc } from "@/lib/trpc/client" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import { toast } from "@/components/TempDesignSystem/Toasts" -import useLang from "@/hooks/useLang" - -import type { UseFormGetValues } from "react-hook-form" - -import type { ModifyDateSchema } from "@/types/components/hotelReservation/myStay/modifyDate" - -interface UseModifyStayOptions { - isLoggedIn?: boolean - getFormValues: UseFormGetValues - handleCloseModal: () => void -} - -export default function useModifyStay({ - isLoggedIn, - getFormValues, - handleCloseModal, -}: UseModifyStayOptions) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setIsLoading }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - - const utils = trpc.useUtils() - - const updateBooking = trpc.booking.update.useMutation({ - onMutate: () => setIsLoading(true), - onSuccess: (updatedBooking) => { - if (!updatedBooking) { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay", - }) - ) - return - } - // Update room details with server response data - updateBookedRoom({ - ...bookedRoom, - checkInDate: updatedBooking.checkInDate, - checkOutDate: updatedBooking.checkOutDate, - }) - - toast.success( - intl.formatMessage({ - defaultMessage: "Your stay was updated", - }) - ) - handleCloseModal() - }, - onError: () => { - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay", - }) - ) - }, - onSettled: () => { - setIsLoading(false) - }, - }) - - async function checkAvailability() { - const formValues = getFormValues() - - if (!formValues.checkInDate || !formValues.checkOutDate) { - toast.error( - intl.formatMessage({ - defaultMessage: "Please select dates", - }) - ) - return { success: false } - } - - setIsLoading(true) - - try { - const availabilityResults = [] - let totalNewPrice = 0 - - try { - const data = await utils.hotel.availability.myStay.fetch({ - booking: { - fromDate: formValues.checkInDate, - toDate: formValues.checkOutDate, - hotelId: bookedRoom.hotelId, - room: { - adults: bookedRoom.adults, - bookingCode: bookedRoom.bookingCode ?? undefined, - childrenInRoom: bookedRoom.childrenInRoom, - rateCode: bookedRoom.rateDefinition.rateCode, - roomTypeCode: bookedRoom.roomTypeCode, - }, - }, - lang, - }) - - if (!data?.selectedRoom || data.selectedRoom.roomsLeft <= 0) { - return { success: false, noAvailability: true } - } - let roomPrice = 0 - if (isLoggedIn && "member" in data.product && data.product.member) { - roomPrice = data.product.member.localPrice.pricePerStay - } else if ("public" in data.product && data.product.public) { - roomPrice = data.product.public.localPrice.pricePerStay - } else if ( - "corporateCheque" in data.product && - data.product.corporateCheque.localPrice.additionalPricePerStay - ) { - roomPrice = - data.product.corporateCheque.localPrice.additionalPricePerStay - } else if ( - "redemption" in data.product && - data.product.redemption.localPrice.additionalPricePerStay - ) { - roomPrice = data.product.redemption.localPrice.additionalPricePerStay - } - totalNewPrice += roomPrice - availabilityResults.push(data) - } catch (error) { - console.error("Error checking room availability:", error) - return { success: false, error: true } - } - - return { - success: true, - newRoomPrice: totalNewPrice, - results: availabilityResults, - } - } catch (error) { - console.error("Error checking availability:", error) - return { success: false, error: true } - } finally { - setIsLoading(false) - } - } - - async function handleModifyStay() { - const formValues = getFormValues() - setIsLoading(true) - - try { - await updateBooking.mutateAsync({ - confirmationNumber: bookedRoom.confirmationNumber, - checkInDate: formValues.checkInDate, - checkOutDate: formValues.checkOutDate, - }) - } catch (error) { - console.error("Error modifying stay:", error) - toast.error( - intl.formatMessage({ - defaultMessage: "Failed to update your stay. Please try again later.", - }) - ) - setIsLoading(false) - } - } - - return { - checkAvailability, - handleModifyStay, - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx deleted file mode 100644 index 2ebe106a0..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -"use client" -import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions" -import Alert from "@/components/TempDesignSystem/Alert" -import useLang from "@/hooks/useLang" - -import { formatStayDetails } from "../CancelStay/utils" -import useModifyStay from "./hooks/useModifyStay" -import Confirmation from "./Confirmation" -import NewDates from "./NewDates" - -import { - type ModifyDateSchema, - modifyDateSchema, - type ModifyStayProps, -} from "@/types/components/hotelReservation/myStay/modifyDate" -import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay" -import { AlertTypeEnum } from "@/types/enums/alert" - -export default function ModifyStay({ isLoggedIn }: ModifyStayProps) { - const intl = useIntl() - const lang = useLang() - - const [error, setError] = useState(false) - const [noAvailability, setNoAvailability] = useState(false) - const [newRoomPrice, setNewRoomPrice] = useState(0) - - const form = useForm({ - resolver: zodResolver(modifyDateSchema), - defaultValues: { - checkInDate: "", - checkOutDate: "", - }, - }) - - const { - currentStep, - isLoading, - actions: { handleCloseView, handleCloseModal, setCurrentStep }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const stayDetails = formatStayDetails({ bookedRoom, lang, intl }) - - const isFirstStep = currentStep === MODAL_STEPS.INITIAL - - const { - multiRoom, - checkInDate, - checkOutDate, - mainRoom, - roomPrice, - canChangeDate, - } = bookedRoom - - const { checkAvailability, handleModifyStay } = useModifyStay({ - isLoggedIn, - getFormValues: form.getValues, - handleCloseModal, - }) - - async function onCheckAvailability() { - setError(false) - setNoAvailability(false) - - const result = await checkAvailability() - - if (result.success) { - setNewRoomPrice(result.newRoomPrice ?? 0) - setCurrentStep(MODAL_STEPS.CONFIRMATION) - } else { - if (result.noAvailability) { - setNoAvailability(true) - } - if (result.error) { - setError(true) - } - } - } - - useEffect(() => { - form.setValue("checkInDate", dt(checkInDate).format("YYYY-MM-DD")) - form.setValue("checkOutDate", dt(checkOutDate).format("YYYY-MM-DD")) - }, [checkInDate, checkOutDate, form]) - - function getModalContent() { - if (bookedRoom && isFirstStep && multiRoom) { - return ( - - ) - } - if (mainRoom && !canChangeDate) { - return ( - - ) - } - if (mainRoom && isFirstStep) - return ( - - ) - - if (mainRoom && !isFirstStep) - return ( - - ) - - if (!mainRoom && isFirstStep) - return ( - - ) - } - - return ( - - - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css deleted file mode 100644 index 77553883e..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/actionPanel.module.css +++ /dev/null @@ -1,79 +0,0 @@ -.actionPanel { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - padding: var(--Spacing-x3); - width: 100%; -} - -.menu { - width: 100%; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.actionPanel .menu .button, -.actionLink { - width: 100%; - color: var(--Scandic-Brand-Burgundy); - justify-content: space-between !important; - padding: var(--Spacing-x1) 0 !important; -} - -.actionLink { - font-weight: 500; - display: flex; -} - -.actionPanel .menu .button:disabled { - color: var(--Scandic-Grey-40); -} - -.disabledLink { - color: var(--Scandic-Grey-40); - display: flex; - justify-content: space-between; - padding: var(--Spacing-x1) 0; - width: 100%; -} -.disabledLink:hover { - cursor: not-allowed; -} - -.info { - width: 100%; - background-color: var(--Base-Background-Primary-Normal); - padding: var(--Spacing-x3); - text-align: right; - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - align-items: flex-end; -} - -.tag { - text-transform: uppercase; - font-size: 12px; - font-weight: 600; - color: var(--Main-Red-60); - font-family: var(--typography-Caption-Labels-fontFamily); -} - -.link { - margin-top: auto; -} - -@media (min-width: 1367px) { - .actionPanel { - flex-direction: row; - } - - .menu { - width: 432px; - } - - .info { - width: 256px; - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx deleted file mode 100644 index f3d906e2a..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/index.tsx +++ /dev/null @@ -1,255 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { CancellationRuleEnum } from "@/constants/booking" -import { customerService } from "@/constants/currentWebHrefs" -import { preliminaryReceipt } from "@/constants/routes/myStay" -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import AddToCalendar from "@/components/HotelReservation/AddToCalendar" -import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers" -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import useLang from "@/hooks/useLang" -import { trackMyStayPageLink } from "@/utils/tracking" - -import AddToCalendarButton from "./Actions/AddToCalendarButton" -import { - checkCancelable, - checkCanDownloadInvoice, - checkDateModifiable, - checkGuaranteeable, - isDatetimePast, -} from "./utils" - -import styles from "./actionPanel.module.css" - -import type { EventAttributes } from "ics" - -import type { Hotel } from "@/types/hotel" - -interface ActionPanelProps { - hotel: Hotel -} - -export default function ActionPanel({ hotel }: ActionPanelProps) { - const intl = useIntl() - const lang = useLang() - const { - actions: { setActiveView }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const { - confirmationNumber, - checkInDate, - checkOutDate, - createDateTime, - canChangeDate, - priceType, - } = bookedRoom - - const datetimeIsInThePast = isDatetimePast(checkInDate) - - const isDateModifyable = checkDateModifiable( - canChangeDate, - datetimeIsInThePast, - bookedRoom.isCancelled, - priceType === "points" - ) - - const isCancelable = checkCancelable( - bookedRoom.isCancelable, - datetimeIsInThePast, - linkedReservationRooms - ) - - const isGuaranteeable = checkGuaranteeable( - !!bookedRoom.guaranteeInfo, - bookedRoom.isCancelled, - datetimeIsInThePast - ) - - const canDownloadInvoice = checkCanDownloadInvoice( - bookedRoom.isCancelled, - bookedRoom.rateDefinition.cancellationRule === - CancellationRuleEnum.CancellableBefore6PM - ) - - const calendarEvent: EventAttributes = { - busyStatus: "FREE", - categories: ["booking", "hotel", "stay"], - created: generateDateTime(createDateTime), - description: hotel.hotelContent.texts.descriptions?.medium, - end: generateDateTime(checkOutDate), - endInputType: "utc", - geo: { - lat: hotel.location.latitude, - lon: hotel.location.longitude, - }, - location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`, - start: generateDateTime(checkInDate), - startInputType: "utc", - status: "CONFIRMED", - title: hotel.name, - url: hotel.contactInformation.websiteUrl, - } - - const handleModifyStay = () => { - trackMyStayPageLink("modify dates") - setActiveView("modifyStay") - } - - const handleCancelStay = () => { - trackMyStayPageLink("cancel booking") - setActiveView("cancelStay") - } - - const handleDownloadInvoice = () => { - trackMyStayPageLink("download invoice") - } - - const handleGuaranteeLateArrival = () => { - trackMyStayPageLink("guarantee late arrival") - setActiveView("guaranteeLateArrival") - } - - const handleCustomerSupport = () => { - trackMyStayPageLink("customer support") - } - - return ( -
-
- - - - - ( - - )} - /> - {canDownloadInvoice ? ( - - {intl.formatMessage({ - defaultMessage: "Download invoice", - })} - - - ) : ( -
- -

- {intl.formatMessage({ - defaultMessage: "Download invoice", - })} -

-
- - -
- )} - - -
-
-
- - {intl.formatMessage({ - defaultMessage: "Reference number", - })} - - - {confirmationNumber} - -
-
- - {hotel.name} - - - {hotel.address.streetAddress} - - - {hotel.address.city} - - - - {hotel.contactInformation.phoneNumber} - - -
- - - {intl.formatMessage({ - defaultMessage: "Customer support", - })} - - - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts deleted file mode 100644 index f4d1389be..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { dt } from "@/lib/dt" - -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" - -export function isDatetimePast(date: Date) { - return dt(date).hour(18).minute(0).second(0).isBefore(dt(), "seconds") -} - -export function checkDateModifiable( - canChangeDate: boolean, - datetimeIsInThePast: boolean, - isCancelled: boolean, - isRewardNight: boolean -) { - return canChangeDate && !datetimeIsInThePast && !isCancelled && !isRewardNight -} - -export function checkCancelable( - isCancelable: boolean, - datetimeIsInThePast: boolean, - linkedReservationRooms: Room[] -) { - const hasAnyCancelableRoom = - isCancelable || linkedReservationRooms.some((room) => room.isCancelable) - - return hasAnyCancelableRoom && !datetimeIsInThePast -} - -export function checkGuaranteeable( - guaranteeInfo: boolean, - isCancelled: boolean, - datetimeIsInThePast: boolean -) { - return !guaranteeInfo && !isCancelled && !datetimeIsInThePast -} - -export function checkCanDownloadInvoice( - isCancelled: boolean, - isFlexBooking: boolean -) { - return !isCancelled && !isFlexBooking -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx deleted file mode 100644 index 32b2dbe2d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" - -import { useManageStayStore } from "@/stores/my-stay/manageStayStore" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Modal from "@/components/Modal" -import Button from "@/components/TempDesignSystem/Button" - -import GuaranteeLateArrival from "../GuaranteeLateArrival" -import CancelStay from "./ActionPanel/Actions/CancelStay" -import ModifyStay from "./ActionPanel/Actions/ModifyStay" -import ActionPanel from "./ActionPanel" - -import styles from "./manangeStay.module.css" - -import type { Hotel } from "@/types/hotel" -import { type CreditCard } from "@/types/user" - -interface ManageStayProps { - hotel: Hotel - savedCreditCards: CreditCard[] | null - refId: string - isLoggedIn: boolean -} - -export default function ManageStay({ - hotel, - savedCreditCards, - refId, - isLoggedIn, -}: ManageStayProps) { - const intl = useIntl() - const { - isOpen, - activeView, - actions: { setIsOpen, handleCloseModal }, - } = useManageStayStore() - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const allRoomsCancelled = - linkedReservationRooms.every((room) => room.isCancelled) && - bookedRoom.isCancelled - - function renderContent() { - switch (activeView) { - case "cancelStay": - return - case "modifyStay": - return - case "guaranteeLateArrival": - return ( - - ) - default: - return - } - } - - return ( - <> - - {isOpen && ( - - {renderContent()} - - )} - - ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css deleted file mode 100644 index 1bfddecda..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/manangeStay.module.css +++ /dev/null @@ -1,3 +0,0 @@ -button.manageStayButton { - color: var(--Text-Inverted); -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx deleted file mode 100644 index 6ff89815c..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/PriceType.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import Cheques from "../Cheques" -import Points from "../Points" -import Price from "../Price" - -import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" - -interface PriceTypeProps - extends Pick< - BookingConfirmation["booking"], - "cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers" - > { - isCancelled: boolean - priceType: PriceTypeEnum -} - -export default function PriceType({ - cheques, - isCancelled, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, -}: PriceTypeProps) { - const intl = useIntl() - - switch (priceType) { - case PriceTypeEnum.cheque: - return - case PriceTypeEnum.money: - return ( - - ) - case PriceTypeEnum.points: - return - case PriceTypeEnum.voucher: - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - default: - return null - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx deleted file mode 100644 index ae8a39cdb..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Points/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import SkeletonShimmer from "@/components/SkeletonShimmer" - -import type { Variant } from "../Rooms/TotalPrice" - -export default function Points({ - points, - variant, -}: { - points: number | null - variant: Variant -}) { - const intl = useIntl() - - if (points === null) { - return - } - - return ( - -

- {intl.formatNumber(points)} - { - /* eslint-disable-next-line formatjs/no-literal-string-in-jsx */ - " " - } - {intl.formatMessage({ - defaultMessage: "Points", - })} -

-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx deleted file mode 100644 index e7c99079d..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Price/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import SkeletonShimmer from "@/components/SkeletonShimmer" -import { formatPrice } from "@/utils/numberFormatting" - -import styles from "./price.module.css" - -import type { Variant } from "../Rooms/TotalPrice" - -export default function Price({ - price, - variant, - isMember, -}: { - price: number | null - variant: Variant - isMember?: boolean -}) { - const intl = useIntl() - const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) - - if (price === null) { - return - } - - return ( - -

- {formatPrice(intl, price, currencyCode)} -

-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx index a69b9f65b..fbe08622f 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/index.tsx @@ -1,6 +1,6 @@ "use client" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" +import { useMyStayStore } from "@/stores/my-stay" import PriceDetailsModal from "@/components/HotelReservation/PriceDetailsModal" @@ -9,19 +9,17 @@ import { calculateTotalPrice, mapToPrice } from "./mapToPrice" import styles from "./priceDetails.module.css" export default function PriceDetails() { - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const rooms = [bookedRoom, ...linkedReservationRooms] - .filter((room) => !room.isCancelled) - .map((room) => ({ - ...room, - breakfastIncluded: room.rateDefinition.breakfastIncluded, - price: mapToPrice(room), - roomType: room.roomName, - })) + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms + .filter((room) => !room.isCancelled) + .map((room) => ({ + ...room, + breakfastIncluded: room.rateDefinition.breakfastIncluded, + price: mapToPrice(room), + roomType: room.roomName, + })), + })) const bookingCode = rooms.find((room) => room.bookingCode)?.bookingCode ?? undefined diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts index 114710931..814f1aa73 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts @@ -3,7 +3,7 @@ import { dt } from "@/lib/dt" import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" import type { Price } from "@/types/components/hotelReservation/price" import { CurrencyEnum } from "@/types/enums/currency" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" +import type { Room } from "@/types/stores/my-stay" export function mapToPrice(room: Room) { switch (room.priceType) { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx similarity index 63% rename from apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx index acab2e125..1ce2acf11 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Cheques/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Cheques.tsx @@ -3,7 +3,7 @@ import { useIntl } from "react-intl" import { Typography } from "@scandic-hotels/design-system/Typography" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" +import { useMyStayStore } from "@/stores/my-stay" import SkeletonShimmer from "@/components/SkeletonShimmer" import { formatPrice } from "@/utils/numberFormatting" @@ -12,16 +12,18 @@ import { CurrencyEnum } from "@/types/enums/currency" export default function Cheques({ cheques, + isCancelled, price, }: { cheques: number + isCancelled: boolean price: number }) { const intl = useIntl() - const currencyCode = useMyStayTotalPriceStore((state) => state.currencyCode) + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) if (!cheques) { - return + return } const totalPrice = formatPrice( @@ -29,12 +31,12 @@ export default function Cheques({ cheques, CurrencyEnum.CC, price, - currencyCode + currency ) return ( - -

{totalPrice}

+ +

{isCancelled ? {totalPrice} : totalPrice}

) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx new file mode 100644 index 000000000..da37f1f92 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Points.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import SkeletonShimmer from "@/components/SkeletonShimmer" +import { formatPrice } from "@/utils/numberFormatting" + +import { CurrencyEnum } from "@/types/enums/currency" + +export default function Points({ + isCancelled, + points, + price, +}: { + isCancelled: boolean + points: number + price: number +}) { + const intl = useIntl() + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) + + if (!points) { + return + } + + const totalPrice = formatPrice( + intl, + points, + CurrencyEnum.POINTS, + price, + currency + ) + + return ( + +

{isCancelled ? {totalPrice} : totalPrice}

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx new file mode 100644 index 000000000..e3cbb3b3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/index.tsx @@ -0,0 +1,21 @@ +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./price.module.css" + +export default function Price({ + isCancelled, + isMember, + price, +}: { + isCancelled: boolean + isMember?: boolean + price: string +}) { + return ( + +

+ {isCancelled ? {price} : price} +

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/price.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/Price/price.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/Price/price.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx new file mode 100644 index 000000000..1c60a1a0c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/Vouchers.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import SkeletonShimmer from "@/components/SkeletonShimmer" +import { formatPrice } from "@/utils/numberFormatting" + +import { CurrencyEnum } from "@/types/enums/currency" + +export default function Vouchers({ + isCancelled, + price, + vouchers, +}: { + isCancelled: boolean + price?: number + vouchers: number +}) { + const intl = useIntl() + const currency = useMyStayStore((state) => state.bookedRoom.currencyCode) + + if (!vouchers) { + return + } + + const totalPrice = formatPrice( + intl, + vouchers, + CurrencyEnum.Voucher, + price, + currency + ) + + return ( + +

{isCancelled ? {totalPrice} : totalPrice}

+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx similarity index 59% rename from apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx index baf37c582..04a7ed7bb 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceType.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceType/index.tsx @@ -1,11 +1,9 @@ "use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" import Cheques from "./Cheques" import Points from "./Points" import Price from "./Price" +import Vouchers from "./Vouchers" import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" @@ -15,12 +13,14 @@ interface PriceTypeProps BookingConfirmation["booking"], "cheques" | "rateDefinition" | "roomPoints" | "totalPrice" | "vouchers" > { + formattedTotalPrice: string isCancelled: boolean priceType: PriceTypeEnum } export default function PriceType({ cheques, + formattedTotalPrice, isCancelled, priceType, rateDefinition, @@ -28,33 +28,38 @@ export default function PriceType({ totalPrice, vouchers, }: PriceTypeProps) { - const intl = useIntl() - switch (priceType) { case PriceTypeEnum.cheque: - return + return ( + + ) case PriceTypeEnum.money: return ( ) case PriceTypeEnum.points: - return + return ( + + ) case PriceTypeEnum.voucher: return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
+ ) default: return null diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx new file mode 100644 index 000000000..9ccaecb9f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/CustomerSupport.tsx @@ -0,0 +1,19 @@ +"use client" +import { DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal" +import Button from "@/components/TempDesignSystem/Button" + +export default function CustomerSupport() { + const intl = useIntl() + + return ( + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css new file mode 100644 index 000000000..630fa03ce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/cancelled.module.css @@ -0,0 +1,18 @@ +div a.link { + align-items: center; + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Inverted); + cursor: pointer; + display: flex; + gap: var(--Space-x1); + height: 48px; + justify-content: center; + padding: var(--Space-x2) var(--Space-x4); + transition: background-color 200ms ease; + + &:hover { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx new file mode 100644 index 000000000..5bb109db6 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Cancelled/index.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Link from "@/components/TempDesignSystem/Link" + +import CustomerSupport from "./CustomerSupport" + +import styles from "./cancelled.module.css" + +export default function Cancelled() { + const intl = useIntl() + return ( + <> + {/* (S) TODO - Link to where?? */} + + {intl.formatMessage({ defaultMessage: "Rebook" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css new file mode 100644 index 000000000..c0b9a6d5f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/customerSupport.module.css @@ -0,0 +1,30 @@ +.links { + display: grid; + gap: var(--Space-x05); +} + +.link { + align-items: center; + background: var(--Surface-Feedback-Information); + border: 1px solid rgba(0, 0, 0, 0.05); + border-radius: var(--Corner-radius-Medium); + color: var(--Text-Interactive-Default); + display: flex; + flex-direction: column; + gap: var(--Space-x1); + padding: var(--Space-x3); + /* text-decoration: none; */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; +} + +@media screen and (min-width: 768px) { + .links { + gap: var(--Space-x3); + grid-template-columns: 1fr 1fr; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx new file mode 100644 index 000000000..ba4e86abd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal/index.tsx @@ -0,0 +1,78 @@ +"use client" +import Link from "next/link" +import { Dialog } from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import styles from "./customerSupport.module.css" + +export default function CustomerSupportModal() { + const intl = useIntl() + const { email, phone } = useMyStayStore((state) => ({ + email: state.hotel.contactInformation.email, + phone: state.hotel.contactInformation.phoneNumber, + })) + + const title = intl.formatMessage({ defaultMessage: "Customer service" }) + const contact = intl.formatMessage( + { + defaultMessage: + "Please call {phone} or email us at {email} for assistance with your order.", + }, + { email, phone } + ) + + return ( + + + {({ close }) => ( + + + +

{contact}

+
+
+ +
+ + + + + {intl.formatMessage({ + defaultMessage: "Make a call", + })} + + + + + + + + {intl.formatMessage({ + defaultMessage: "Send an email", + })} + + + +
+
+ + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Close" })} + + +
+ )} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx new file mode 100644 index 000000000..92ff68496 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/AddToCalendarButton.tsx @@ -0,0 +1,43 @@ +"use client" + +import { Button as ButtonRAC } from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trackMyStayPageLink } from "@/utils/tracking" + +import styles from "./button.module.css" + +export default function AddToCalendarButton({ + disabled, + onPress, +}: { + disabled?: boolean + onPress: () => void +}) { + const intl = useIntl() + + function handleAddToCalendar() { + trackMyStayPageLink("add to calendar") + onPress() + } + + return ( + + + + + {intl.formatMessage({ + defaultMessage: "Add to calendar", + })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css new file mode 100644 index 000000000..e6b2fafcf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/button.module.css @@ -0,0 +1,18 @@ +.button { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; + width: 100%; + + &:disabled { + color: var(--Scandic-Grey-40); + } +} + +.text { + color: var(--Text-Interactive-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx new file mode 100644 index 000000000..1ea0e7e07 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/AddToCalendar/index.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useMyStayStore } from "@/stores/my-stay" + +import AddToCalendar from "@/components/HotelReservation/AddToCalendar" +import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers" + +import { dateHasPassed } from "../utils" +import AddToCalendarButton from "./AddToCalendarButton" + +import type { EventAttributes } from "ics" + +export default function AddToCalendarAction() { + const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore( + (state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + createDateTime: state.bookedRoom.createDateTime, + hotel: state.hotel, + }) + ) + + const calendarEvent: EventAttributes = { + busyStatus: "FREE", + categories: ["booking", "hotel", "stay"], + created: generateDateTime(createDateTime), + description: hotel.hotelContent.texts.descriptions?.medium, + end: generateDateTime(checkOutDate), + endInputType: "utc", + geo: { + lat: hotel.location.latitude, + lon: hotel.location.longitude, + }, + location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`, + start: generateDateTime(checkInDate), + startInputType: "utc", + status: "CONFIRMED", + title: hotel.name, + url: hotel.contactInformation.websiteUrl, + } + + const disabled = dateHasPassed( + checkInDate, + hotel.hotelFacts.checkin.checkInTime + ) + + return ( + ( + + )} + /> + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx new file mode 100644 index 000000000..8afe7f741 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Alerts.tsx @@ -0,0 +1,46 @@ +"use client" +import { useIntl } from "react-intl" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +interface AlertsProps extends React.PropsWithChildren { + closeModal: () => void +} + +export default function Alerts({ children, closeModal }: AlertsProps) { + const intl = useIntl() + const mainRoom = useMyStayStore((state) => state.bookedRoom) + + if (!mainRoom) { + const title = intl.formatMessage({ defaultMessage: "Cancel stay" }) + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) + } + + return <>{children} +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx new file mode 100644 index 000000000..5b9affdb8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/CancelStayPriceContainer.tsx @@ -0,0 +1,78 @@ +"use client" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer" +import { formatPrice } from "@/utils/numberFormatting" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +export default function CancelStayPriceContainer() { + const intl = useIntl() + + const { bookedRoom, nights, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + nights: dt(state.bookedRoom.checkOutDate) + .startOf("day") + .diff(dt(state.bookedRoom.checkInDate).startOf("day"), "days"), + rooms: state.rooms, + })) + const formRooms = useWatch({ name: "rooms" }) + + if (!Array.isArray(formRooms)) { + return null + } + + const { totalAdults, totalChildren } = formRooms.reduce( + (total, formRoom) => { + if (formRoom.checked) { + const room = rooms.find( + (r) => r.confirmationNumber === formRoom.confirmationNumber + ) + if (room) { + total.totalAdults = total.totalAdults + room.adults + if (room.childrenInRoom.length) { + total.totalChildren = + total.totalChildren + room.childrenInRoom.length + } + } + } + return total + }, + { totalAdults: 0, totalChildren: 0 } + ) + + const adultsText = intl.formatMessage( + { + defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", + }, + { totalAdults: totalAdults } + ) + const childrenText = intl.formatMessage( + { + defaultMessage: + "{totalChildren, plural, one {# child} other {# children}}", + }, + { totalChildren: totalChildren } + ) + const nightsText = intl.formatMessage( + { + defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", + }, + { totalNights: nights } + ) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx new file mode 100644 index 000000000..4c3dbab3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/index.tsx @@ -0,0 +1,111 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" + +import styles from "./multiroom.module.css" + +import type { Room } from "@/types/stores/my-stay" + +export default function Multiroom() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + const notCancelableRooms = rooms.filter((r) => !r.isCancelable) + const cancelableRooms = rooms.filter((r) => !r.isCancelled && r.isCancelable) + const isSingleRoom = rooms.length === 1 + + if (isSingleRoom) { + return null + } + + const myRooms = intl.formatMessage({ defaultMessage: "My rooms" }) + const selectRoom = intl.formatMessage({ + defaultMessage: "Select room", + }) + const cannotBeCancelled = intl.formatMessage({ + defaultMessage: "Cannot be cancelled", + }) + + if (notCancelableRooms.length) { + return ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "This stay has multiple terms.", + })} +

+
+
+ + +
+
+ ) + } + + return +} + +interface ListProps { + disabled?: boolean + rooms: Room[] + title: string +} + +function List({ disabled = false, rooms, title }: ListProps) { + const intl = useIntl() + const refMsg = intl.formatMessage({ defaultMessage: "Ref" }) + return ( +
+ +

{title}

+
+ +
    + {rooms.map((room) => { + const roomNumber = room.roomNumber + return ( +
  • + +
    +
    + +

    + {intl.formatMessage( + { + defaultMessage: "Room {roomIndex}", + }, + { + roomIndex: roomNumber, + } + )} +

    +
    +
    + +

    {room.roomName}

    +
    + + {/* eslint-disable formatjs/no-literal-string-in-jsx */} +

    + {refMsg}: {room.confirmationNumber} +

    +
    +
    +
    +
  • + ) + })} +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css new file mode 100644 index 000000000..8ed5b8e2c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/Multiroom/multiroom.module.css @@ -0,0 +1,74 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Space-x3); +} + +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x5); +} + +.rooms { + display: flex; + flex-direction: column; + gap: var(--Space-x1); +} + +.list { + display: flex; + flex-direction: column; + gap: var(--Space-x1); + list-style: none; + margin: 0; + padding: var(--Space-x05) 0 0; +} + +.checkbox { + background: var(--Background-Primary); + border: 2px solid transparent; + border-radius: var(--Corner-radius-md); + padding: var(--Space-x2) var(--Space-x15); +} + +.checkbox:has(input:checked) { + border-color: var(--Border-Interactive-Selected); +} + +.checkbox:has(input:checked) span[class*="checkbox_checkbox_"] { + background-color: var(--Surface-UI-Fill-Active); +} + +.checkbox:has(input:disabled) { + background-color: var(--Surface-UI-Fill-Disabled); + border: 1px solid var(--Border-Interactive-Disabled); + cursor: not-allowed; +} + +.checkbox:has(input:disabled) .chip { + background-color: var(--Surface-UI-Fill-Disabled); + border: 1px solid var(--Text-Interactive-Disabled); +} + +.checkbox:has(input:disabled) p { + color: var(--Text-Interactive-Disabled); +} + +.room { + align-items: center; + display: grid; + gap: var(--Space-x1); + grid-template-columns: auto 1fr auto; + width: 100%; +} + +.chip { + background-color: var(--Surface-Brand-Accent-Default); + border-radius: var(--Corner-radius-sm); + padding: var(--Space-x1); +} + +.chipText { + color: var(--Text-Heading); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css new file mode 100644 index 000000000..8650139bf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/confirmation.module.css @@ -0,0 +1,9 @@ +.form { + display: flex; + flex-direction: column; + gap: var(--Space-x5); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx new file mode 100644 index 000000000..2b1894a18 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/Confirmation/index.tsx @@ -0,0 +1,127 @@ +"use client" +import { useFormContext, useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import useLang from "@/hooks/useLang" + +import CancelStayPriceContainer from "../CancelStayPriceContainer" +import Multiroom from "./Multiroom" + +import styles from "./confirmation.module.css" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +interface CancelStayConfirmationProps { + closeModal: () => void + onSubmit: (data: CancelStayFormValues) => void +} + +export default function CancelStayConfirmation({ + closeModal, + onSubmit, +}: CancelStayConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const { handleSubmit } = useFormContext() + const formRooms = useWatch({ name: "rooms" }) + + const { fromDate, hotel, isCancelable, rate, toDate } = useMyStayStore( + (state) => ({ + fromDate: state.bookedRoom.checkInDate, + hotel: state.hotel, + isCancelable: state.bookedRoom.isCancelable, + rate: state.bookedRoom.rate, + toDate: state.bookedRoom.checkOutDate, + }) + ) + + const checkInDate = dt(fromDate).locale(lang).format("dddd D MMM YYYY") + const checkOutDate = dt(toDate).locale(lang).format("dddd D MMM YYYY") + + const title = intl.formatMessage({ defaultMessage: "Cancel booking" }) + const primaryLabel = intl.formatMessage({ + defaultMessage: "Cancel stay", + }) + const secondaryLabel = intl.formatMessage({ + defaultMessage: "Back", + }) + + const notCancelableText = intl.formatMessage( + { + defaultMessage: + "Your stay has been booked with {rate} terms which unfortunately doesn’t allow for cancellation.", + }, + { + rate, + strong: (str) => {str}, + } + ) + + const text = intl.formatMessage( + { + defaultMessage: + "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.", + }, + { + checkInDate, + checkOutDate, + hotel: hotel.name, + strong: (str) => {str}, + } + ) + + const isValid = Array.isArray(formRooms) + ? formRooms.some((r) => r.checked) + : false + + return ( + + + +

+ {isCancelable ? text : notCancelableText} +

+
+
+ +
+ {isCancelable ? ( + <> + + + + ) : null} + +
+ + + {secondaryLabel} + + {isCancelable ? ( + + {primaryLabel} + + ) : ( + + {intl.formatMessage({ defaultMessage: "Close" })} + + )} + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css new file mode 100644 index 000000000..fd87f381b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/finalConfirmation.module.css @@ -0,0 +1,9 @@ +.toastContainer { + display: flex; + flex-direction: column; + gap: var(--Space-x05); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx new file mode 100644 index 000000000..3c5cbca8c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/FinalConfirmation/index.tsx @@ -0,0 +1,170 @@ +"use client" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import CancelStayPriceContainer from "../CancelStayPriceContainer" + +import styles from "./finalConfirmation.module.css" + +import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay" + +interface FinalConfirmationProps { + closeModal: () => void +} + +export default function FinalConfirmation({ + closeModal, +}: FinalConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const formRooms = useWatch({ name: "rooms" }) + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms, + })) + + const cancelledStayMsg = intl.formatMessage({ + defaultMessage: "Your stay was cancelled", + }) + const sorryMsg = intl.formatMessage({ + defaultMessage: "We’re sorry that things didn’t work out.", + }) + + const cancelBookingsMutation = trpc.booking.cancelMany.useMutation({ + onSuccess(data, variables) { + const allCancellationsWentThrough = data.every((cancelled) => cancelled) + if (allCancellationsWentThrough) { + if (data.length === rooms.length) { + toast.success( +
+ + {cancelledStayMsg} + + + {sorryMsg} + +
+ ) + } else { + const cancelledRooms = rooms.filter((r) => + variables.confirmationNumbers.includes(r.confirmationNumber) + ) + for (const cancelledRoom of cancelledRooms) { + toast.success( +
+ + + + {intl.formatMessage( + { defaultMessage: "{roomName} room was cancelled" }, + { roomName: cancelledRoom.roomName } + )} + + + + + + {intl.formatMessage({ + defaultMessage: + "Your Stay is still active with the other room", + })} + + +
+ ) + } + } + } else { + toast.warning( + intl.formatMessage({ + defaultMessage: + "Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.", + }) + ) + } + + utils.booking.get.invalidate({ + confirmationNumber: bookedRoom.confirmationNumber, + }) + utils.booking.linkedReservations.invalidate({ + lang, + rooms: bookedRoom.linkedReservations, + }) + closeModal() + }, + onError() { + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong. Please try again later.", + }) + ) + }, + }) + + function cancelBooking() { + if (Array.isArray(formRooms)) { + const confirmationNumbersToCancel = formRooms + .filter((r) => r.checked) + .map((r) => r.confirmationNumber) + if (confirmationNumbersToCancel.length) { + cancelBookingsMutation.mutate({ + confirmationNumbers: confirmationNumbersToCancel, + language: lang, + }) + } + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong. Please try again later.", + }) + ) + } + } + + const confirm = intl.formatMessage({ + defaultMessage: "Confirm cancellation", + }) + const dontCancel = intl.formatMessage({ + defaultMessage: "Don't cancel", + }) + const text = intl.formatMessage({ + defaultMessage: "Are you sure you want to continue with the cancellation?", + }) + const title = intl.formatMessage({ + defaultMessage: "Cancel booking", + }) + + return ( + + + +

{text}

+
+
+ + + + + + {dontCancel} + + + {confirm} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx new file mode 100644 index 000000000..c0b110c32 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/Steps/index.tsx @@ -0,0 +1,60 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { FormProvider, useForm } from "react-hook-form" + +import { useMyStayStore } from "@/stores/my-stay" + +import CancelStayConfirmation from "./Confirmation" +import FinalConfirmation from "./FinalConfirmation" + +import { + type CancelStayFormValues, + cancelStaySchema, +} from "@/types/components/hotelReservation/myStay/cancelStay" + +interface StepsProps { + closeModal: () => void +} + +export default function Steps({ closeModal }: StepsProps) { + const [confirm, setConfirm] = useState(false) + const rooms = useMyStayStore((state) => state.rooms) + + const methods = useForm({ + mode: "onSubmit", + reValidateMode: "onChange", + resolver: zodResolver(cancelStaySchema), + values: { + rooms: rooms.map((room, idx) => ({ + // Single room booking + checked: rooms.length === 1, + confirmationNumber: room.confirmationNumber, + id: idx + 1, + })), + }, + }) + + function handleSubmit(data: CancelStayFormValues) { + const checkedRooms = data.rooms.filter((r) => r.checked) + if (checkedRooms.length) { + setConfirm(true) + } + } + + const stepOne = !confirm + const stepTwo = confirm + return ( + + {/* Step 1 */} + {stepOne ? ( + + ) : null} + {/* Step 2 */} + {stepTwo ? : null} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/cancelStay.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/CancelStay/cancelStay.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/cancelStay.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx new file mode 100644 index 000000000..339a34efe --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CancelStay/index.tsx @@ -0,0 +1,28 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import Alerts from "./Alerts" +import Steps from "./Steps" + +export default function CancelStay() { + const intl = useIntl() + return ( + + + {intl.formatMessage({ defaultMessage: "Cancel stay" })} + + + + {({ close }) => ( + + + + )} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx new file mode 100644 index 000000000..1482b6014 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/CannotChangeDate.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function CannotChangeDate({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx new file mode 100644 index 000000000..b9aa33069 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/MultiRoomBooking.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function MultiRoomBooking({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx new file mode 100644 index 000000000..153722593 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/NotMainRoom.tsx @@ -0,0 +1,42 @@ +"use client" +import { useIntl } from "react-intl" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function NotMainRoom({ + closeModal, +}: { + closeModal: () => void +}) { + const intl = useIntl() + return ( + + + + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx new file mode 100644 index 000000000..10d552583 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Alerts/index.tsx @@ -0,0 +1,31 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import CannotChangeDate from "./CannotChangeDate" +import MultiRoomBooking from "./MultiRoomBooking" +import NotMainRoom from "./NotMainRoom" + +export default function Alerts({ + children, + closeModal, +}: React.PropsWithChildren<{ closeModal: () => void }>) { + const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({ + canChangeDate: state.bookedRoom.canChangeDate, + mainRoom: state.bookedRoom.mainRoom, + multiRoom: state.bookedRoom.multiRoom, + })) + + if (multiRoom) { + return + } + + if (!mainRoom) { + return + } + + if (!canChangeDate) { + return + } + + return <>{children} +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx new file mode 100644 index 000000000..70f29f450 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/index.tsx @@ -0,0 +1,64 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./priceAndDate.module.css" + +interface PriceAndDateProps { + checkInDate: string + checkOutDate: string + label: string + price: string + striked?: boolean +} + +export default function PriceAndDate({ + checkInDate, + checkOutDate, + label, + price, + striked = false, +}: PriceAndDateProps) { + const intl = useIntl() + + const checkInMsg = intl.formatMessage({ + defaultMessage: "Check-in", + }) + const checkOutMsg = intl.formatMessage({ + defaultMessage: "Check-out", + }) + + return ( +
+
+ +

{label}

+
+ +

{price}

+
+
+
+ +

{checkInMsg}

+
+ +

+ {striked ? {checkInDate} : checkInDate} +

+
+
+
+ +

{checkOutMsg}

+
+ +

+ {striked ? {checkOutDate} : checkOutDate} +

+
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css new file mode 100644 index 000000000..2a15f4968 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/PriceAndDate/priceAndDate.module.css @@ -0,0 +1,18 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Space-x1); +} + +.item { + display: flex; + justify-content: space-between; +} + +.textDefault { + color: var(--Text-Default); +} + +.textSecondary { + color: var(--Text-Secondary); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css new file mode 100644 index 000000000..053bf66a8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/confirmation.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.dateComparison { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx new file mode 100644 index 000000000..8d6a65b2a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Confirmation/index.tsx @@ -0,0 +1,183 @@ +"use client" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer" +import Divider from "@/components/TempDesignSystem/Divider" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import PriceAndDate from "./PriceAndDate" + +import styles from "./confirmation.module.css" + +import type { Lang } from "@/constants/languages" + +interface ConfirmationProps { + checkInDate: string + checkOutDate: string + closeModal: () => void + newPrice: string +} + +function formatDate(date: Date | string, lang: Lang) { + return dt(date).locale(lang).format("dddd, DD MMM, YYYY") +} + +export default function Confirmation({ + checkInDate, + checkOutDate, + closeModal, + newPrice, +}: ConfirmationProps) { + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const { bookedRoom, oldPrice, totalAdults, totalChildren } = useMyStayStore( + (state) => ({ + bookedRoom: state.bookedRoom, + oldPrice: state.totalPrice, + totalAdults: state.rooms.reduce( + (total, room) => total + (room.isCancelled ? 0 : room.adults), + 0 + ), + totalChildren: state.rooms.reduce( + (total, room) => + total + (room.isCancelled ? 0 : room.childrenInRoom.length), + 0 + ), + }) + ) + + const updateBooking = trpc.booking.update.useMutation({ + onSuccess: (updatedBooking) => { + if (updatedBooking) { + utils.booking.get.invalidate({ + confirmationNumber: updatedBooking.confirmationNumber, + }) + + toast.success( + intl.formatMessage({ + defaultMessage: "Your stay was updated", + }) + ) + + closeModal() + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update your stay", + }) + ) + } + }, + onError: () => { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to update your stay", + }) + ) + }, + }) + + function handleModifyStay() { + updateBooking.mutate({ + confirmationNumber: bookedRoom.confirmationNumber, + checkInDate, + checkOutDate, + }) + } + + const originalCheckIn = formatDate(bookedRoom.checkInDate, lang) + const originalCheckOut = formatDate(bookedRoom.checkOutDate, lang) + const newCheckIn = formatDate(checkInDate, lang) + const newCheckOut = formatDate(checkOutDate, lang) + + const nights = dt(newCheckOut) + .startOf("day") + .diff(dt(newCheckIn).startOf("day"), "days") + + const nightsText = intl.formatMessage( + { + defaultMessage: "{totalNights, plural, one {# night} other {# nights}}", + }, + { totalNights: nights } + ) + const newDatesLabel = intl.formatMessage({ + defaultMessage: "New dates", + }) + const oldDatesLabel = intl.formatMessage({ + defaultMessage: "Old dates", + }) + const title = intl.formatMessage({ + defaultMessage: "Confirm date change", + }) + const totalDueMsg = intl.formatMessage({ + defaultMessage: "Total due", + }) + const adultsText = intl.formatMessage( + { + defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}", + }, + { totalAdults: totalAdults } + ) + const childrenText = intl.formatMessage( + { + defaultMessage: + "{totalChildren, plural, one {# child} other {# children}}", + }, + { totalChildren: totalChildren } + ) + + return ( + + + +
+
+ + + + + +
+ + +
+
+ + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Confirm" })} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx new file mode 100644 index 000000000..0a37d635d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/Error.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function Error() { + const intl = useIntl() + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx new file mode 100644 index 000000000..96b8098ec --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/Alerts/NoAvailability.tsx @@ -0,0 +1,21 @@ +"use client" +import { useIntl } from "react-intl" + +import Alert from "@/components/TempDesignSystem/Alert" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export default function NoAvailability() { + const intl = useIntl() + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/calendarButton.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/calendarButton.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/calendarButton.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/index.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/CalendarButton/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/CalendarButton/index.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx similarity index 64% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx index f8c0701cc..b0040e5bf 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/index.tsx @@ -1,16 +1,15 @@ -import { da, de, fi, nb, sv } from "date-fns/locale" -import { useEffect, useState } from "react" +"use client" +import { useState } from "react" import { createPortal } from "react-dom" import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop" import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile" import Modal from "@/components/Modal" -import Alert from "@/components/TempDesignSystem/Alert" import Caption from "@/components/TempDesignSystem/Text/Caption" import useLang from "@/hooks/useLang" @@ -20,55 +19,33 @@ import styles from "./newDates.module.css" import type { DateRange } from "react-day-picker" -import { AlertTypeEnum } from "@/types/enums/alert" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" +export default function NewDates() { + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.mainRoom.checkInDate, + checkOutDate: state.mainRoom.checkOutDate, + })) -const locales = { - [Lang.da]: da, - [Lang.de]: de, - [Lang.fi]: fi, - [Lang.no]: nb, - [Lang.sv]: sv, -} - -interface NewDatesProps { - mainRoom: Room - noAvailability: boolean - error: boolean -} - -export default function NewDates({ - mainRoom, - noAvailability, - error, -}: NewDatesProps) { const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false) const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false) const [selectedDates, setSelectedDates] = useState(() => ({ - from: dt(mainRoom.checkInDate).startOf("day").toDate(), - to: dt(mainRoom.checkOutDate).startOf("day").toDate(), + from: dt(checkInDate).startOf("day").toDate(), + to: dt(checkOutDate).startOf("day").toDate(), })) const intl = useIntl() const lang = useLang() const { setValue } = useFormContext() - // Initialize form values on mount - useEffect(() => { - setValue("checkInDate", dt(mainRoom.checkInDate).format("YYYY-MM-DD")) - setValue("checkOutDate", dt(mainRoom.checkOutDate).format("YYYY-MM-DD")) - }, [mainRoom.checkInDate, mainRoom.checkOutDate, setValue]) - // Calculate default number of days between check-in and check-out - const defaultDaysBetween = dt(mainRoom.checkOutDate) + const defaultDaysBetween = dt(checkOutDate) .startOf("day") - .diff(dt(mainRoom.checkInDate).startOf("day"), "days") + .diff(dt(checkInDate).startOf("day"), "days") function showCheckInPicker() { // Update selected dates before showing picker setSelectedDates((prev) => ({ - from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(), - to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(), + from: prev.from ?? dt(checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(checkOutDate).startOf("day").toDate(), })) setShowCheckInDatePicker(true) setShowCheckOutDatePicker(false) @@ -77,8 +54,8 @@ export default function NewDates({ function showCheckOutPicker() { // Update selected dates before showing picker setSelectedDates((prev) => ({ - from: prev.from ?? dt(mainRoom.checkInDate).startOf("day").toDate(), - to: prev.to ?? dt(mainRoom.checkOutDate).startOf("day").toDate(), + from: prev.from ?? dt(checkInDate).startOf("day").toDate(), + to: prev.to ?? dt(checkOutDate).startOf("day").toDate(), })) setShowCheckOutDatePicker(true) setShowCheckInDatePicker(false) @@ -126,30 +103,11 @@ export default function NewDates({ setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD")) } + const fromDate = selectedDates.from ?? dt(checkInDate).toDate() + const toDate = selectedDates.to ?? dt(checkOutDate).toDate() + return ( <> - {noAvailability && ( - - )} - {error && ( - - )}
@@ -190,21 +148,13 @@ export default function NewDates({ setShowCheckInDatePicker(false)} handleOnSelect={handleCheckInDateSelect} - locales={locales} - selectedDate={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } - startMonth={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } + selectedDate={fromDate} + startMonth={fromDate} /> setShowCheckInDatePicker(false)} handleOnSelect={handleCheckInDateSelect} - locales={locales} - selectedDate={ - selectedDates.from ?? dt(mainRoom.checkInDate).toDate() - } + selectedDate={fromDate} hideHeader /> , @@ -220,21 +170,13 @@ export default function NewDates({ setShowCheckOutDatePicker(false)} handleOnSelect={handleCheckOutDateSelect} - locales={locales} - selectedDate={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } - startMonth={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } + selectedDate={toDate} + startMonth={toDate} /> setShowCheckOutDatePicker(false)} handleOnSelect={handleCheckOutDateSelect} - locales={locales} - selectedDate={ - selectedDates.to ?? dt(mainRoom.checkOutDate).toDate() - } + selectedDate={toDate} hideHeader /> , diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/newDates.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/Actions/ModifyStay/NewDates/newDates.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/NewDates/newDates.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx new file mode 100644 index 000000000..c65908fcd --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/Form/index.tsx @@ -0,0 +1,85 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import NoAvailability from "./Alerts/NoAvailability" +import NewDates from "./NewDates" + +import { + type ChangeDatesFormProps, + type ChangeDatesSchema, + changeDatesSchema, +} from "@/types/components/hotelReservation/myStay/changeDates" + +export default function Form({ + checkAvailability, + closeModal, + noAvailability, +}: ChangeDatesFormProps) { + const intl = useIntl() + + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + })) + + const methods = useForm({ + defaultValues: { + checkInDate: dt(checkInDate).format("YYYY-MM-DD"), + checkOutDate: dt(checkOutDate).format("YYYY-MM-DD"), + }, + resolver: zodResolver(changeDatesSchema), + }) + + async function handleSubmit(values: ChangeDatesSchema) { + if (values.checkInDate && values.checkOutDate) { + await checkAvailability(values.checkInDate, values.checkOutDate) + } else { + toast.error( + intl.formatMessage({ + defaultMessage: "Please select dates", + }) + ) + } + } + + return ( + +
+ + + + {noAvailability && } + + + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ + defaultMessage: "Check availability", + })} + + + +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx new file mode 100644 index 000000000..96338dacf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/Steps/index.tsx @@ -0,0 +1,136 @@ +"use client" +import { useSession } from "next-auth/react" +import { useState } from "react" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" +import { useMyStayStore } from "@/stores/my-stay" + +import { sumPackages } from "@/components/HotelReservation/utils" +import useLang from "@/hooks/useLang" +import { isValidClientSession } from "@/utils/clientSession" +import { formatPrice } from "@/utils/numberFormatting" + +import Confirmation from "./Confirmation" +import Form from "./Form" + +import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates" +import { CurrencyEnum } from "@/types/enums/currency" + +interface Dates { + fromDate: string + toDate: string +} + +export default function Steps({ closeModal }: ChangeDatesStepsProps) { + const { data: session } = useSession() + const isLoggedIn = isValidClientSession(session) + const intl = useIntl() + const lang = useLang() + const utils = trpc.useUtils() + const [dates, setDates] = useState(null) + const [newPrice, setNewPrice] = useState(null) + const [noAvailability, setNoAvailability] = useState(false) + + const { breakfast, currencyCode, hotelId, packages, room } = useMyStayStore( + (state) => ({ + breakfast: state.bookedRoom.breakfast, + currencyCode: state.bookedRoom.currencyCode, + hotelId: state.bookedRoom.hotelId, + packages: state.bookedRoom.packages ?? [], + room: { + adults: state.bookedRoom.adults, + bookingCode: state.bookedRoom.bookingCode ?? undefined, + childrenInRoom: state.bookedRoom.childrenInRoom, + rateCode: state.bookedRoom.rateDefinition.rateCode, + roomTypeCode: state.bookedRoom.roomTypeCode, + }, + }) + ) + + async function checkAvailability(fromDate: string, toDate: string) { + setNoAvailability(false) + + const data = await utils.hotel.availability.myStay.fetch({ + booking: { fromDate, hotelId, room, toDate }, + lang, + }) + + if (!data || !data.selectedRoom || !data.selectedRoom.roomsLeft) { + setNoAvailability(true) + return + } + + setDates({ fromDate, toDate }) + + const pkgsSum = sumPackages(packages) + const extraPrice = pkgsSum.price + (breakfast?.localPrice.totalPrice || 0) + if (isLoggedIn && "member" in data.product && data.product.member) { + const { currency, pricePerStay } = data.product.member.localPrice + setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) + } else if ("public" in data.product && data.product.public) { + const { currency, pricePerStay } = data.product.public.localPrice + setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency)) + } else if ( + "corporateCheque" in data.product && + data.product.corporateCheque.localPrice.additionalPricePerStay + ) { + const { additionalPricePerStay, currency, numberOfCheques } = + data.product.corporateCheque.localPrice + setNewPrice( + formatPrice( + intl, + numberOfCheques, + CurrencyEnum.CC, + additionalPricePerStay + extraPrice, + currency?.toString() ?? pkgsSum.currency ?? currencyCode + ) + ) + } else if ( + "redemption" in data.product && + data.product.redemption.localPrice.additionalPricePerStay + ) { + const { additionalPricePerStay, currency, pointsPerStay } = + data.product.redemption.localPrice + setNewPrice( + formatPrice( + intl, + pointsPerStay, + CurrencyEnum.POINTS, + additionalPricePerStay + extraPrice, + currency?.toString() ?? pkgsSum.currency ?? currencyCode + ) + ) + } + } + + function goBackToSelectDates() { + setNewPrice(null) + setDates(null) + setNoAvailability(false) + } + + const hasNewDate = newPrice && dates + + const stepOne = !hasNewDate + const stepTwo = hasNewDate + return ( + <> + {stepOne ? ( +
+ ) : null} + {stepTwo ? ( + + ) : null} + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx new file mode 100644 index 000000000..064f45ecf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ChangeDates/index.tsx @@ -0,0 +1,49 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import { dateHasPassed } from "../utils" +import Alerts from "./Alerts" +import Steps from "./Steps" + +export default function ChangeDates() { + const intl = useIntl() + + const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } = + useMyStayStore((state) => ({ + canChangeDate: state.bookedRoom.canChangeDate, + checkInDate: state.bookedRoom.checkInDate, + checkInTime: state.hotel.hotelFacts.checkin.checkInTime, + isCancelled: state.bookedRoom.isCancelled, + priceType: state.bookedRoom.priceType, + })) + + const isRewardNight = priceType === "points" + const isDisabled = + canChangeDate && + !isCancelled && + !isRewardNight && + dateHasPassed(checkInDate, checkInTime) + + const text = intl.formatMessage({ defaultMessage: "Change dates" }) + return ( + + + {text} + + + + {({ close }) => ( + + + + )} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx new file mode 100644 index 000000000..1a3798eef --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/CustomerSupport/index.tsx @@ -0,0 +1,18 @@ +"use client" +import { DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal" +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +export default function CustomerSupport() { + const intl = useIntl() + return ( + + + {intl.formatMessage({ defaultMessage: "Customer support" })} + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css similarity index 54% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css index ec20a477a..94d862acd 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/guaranteeLateArrival.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/form.module.css @@ -1,52 +1,3 @@ -.card { - display: flex; - align-items: center; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2) var(--Spacing-x-one-and-half); - border-radius: var(--Corner-radius-Medium); - background-color: var(--Base-Surface-Subtle-Normal); -} - -.addCreditCard { - display: flex; - align-items: center; - justify-content: center; - width: 100%; -} - -.guaranteeCost { - display: flex; - justify-content: flex-end; - padding: var(--Spacing-x2); - align-items: flex-end; - gap: var(--Spacing-x3); - border-radius: var(--Corner-radius-Medium); - background-color: var(--Base-Surface-Subtle-Normal); -} - -.guaranteeCostText { - display: flex; - flex-direction: column; -} - -.termsAndConditions { - display: grid; - gap: var(--Spacing-x2); - color: var(--Text-Secondary); -} - -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.paymentOptionContainer { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - .loading { display: flex; align-items: center; @@ -56,3 +7,42 @@ height: 640px; max-height: 100%; } + +.form { + display: grid; + gap: var(--Spacing-x3); +} + +.termsAndConditions { + color: var(--Text-Secondary); + display: grid; + gap: var(--Spacing-x2); +} + +.termsAndConditions .checkbox span { + align-items: flex-start; +} + +.guaranteeCost { + align-items: center; + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Medium); + display: flex; + gap: var(--Spacing-x3); + justify-content: flex-end; + padding: var(--Spacing-x2); +} + +.guaranteeCostText { + align-items: flex-end; + display: flex; + flex-direction: column; +} + +.baseTextHighContrast { + color: var(--Base-Text-High-contrast); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx new file mode 100644 index 000000000..2f7ee30cb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/index.tsx @@ -0,0 +1,194 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { PaymentMethodEnum } from "@/constants/booking" +import { + bookingTermsAndConditions, + privacyPolicy, +} from "@/constants/currentWebHrefs" +import { guaranteeCallback } from "@/constants/routes/hotelReservation" +import { env } from "@/env/client" +import { useMyStayStore } from "@/stores/my-stay" + +import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup" +import MySavedCards from "@/components/HotelReservation/MySavedCards" +import PaymentOption from "@/components/HotelReservation/PaymentOption" +import LoadingSpinner from "@/components/LoadingSpinner" +import Divider from "@/components/TempDesignSystem/Divider" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import { toast } from "@/components/TempDesignSystem/Toasts" +import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking" +import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" +import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay" + +import { type GuaranteeFormData, paymentSchema } from "./schema" + +import styles from "./form.module.css" + +export default function Form() { + const intl = useIntl() + const lang = useLang() + + const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } = + useMyStayStore((state) => ({ + confirmationNumber: state.bookedRoom.confirmationNumber, + currencyCode: state.bookedRoom.currencyCode, + hotelId: state.bookedRoom.hotelId, + refId: state.refId, + savedCreditCards: state.savedCreditCards, + })) + + const methods = useForm({ + defaultValues: { + paymentMethod: savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, + termsAndConditions: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(paymentSchema), + }) + + const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}` + + const { guaranteeBooking, isLoading, handleGuaranteeError } = + useGuaranteeBooking(confirmationNumber) + + if (isLoading) { + return ( +
+ +
+ ) + } + + function handleGuaranteeLateArrival(data: GuaranteeFormData) { + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + trackGlaSaveCardAttempt(hotelId, savedCreditCard, "yes") + if (confirmationNumber) { + const card = savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined + guaranteeBooking.mutate({ + confirmationNumber, + language: lang, + ...(card && { card }), + success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`, + error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`, + cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`, + }) + } else { + handleGuaranteeError("No confirmation number") + toast.error( + intl.formatMessage({ + defaultMessage: "Something went wrong!", + }) + ) + } + } + + const guaranteeMsg = intl.formatMessage( + { + defaultMessage: + "By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general Terms & Conditions, and understand Scandic will process my personal data for this stay in accordance with Scandic's Privacy Policy. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.", + }, + { + termsAndConditionsLink: (str) => ( + + {str} + + ), + privacyPolicyLink: (str) => ( + + {str} + + ), + } + ) + + return ( + + + {savedCreditCards?.length ? ( + + ) : null} + + + +
+ + +

{guaranteeMsg}

+
+
+
+
+
+ + + {intl.formatMessage({ + defaultMessage: "Total due", + })} + + + + + {intl.formatMessage({ + defaultMessage: + "Your card will only be charged in the event of a no-show", + })} + + +
+ + + + {formatPrice(intl, 0, currencyCode)} + + +
+ +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/schema.ts b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/schema.ts similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/schema.ts rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/Form/schema.ts diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx new file mode 100644 index 000000000..9f24f003a --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/GuaranteeLateArrival/index.tsx @@ -0,0 +1,69 @@ +"use client" +import { Dialog, DialogTrigger } from "react-aria-components" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import { dateHasPassed } from "../utils" +import Form from "./Form" + +export default function GuaranteeLateArrival() { + const intl = useIntl() + + const { checkInDate, checkInTime, guaranteeInfo, isCancelled } = + useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkInTime: state.hotel.hotelFacts.checkin.checkInTime, + guaranteeInfo: state.bookedRoom.guaranteeInfo, + isCancelled: state.bookedRoom.isCancelled, + })) + + const guaranteeable = + !guaranteeInfo && !isCancelled && !dateHasPassed(checkInDate, checkInTime) + + if (!guaranteeable) { + return null + } + + const arriveLateMsg = intl.formatMessage({ + defaultMessage: + "Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.", + }) + const text = intl.formatMessage({ + defaultMessage: "Guarantee late arrival", + }) + + return ( + + {text} + + + {({ close }) => ( + + + +

{arriveLateMsg}

+
+
+ +
+ + + + {intl.formatMessage({ defaultMessage: "Back" })} + + + {intl.formatMessage({ defaultMessage: "Guarantee" })} + + + + )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx new file mode 100644 index 000000000..f316bf689 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/index.tsx @@ -0,0 +1,56 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { CancellationRuleEnum } from "@/constants/booking" +import { preliminaryReceipt } from "@/constants/routes/myStay" +import { useMyStayStore } from "@/stores/my-stay" + +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" +import { trackMyStayPageLink } from "@/utils/tracking" + +import styles from "./view.module.css" + +export default function ViewAndPrintReceipt() { + const intl = useIntl() + const lang = useLang() + const canDownloadInvoice = useMyStayStore( + (state) => + !state.bookedRoom.isCancelled && + !( + state.bookedRoom.rateDefinition.cancellationRule === + CancellationRuleEnum.CancellableBefore6PM + ) + ) + + if (!canDownloadInvoice) { + return null + } + + function trackClick() { + trackMyStayPageLink("download invoice") + } + + const printMsg = intl.formatMessage({ + defaultMessage: "View and print receipt", + }) + + return ( +
+ + + + {printMsg} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css new file mode 100644 index 000000000..bee715a3b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/ViewAndPrintReceipt/view.module.css @@ -0,0 +1,7 @@ +.download { + align-items: center; + color: var(--Text-Interactive-Default); + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css new file mode 100644 index 000000000..2cf353a24 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/actions.module.css @@ -0,0 +1,5 @@ +.list { + list-style: none; + margin: 0; + padding: 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx new file mode 100644 index 000000000..0600fd4c7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/index.tsx @@ -0,0 +1,21 @@ +import AddToCalendar from "./AddToCalendar" +import CancelStay from "./CancelStay" +import ChangeDates from "./ChangeDates" +import CustomerSupport from "./CustomerSupport" +import GuaranteeLateArrival from "./GuaranteeLateArrival" +import ViewAndPrintReceipt from "./ViewAndPrintReceipt" + +import styles from "./actions.module.css" + +export default function Actions() { + return ( +
+ + + + + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts new file mode 100644 index 000000000..42cfb4097 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Actions/utils.ts @@ -0,0 +1,7 @@ +import { dt } from "@/lib/dt" + +export function dateHasPassed(date: Date, time: string) { + const hour = dt(time, "HH:mm").hour() + const minute = dt(time, "HH:mm").minute() + return dt(date).hour(hour).minute(minute).isBefore(dt(), "minutes") +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx new file mode 100644 index 000000000..9b5e36402 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/index.tsx @@ -0,0 +1,44 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./info.module.css" + +export default function Info() { + const intl = useIntl() + const text = intl.formatMessage({ defaultMessage: "Booking number" }) + + const { address, confirmationNumber, hotelName, phoneNumber } = + useMyStayStore((state) => ({ + address: state.hotel.address, + confirmationNumber: state.bookedRoom.confirmationNumber, + hotelName: state.hotel.name, + phoneNumber: state.hotel.contactInformation.phoneNumber, + })) + + return ( +
+
+ + {text} + + + + {confirmationNumber} + + +
+ +
+ {hotelName} + {address.streetAddress} + {address.city} + {phoneNumber} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css new file mode 100644 index 000000000..911d67310 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/Info/info.module.css @@ -0,0 +1,29 @@ +.container { + align-items: flex-start; + background-color: var(--Surface-Primary-OnSurface-Default); + border-radius: var(--Corner-radius-md); + display: flex; + flex-direction: column; + gap: var(--Space-x2); + justify-content: center; + padding: var(--Space-x15) var(--Space-x3); +} + +.booking { + display: flex; + flex-direction: column; + gap: var(--Space-x05); +} + +.text { + color: var(--Text-Default); +} + +.confirmationNumber { + color: var(--Text-Heading); +} + +.address { + display: flex; + flex-direction: column; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx new file mode 100644 index 000000000..b4471d3fc --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/index.tsx @@ -0,0 +1,65 @@ +"use client" +import { + Button as ButtonRAC, + Dialog, + DialogTrigger, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal" + +import Actions from "./Actions" +import Info from "./Info" + +import styles from "./manageStay.module.css" + +export default function ManageStay() { + const intl = useIntl() + const allRoomsAreCancelled = useMyStayStore( + (state) => state.allRoomsAreCancelled + ) + + const color = allRoomsAreCancelled + ? "Icon/Interactive/Disabled" + : "Icon/Inverted" + + const manageStay = intl.formatMessage({ + defaultMessage: "Manage stay", + }) + + return ( + + + + {manageStay} + + + + + + {({ close }) => ( + <> +
+ + {manageStay} + + + + +
+
+ + +
+ + )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css new file mode 100644 index 000000000..eb1c294cb --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/ManageStay/manageStay.module.css @@ -0,0 +1,52 @@ +.trigger { + align-items: center; + background-color: var(--Component-Button-Brand-Tertiary-Fill-Default); + border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Inverted); + cursor: pointer; + display: flex; + gap: var(--Space-x1); + height: 48px; + justify-content: center; + padding: var(--Space-x2) var(--Space-x4); + transition: background-color 200ms ease; + + &:hover { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover); + } + + &:disabled { + background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled); + cursor: not-allowed; + } +} + +.dialog { + display: grid; + gap: var(--Space-x3); +} + +.header { + align-items: center; + display: flex; + gap: var(--Space-x2); + justify-content: space-between; +} + +.title { + color: var(--Text-Default); +} + +.close { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.content { + display: grid; + gap: var(--Space-x3); + grid-template-columns: 1fr 1fr; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx new file mode 100644 index 000000000..d0a9014f7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/index.tsx @@ -0,0 +1,34 @@ +"use client" +import Link from "next/link" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import ManageStay from "./ManageStay" + +import styles from "./notCancelled.module.css" + +export default function NotCancelled() { + const intl = useIntl() + const location = useMyStayStore((state) => state.hotel.location) + + const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}` + return ( + <> + + + + + {intl.formatMessage({ + defaultMessage: "Find us", + })} + + + + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css new file mode 100644 index 000000000..279f6bc24 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/NotCancelled/notCancelled.module.css @@ -0,0 +1,9 @@ +.link { + align-items: center; + border: 2px solid var(--Component-Button-Brand-Secondary-Border-Default); + border-radius: var(--Corner-radius-rounded); + color: var(--Text-Interactive-Default); + display: flex; + justify-content: center; + text-decoration: none; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css new file mode 100644 index 000000000..96af8da99 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/actions.module.css @@ -0,0 +1,12 @@ +.actionArea { + display: grid; + gap: var(--Spacing-x2); +} + +@media (min-width: 768px) { + .actionArea { + gap: var(--Spacing-x2); + grid-template-columns: 1fr 1fr; + padding-top: var(--Spacing-x3); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx new file mode 100644 index 000000000..504a67c86 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/index.tsx @@ -0,0 +1,16 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import Cancelled from "./Cancelled" +import NotCancelled from "./NotCancelled" + +import styles from "./actions.module.css" + +export default function Actions() { + const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled) + return ( +
+ {isCancelled ? : } +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css similarity index 50% rename from apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css index b28e47db1..8e6197099 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/totalPrice.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/bookingCode.module.css @@ -1,5 +1,5 @@ -.totalPrice { - display: flex; +.row { align-items: center; - gap: 10px; + display: flex; + justify-content: space-between; } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx new file mode 100644 index 000000000..4230d4885 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/BookingCode/index.tsx @@ -0,0 +1,41 @@ +"use client" +import { useIntl } from "react-intl" + +import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import IconChip from "@/components/TempDesignSystem/IconChip" + +import styles from "./bookingCode.module.css" + +export default function BookingCode() { + const intl = useIntl() + const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode) + + if (!bookingCode) { + return null + } + + return ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "Booking code", + })} +

+
+ + } + > + + {bookingCode} + + +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css new file mode 100644 index 000000000..6ae0066a8 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/cancellations.module.css @@ -0,0 +1,16 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.row .textDefault { + color: var(--Text-Default); + text-transform: capitalize; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx new file mode 100644 index 000000000..5e60ca1be --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Cancellations/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./cancellations.module.css" + +export default function Cancellations() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + const cancelledRooms = rooms.filter((r) => r.isCancelled).length + + if (!cancelledRooms) { + return null + } + + const totalRoomsMsg = intl.formatMessage( + { + defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}", + }, + { totalRooms: cancelledRooms } + ) + + return ( +
+
+ + +

+ {intl.formatMessage({ defaultMessage: "Cancellations" })} +

+
+
+ + +

{totalRoomsMsg}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/dates.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx new file mode 100644 index 000000000..5b436d4aa --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Dates/index.tsx @@ -0,0 +1,53 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { dt } from "@/lib/dt" +import { useMyStayStore } from "@/stores/my-stay" + +import useLang from "@/hooks/useLang" + +import styles from "./dates.module.css" + +export default function Dates() { + const intl = useIntl() + const lang = useLang() + const { checkInDate, checkOutDate } = useMyStayStore((state) => ({ + checkInDate: state.bookedRoom.checkInDate, + checkOutDate: state.bookedRoom.checkOutDate, + })) + + const from = dt(checkInDate).locale(lang).format("D MMM") + const fromYear = dt(checkInDate).year() + const to = dt(checkOutDate).locale(lang).format("D MMM") + const toYear = dt(checkOutDate).year() + + const isSameYear = fromYear === toYear + + const stayFrom = isSameYear ? from : `${from}, ${fromYear}` + const stayTo = `${to}, ${toYear}` + + return ( +
+
+ + +

+ {intl.formatMessage({ + defaultMessage: "Dates", + })} +

+
+
+ + +

+ {/* eslint-disable formatjs/no-literal-string-in-jsx */} + {stayFrom} → {stayTo} +

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/guaranteeInfo.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx new file mode 100644 index 000000000..f260c0756 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/GuaranteeInfo/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./guaranteeInfo.module.css" + +export default function GuaranteeInfo() { + const intl = useIntl() + const { allRoomsAreCancelled, guaranteeInfo } = useMyStayStore((state) => ({ + allRoomsAreCancelled: state.allRoomsAreCancelled, + guaranteeInfo: state.bookedRoom.guaranteeInfo, + })) + + if (allRoomsAreCancelled || !guaranteeInfo) { + return null + } + + return ( +
+
+ + +

+ {intl.formatMessage({ + defaultMessage: "Late arrival", + })} +

+
+
+ +

+ {intl.formatMessage({ + defaultMessage: "Check-in after 18:00", + })} +

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css new file mode 100644 index 000000000..c5d9fe824 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/guests.module.css @@ -0,0 +1,20 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} + +.row p.guests { + color: var(--Text-Default); + text-transform: capitalize; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx new file mode 100644 index 000000000..0a59ff680 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Guests/index.tsx @@ -0,0 +1,61 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./guests.module.css" + +export default function Guests() { + const intl = useIntl() + const rooms = useMyStayStore((state) => state.rooms) + + const adults = rooms.reduce((acc, room) => acc + room.adults, 0) + + const children = rooms.reduce( + (acc, room) => acc + (room.childrenAges?.length ?? 0), + 0 + ) + + const adultsMsg = intl.formatMessage( + { + defaultMessage: "{adults, plural, one {# adult} other {# adults}}", + }, + { adults } + ) + + const childrenMsg = intl.formatMessage( + { + defaultMessage: "{children, plural, one {# child} other {# children}}", + }, + { children } + ) + + const adultsOnlyMsg = adultsMsg + const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(" · ") + + let guests = "" + if (children > 0) { + guests = adultsAndChildrenMsg + } else { + guests = adultsOnlyMsg + } + + return ( +
+
+ + +

+ {intl.formatMessage({ defaultMessage: "Guests" })} +

+
+
+ +

{guests}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css new file mode 100644 index 000000000..a88172b13 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/button.module.css @@ -0,0 +1,14 @@ +.button { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + gap: var(--Space-x1); + padding: var(--Space-x1) 0; + width: 100%; +} + +.text { + color: var(--Text-Interactive-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx new file mode 100644 index 000000000..08d04a02f --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/Button/index.tsx @@ -0,0 +1,31 @@ +"use client" +import { Button as ButtonRAC } from "react-aria-components" + +import { + MaterialIcon +,type + MaterialIconProps} from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./button.module.css" + + +interface ButtonProps extends React.PropsWithChildren { + icon: MaterialIconProps["icon"] + isDisabled?: boolean +} + +export default function Button({ + children, + icon, + isDisabled = false, +}: ButtonProps) { + return ( + + + + {children} + + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css new file mode 100644 index 000000000..125e21d86 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/body.module.css @@ -0,0 +1,15 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + max-height: 70vh; + overflow-y: auto; + width: 100%; +} + +@media screen and (min-width: 768px) { + .content { + width: 640px; + max-width: 100%; + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx new file mode 100644 index 000000000..0558f3f4b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Body/index.tsx @@ -0,0 +1,5 @@ +import styles from "./body.module.css" + +export default function Body({ children }: React.PropsWithChildren) { + return
{children}
+} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css new file mode 100644 index 000000000..a4a66ca3c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/footer.module.css @@ -0,0 +1,7 @@ +.footer { + border-top: 1px solid var(--Base-Border-Subtle); + display: flex; + justify-content: space-between; + padding-top: var(--Spacing-x3); + width: 100%; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx new file mode 100644 index 000000000..8947c186c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Footer/index.tsx @@ -0,0 +1,64 @@ +import Button from "@/components/TempDesignSystem/Button" + +import styles from "./footer.module.css" + +import type { ButtonHTMLAttributes, PropsWithChildren } from "react" +import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components" + +import type { ButtonProps as _ButtonProps } from "@/components/TempDesignSystem/Button/button" + +export default function Footer({ children }: PropsWithChildren) { + return
{children}
+} + +interface ButtonProps extends PropsWithChildren { + intent?: _ButtonProps["intent"] + onClick?: ReactAriaButtonProps["onPress"] + type?: ButtonHTMLAttributes["type"] +} + +interface PrimaryButtonProps extends ButtonProps { + disabled?: boolean + form?: string +} + +Footer.Primary = function PrimaryButton({ + children, + disabled = false, + form, + intent = "primary", + onClick, + type = "button", +}: PrimaryButtonProps) { + return ( + + ) +} + +Footer.Secondary = function SecondaryButton({ + children, + intent = "text", + onClick, + type = "button", +}: ButtonProps) { + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css new file mode 100644 index 000000000..814fc100d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/header.module.css @@ -0,0 +1,15 @@ +.header { + display: grid; + gap: var(--Space-x05) var(--Space-x2); + grid-template-columns: 1fr auto; +} + +.close { + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + padding: 0; +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx new file mode 100644 index 000000000..039e1a681 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/Header/index.tsx @@ -0,0 +1,24 @@ +import { Button as ButtonRAC } from "react-aria-components" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" + +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./header.module.css" + +interface HeaderProps extends React.PropsWithChildren { + handleClose: () => void + title: string +} + +export default function Header({ children, handleClose, title }: HeaderProps) { + return ( +
+ {title} + + + + {children} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx new file mode 100644 index 000000000..4a237afaf --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/index.tsx @@ -0,0 +1,15 @@ +import Body from "./Body" +import Footer from "./Footer" +import Header from "./Header" + +import styles from "./modalContent.module.css" + +import type { PropsWithChildren } from "react" + +export default function ModalContent({ children }: PropsWithChildren) { + return
{children}
+} + +ModalContent.Body = Body +ModalContent.Footer = Footer +ModalContent.Header = Header diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css new file mode 100644 index 000000000..37a8f2d91 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/ModalContent/modalContent.module.css @@ -0,0 +1,4 @@ +.container { + display: grid; + gap: var(--Space-x3); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx new file mode 100644 index 000000000..82be1e27c --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/index.tsx @@ -0,0 +1,17 @@ +import { Modal as ModalRAC, ModalOverlay } from "react-aria-components" + +import Button from "./Button" +import ModalContent from "./ModalContent" + +import styles from "./modal.module.css" + +export default function Modal({ children }: React.PropsWithChildren) { + return ( + + {children} + + ) +} + +Modal.Button = Button +Modal.Content = ModalContent diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css new file mode 100644 index 000000000..f796f38c9 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Modal/modal.module.css @@ -0,0 +1,70 @@ +.overlay { + background: rgba(0, 0, 0, 0.4); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + width: 100dvw; + z-index: var(--default-modal-overlay-z-index); + + &[data-entering] { + animation: overlay-fade 200ms; + } + + &[data-exiting] { + animation: overlay-fade 150ms reverse ease-in; + } +} + +@keyframes overlay-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.modal { + background: var(--UI-Input-Controls-Surface-Normal); + border-top-left-radius: var(--Corner-radius-Large); + border-top-right-radius: var(--Corner-radius-Large); + max-height: 95dvh; + overflow-y: auto; + padding: var(--Space-x3); + position: absolute; + z-index: var(--default-modal-z-index); + + &[data-entering] { + animation: modal-anim 200ms; + } + + &[data-exiting] { + animation: modal-anim 150ms reverse ease-in; + } +} + +@keyframes modal-anim { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } +} + +@media screen and (min-width: 768px) { + .overlay { + align-items: center; + display: flex; + justify-content: center; + } + + .modal { + border-radius: var(--Corner-radius-Large); + width: min(690px, 100dvw); + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx similarity index 92% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx index 0934653b1..f4e2ea81e 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/index.tsx @@ -4,22 +4,20 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./priceContainer.module.css" interface PriceContainerProps { - text: string - price: number - currencyCode: string - nightsText: string adultsText: string childrenText: string + nightsText: string + price: string + text: string totalChildren?: number } export default function PriceContainer({ - text, - price, - currencyCode, - nightsText, adultsText, childrenText, + nightsText, + price, + text, totalChildren = 0, }: PriceContainerProps) { return ( @@ -37,7 +35,7 @@ export default function PriceContainer({
- {price} {currencyCode} + {price}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/priceContainer.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/ManageStay/ActionPanel/PriceContainer/priceContainer.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/PriceContainer/priceContainer.module.css diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx new file mode 100644 index 000000000..0a08f7700 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/index.tsx @@ -0,0 +1,49 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import Divider from "@/components/TempDesignSystem/Divider" + +import styles from "./reference.module.css" + +export default function Reference() { + const intl = useIntl() + const { cancellationNumber, confirmationNumber, isCancelled, rooms } = + useMyStayStore((state) => ({ + cancellationNumber: state.bookedRoom.cancellationNumber, + confirmationNumber: state.bookedRoom.confirmationNumber, + isCancelled: state.bookedRoom.isCancelled, + rooms: state.rooms, + })) + + if (rooms.length > 1) { + return null + } + + const title = isCancelled + ? intl.formatMessage({ + defaultMessage: "Cancellation number", + }) + : intl.formatMessage({ + defaultMessage: "Booking number", + }) + + return ( + <> +
+ +

{title}

+
+ +

+ {isCancelled ? {cancellationNumber} : confirmationNumber} +

+
+
+ + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css new file mode 100644 index 000000000..196d88f5d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Reference/reference.module.css @@ -0,0 +1,10 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; + padding-bottom: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx deleted file mode 100644 index 6ea19e693..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/ReferenceCardSkeleton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import SkeletonShimmer from "@/components/SkeletonShimmer" -import Divider from "@/components/TempDesignSystem/Divider" - -import styles from "./referenceCard.module.css" - -export default function ReferenceCardSkeleton() { - return ( -
-
- -
- -
- - -
-
- - -
-
- - -
- -
- -
-
- - -
-
- ) -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx new file mode 100644 index 000000000..de1c5bcce --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/index.tsx @@ -0,0 +1,43 @@ +"use client" +import { useIntl } from "react-intl" + +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { useMyStayStore } from "@/stores/my-stay" + +import styles from "./room.module.css" + +export default function Room() { + const intl = useIntl() + const { bookedRoom, rooms } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + rooms: state.rooms, + })) + + const roomMsg = intl.formatMessage({ + defaultMessage: "Room", + }) + const roomsMsg = intl.formatMessage({ + defaultMessage: "Rooms", + }) + + const room = + rooms.length > 1 ? `${rooms.length} ${roomsMsg}` : bookedRoom.roomName + const title = rooms.length > 1 ? roomsMsg : roomMsg + + return ( +
+
+ + +

{title}

+
+
+ + +

{room}

+
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css new file mode 100644 index 000000000..fe9adce2d --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Room/room.module.css @@ -0,0 +1,15 @@ +.row { + align-items: center; + display: flex; + justify-content: space-between; +} + +.label { + align-items: center; + display: flex; + gap: var(--Space-x1); +} + +.textDefault { + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx index d223eb15f..38d336df2 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/index.tsx @@ -1,397 +1,48 @@ "use client" - -import { useEffect } from "react" import { useIntl } from "react-intl" -import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" -import { BookingStatusEnum } from "@/constants/booking" -import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" -import IconChip from "@/components/TempDesignSystem/IconChip" -import Link from "@/components/TempDesignSystem/Link" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast" -import useLang from "@/hooks/useLang" -import ManageStay from "../ManageStay" import TotalPrice from "../Rooms/TotalPrice" -import { mapRoomDetails } from "../utils/mapRoomDetails" -import ReferenceCardSkeleton from "./ReferenceCardSkeleton" +import Actions from "./Actions" +import BookingCode from "./BookingCode" +import Cancellations from "./Cancellations" +import Dates from "./Dates" +import GuaranteeInfo from "./GuaranteeInfo" +import Guests from "./Guests" +import Reference from "./Reference" +import Room from "./Room" import styles from "./referenceCard.module.css" -import type { Hotel, Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { CreditCard } from "@/types/user" - -interface ReferenceCardProps { - booking: BookingConfirmation["booking"] - hotel: Hotel - room: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - savedCreditCards: CreditCard[] | null - refId: string - isLoggedIn: boolean -} - -export function ReferenceCard({ - booking, - hotel, - room, - savedCreditCards, - refId, - isLoggedIn, -}: ReferenceCardProps) { +export function ReferenceCard() { const intl = useIntl() - const lang = useLang() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - const addBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.addBookedRoom - ) - const addRoomPrice = useMyStayTotalPriceStore( - (state) => state.actions.addRoomPrice - ) - - // Initialize store with server data - useEffect(() => { - // Add price and details for booked room (main room or single room) - addRoomPrice({ - id: booking.confirmationNumber, - totalPrice: - booking.reservationStatus === BookingStatusEnum.Cancelled - ? 0 - : booking.totalPrice, - currencyCode: booking.currencyCode, - isMainBooking: true, - roomPoints: booking.roomPoints, - }) - addBookedRoom( - mapRoomDetails({ - booking, - room, - roomNumber: 1, - }) - ) - }, [booking, room, addBookedRoom, addRoomPrice]) - useGuaranteePaymentFailedToast() - - if (!bookedRoom.roomNumber) return - - const { - confirmationNumber, - cancellationNumber, - checkInDate, - checkOutDate, - isCancelled, - bookingCode, - rateDefinition, - priceType, - } = bookedRoom - - const isMultiRoom = bookedRoom.linkedReservations.length > 0 - - const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}` - - const allRooms = [bookedRoom, ...linkedReservationRooms] - - const adults = allRooms - .filter((room) => !room.isCancelled) - .reduce((acc, room) => acc + room.adults, 0) - - const children = allRooms - .filter((room) => !room.isCancelled) - .reduce((acc, room) => acc + (room.childrenAges?.length ?? 0), 0) - - const cancelledRooms = allRooms.filter((room) => room.isCancelled).length - const allRoomsCancelled = allRooms.every((room) => room.isCancelled) - - const adultsMsg = intl.formatMessage( - { - defaultMessage: "{adults, plural, one {# adult} other {# adults}}", - }, - { - adults: adults, - } - ) - - const childrenMsg = intl.formatMessage( - { - defaultMessage: "{children, plural, one {# child} other {# children}}", - }, - { - children: children, - } - ) - - const cancelledRoomsMsg = intl.formatMessage( - { - defaultMessage: "{rooms, plural, one {# room} other {# rooms}}", - }, - { - rooms: cancelledRooms, - } - ) - - const roomCancelledRoomsMsg = intl.formatMessage({ - defaultMessage: "Room cancelled", - }) - - const roomsMsg = intl.formatMessage( - { - defaultMessage: "{rooms, plural, one {# room} other {# rooms}}", - }, - { - rooms: allRooms.filter((room) => !room.isCancelled).length, - } - ) - const adultsOnlyMsg = adultsMsg - const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") - const adultsAndRoomsMsg = [adultsMsg, roomsMsg].join(", ") - const adultsAndChildrenAndRoomsMsg = [adultsMsg, childrenMsg, roomsMsg].join( - ", " - ) - return (
- {!isMultiRoom && ( - <> -
- - {intl.formatMessage({ - defaultMessage: "Reference", - })} - - - {isCancelled && !isMultiRoom - ? intl.formatMessage({ - defaultMessage: "Cancellation number", - }) - : intl.formatMessage({ - defaultMessage: "Reference number", - })} - - - {isCancelled && !isMultiRoom - ? cancellationNumber - : confirmationNumber} - -
+ + + + + + + - - - )} - - {!allRoomsCancelled && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Guests", - })} -

-
- -

- {allRooms.length > 1 - ? children > 0 - ? adultsAndChildrenAndRoomsMsg - : adultsAndRoomsMsg - : children > 0 - ? adultsAndChildrenMsg - : adultsOnlyMsg} -

-
-
- )} - {allRooms.some((room) => room.isCancelled) && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Cancellation", - })} -

-
- -

- {isMultiRoom - ? // eslint-disable-next-line formatjs/no-literal-string-in-jsx - `${cancelledRoomsMsg} ${intl.formatMessage({ - defaultMessage: "cancelled", - })}` - : roomCancelledRoomsMsg} -

-
-
- )} - {!allRoomsCancelled && ( - <> -
- -

- {intl.formatMessage({ - defaultMessage: "Check-in", - })} -

-
- -

- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${dt(checkInDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage( - { - defaultMessage: "from", - } - )} ${hotel.hotelFacts.checkin.checkInTime}`} -

-
-
-
- -

- {intl.formatMessage({ - defaultMessage: "Check-out", - })} -

-
- -

- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {`${dt(checkOutDate).locale(lang).format("dddd, D MMMM")} ${intl.formatMessage( - { - defaultMessage: "until", - } - )} ${hotel.hotelFacts.checkin.checkOutTime}`} -

-
-
- - )} - - {booking.guaranteeInfo && !allRoomsCancelled && ( - <> -
- - -

- - {intl.formatMessage({ - defaultMessage: "Booking guaranteed.", - })} - - {/* eslint-disable formatjs/no-literal-string-in-jsx */}{" "} - {/* eslint-enable formatjs/no-literal-string-in-jsx */} - {intl.formatMessage({ - defaultMessage: - "Your stay remains available for check-in after 18:00.", - })} -

-
-
- - - )} - -
- +
+

{intl.formatMessage({ defaultMessage: "Total", })}

- +
- {bookingCode && ( -
- -

- {intl.formatMessage({ - defaultMessage: "Booking code", - })} -

-
- - } - > - {intl.formatMessage( - { - defaultMessage: "Booking code: {value}", - }, - { - value: bookingCode, - strong: (text) => ( - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {text} - - ), - } - )} - - -
- )} -
- - -
- {isMultiRoom && ( - -

- {intl.formatMessage({ - defaultMessage: "Multi-room stay", - })} -

-
- )} - - -

- {rateDefinition.generalTerms.map((term) => ( - - {term} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {term.endsWith(".") ? " " : ". "} - - ))} -

-
+ +
) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css index b496cbbdf..ed766d2ea 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/referenceCard.module.css @@ -1,72 +1,19 @@ .referenceCard { - width: var(--max-width-content); - max-width: 588px; - margin: 0 auto; - padding: var(--Spacing-x3); - border-radius: var(--Corner-radius-Large); background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Large); box-shadow: var(--popup-box-shadow); + display: flex; + flex-direction: column; + gap: var(--Space-x1); + margin: 0 auto; + max-width: 588px; + padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x4); + width: var(--max-width-content); } -.referenceRow { +.row { + align-items: center; display: flex; justify-content: space-between; - align-items: center; - padding-bottom: var(--Spacing-x-one-and-half); -} - -.divider { - margin-bottom: var(--Spacing-x-one-and-half); -} - -.cancelledRooms { - color: var(--Scandic-Brand-Scandic-Red); -} - -.actionArea { - display: flex; - gap: var(--Spacing-x2); - margin: var(--Spacing-x4) 0 var(--Spacing-x3); -} - -.note { - text-align: center; - width: 80%; - margin: 0 auto; -} - -.cancelledNote { - color: var(--UI-Text-Placeholder); -} - -.titleDesktop { - display: none; -} - -.guaranteed { - align-items: flex-start; - border-radius: var(--Corner-radius-Medium); - display: flex; - background-color: var(--Surface-Feedback-Succes); - gap: var(--Spacing-x1); - padding: var(--Spacing-x1); - margin-bottom: var(--Space-x1); -} - -.guaranteedText { - color: var(--Surface-Feedback-Succes-Accent); -} - -@media (min-width: 768px) { - .actionArea { - gap: var(--Spacing-x3); - } - - .titleMobile { - display: none; - } - - .titleDesktop { - display: block; - } + padding-top: var(--Space-x1); } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/MultiRoomSkeleton.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/MultiRoomSkeleton.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/MultiRoomSkeleton.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/ToggleSidePeek.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/ToggleSidePeek.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/ToggleSidePeek.tsx diff --git a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx similarity index 55% rename from apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx index 6f2cf7363..365bbcc46 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/MultiRoom/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/MultiRoom/index.tsx @@ -1,136 +1,59 @@ "use client" -import { use, useEffect } from "react" import { useIntl } from "react-intl" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" -import { BookingStatusEnum } from "@/constants/booking" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" +import { IconForFeatureCode } from "@/components/HotelReservation/utils" import Image from "@/components/Image" import Divider from "@/components/TempDesignSystem/Divider" import IconChip from "@/components/TempDesignSystem/IconChip" import useLang from "@/hooks/useLang" +import { formatPrice } from "@/utils/numberFormatting" -import { IconForFeatureCode } from "../../utils" -import { hasModifiableRate } from "../utils" -import { hasBreakfastPackageFromBookingFlow } from "../utils/hasBreakfastPackage" -import { mapRoomDetails } from "../utils/mapRoomDetails" -import MultiRoomSkeleton from "./MultiRoomSkeleton" -import PriceType from "./PriceType" +import PriceType from "../../PriceType" +import { hasModifiableRate } from "../../utils" import ToggleSidePeek from "./ToggleSidePeek" import styles from "./multiRoom.module.css" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import type { Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" +import type { Room } from "@/types/stores/my-stay" +import type { SafeUser } from "@/types/user" interface MultiRoomProps { - booking?: BookingConfirmation["booking"] - room?: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - bookingPromise?: Promise - index?: number - user: User | null + booking: Room + roomNr: number + user: SafeUser } -export default function MultiRoom({ - room: initialRoom, - booking: initialBooking, - bookingPromise, - index, - user, -}: MultiRoomProps) { +export default function MultiRoom({ booking, roomNr, user }: MultiRoomProps) { const intl = useIntl() const lang = useLang() - const addRoomPrice = useMyStayTotalPriceStore( - (state) => state.actions.addRoomPrice - ) - - const addLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.addLinkedReservationRoom - ) - - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - - const allRooms = [bookedRoom, ...linkedReservationRooms] - - // Resolve promise data directly without setState - let bookingInfo = initialBooking - let roomInfo = initialRoom - - if (bookingPromise) { - const promiseData = use(bookingPromise) - if (promiseData) { - bookingInfo = promiseData.booking - roomInfo = promiseData.room - } - } - const isBookingCancelled = - bookingInfo?.reservationStatus === BookingStatusEnum.Cancelled - - const multiRoom = allRooms.find( - (room) => room.confirmationNumber === bookingInfo?.confirmationNumber - ) - - // Update stores when data is available - useEffect(() => { - if (bookingInfo) { - addRoomPrice({ - id: bookingInfo.confirmationNumber, - totalPrice: isBookingCancelled ? 0 : bookingInfo.totalPrice, - currencyCode: bookingInfo.currencyCode, - isMainBooking: false, - roomPoints: bookingInfo.roomPoints, - }) - - // Add room details to the store - addLinkedReservationRoom( - mapRoomDetails({ - booking: bookingInfo, - room: roomInfo ?? null, - roomNumber: index !== undefined ? index + 2 : 1, - }) - ) - } - }, [ - bookingInfo, - roomInfo, - index, - isBookingCancelled, - addRoomPrice, - addLinkedReservationRoom, - ]) - - if (!multiRoom?.roomNumber) return const { adults, + breakfast, + cancellationNumber, checkInDate, cheques, childrenAges, confirmationNumber, - cancellationNumber, + currencyCode, hotelId, - roomPoints, packages, rateDefinition, + room, + roomName, + roomPoints, isCancelled, priceType, + roomTypeCode, vouchers, totalPrice, - } = multiRoom + } = booking const fromDate = dt(checkInDate).locale(lang) @@ -155,10 +78,27 @@ export default function MultiRoom({ const adultsOnlyMsg = adultsMsg const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) + + let breakfastPrice = intl.formatMessage({ + defaultMessage: "No breakfast", + }) + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ + defaultMessage: "Included", + }) + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } + return (
-

{roomInfo?.name}

+

{roomName}

{isCancelled ? ( @@ -189,7 +129,7 @@ export default function MultiRoom({ defaultMessage: "Room {roomIndex}", }, { - roomIndex: index !== undefined ? index + 2 : 1, + roomIndex: roomNr, } )} @@ -229,8 +169,8 @@ export default function MultiRoom({
@@ -243,30 +183,30 @@ export default function MultiRoom({ item.code as RoomPackageCodeEnum ) ) && ( -
- {packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) +
+ {packages + .filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum ) - .map((item) => { - return ( - - - - ) - })} -
- )} + ) + .map((item) => { + return ( + + + + ) + })} +
+ )}
{roomInfo?.name
@@ -285,18 +225,20 @@ export default function MultiRoom({

-
- -

- {intl.formatMessage({ - defaultMessage: "Terms", - })} -

-
- -

{rateDefinition.cancellationText}

-
-
+ {rateDefinition.cancellationText ? ( +
+ +

+ {intl.formatMessage({ + defaultMessage: "Terms", + })} +

+
+ +

{rateDefinition.cancellationText}

+
+
+ ) : null} {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -315,31 +257,21 @@ export default function MultiRoom({
)} -
- -

- {intl.formatMessage({ - defaultMessage: "Breakfast", - })} -

-
- - -

- {hasBreakfastPackageFromBookingFlow( - packages?.map((pkg) => ({ - code: pkg.code, - })) ?? [] - ) - ? intl.formatMessage({ - defaultMessage: "Included", - }) - : intl.formatMessage({ - defaultMessage: "Not included", + {breakfastPrice !== null && ( +

+ +

+ {intl.formatMessage({ + defaultMessage: "Breakfast", })} -

-
-
+

+
+ + +

{breakfastPrice}

+
+
+ )}
@@ -351,6 +283,7 @@ export default function MultiRoom({ state.bookedRoom) - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) + const { + adults, + bookingCode, + breakfast, + checkInDate, + cheques, + childrenAges, + confirmationNumber, + formattedTotalPrice, + hotel, + isCancelled, + packages, + priceType, + rateDefinition, + roomName, + roomNumber, + roomPoints, + roomTypeCode, + totalPrice, + vouchers, + } = useMyStayStore((state) => ({ + adults: state.bookedRoom.adults, + bookingCode: state.bookedRoom.bookingCode, + breakfast: state.bookedRoom.breakfast, + checkInDate: state.bookedRoom.checkInDate, + cheques: state.bookedRoom.cheques, + childrenAges: state.bookedRoom.childrenAges, + confirmationNumber: state.bookedRoom.confirmationNumber, + formattedTotalPrice: state.totalPrice, + hotel: state.hotel, + isCancelled: state.bookedRoom.isCancelled, + packages: state.bookedRoom.packages, + priceType: state.bookedRoom.priceType, + rateDefinition: state.bookedRoom.rateDefinition, + roomName: state.bookedRoom.roomName, + roomNumber: state.bookedRoom.roomNumber, + roomPoints: state.bookedRoom.roomPoints, + roomTypeCode: state.bookedRoom.roomTypeCode, + totalPrice: state.bookedRoom.totalPrice, + vouchers: state.bookedRoom.vouchers, + })) - if (!bookedRoom.roomNumber) { + if (!roomNumber) { return (
@@ -53,23 +88,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { ) } - const fromDate = dt(bookedRoom.checkInDate).locale(lang) - - const { - adults, - bookingCode, - breakfast, - cheques, - childrenAges, - confirmationNumber, - isCancelled, - packages, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, - } = bookedRoom + const fromDate = dt(checkInDate).locale(lang) const mainBedWidthValueMsg = intl.formatMessage( { @@ -117,23 +136,24 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { ) ) - const breakfastText = rateDefinition.breakfastIncluded - ? intl.formatMessage({ + let breakfastPrice = null + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ defaultMessage: "Included", }) - : breakfast - ? formatPrice( - intl, - breakfast.localPrice.totalPrice, - breakfast.localPrice.currency - ) - : null + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } return (
-

{bookedRoom.roomName}

+

{roomName}

@@ -145,7 +165,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { defaultMessage: "Room {roomIndex}", }, { - roomIndex: bookedRoom.roomNumber, + roomIndex: roomNumber, } )} @@ -169,7 +189,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
@@ -183,32 +203,32 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) { item.code as RoomPackageCodeEnum ) ) && ( -
- {packages - .filter((item) => - Object.values(RoomPackageCodeEnum).includes( - item.code as RoomPackageCodeEnum - ) +
+ {packages + .filter((item) => + Object.values(RoomPackageCodeEnum).includes( + item.code as RoomPackageCodeEnum ) - .map((item) => { - return ( - - - - ) - })} -
- )} + ) + .map((item) => { + return ( + + + + ) + })} +
+ )}
{bookedRoom.roomName} @@ -240,29 +260,31 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
-
- - - -

- {intl.formatMessage({ - defaultMessage: "Terms", - })} -

-
-
-
- -

- {rateDefinition.cancellationText} -

-
+ {rateDefinition.cancellationText ? ( +
+ + + +

+ {intl.formatMessage({ + defaultMessage: "Terms", + })} +

+
+
+
+ +

+ {rateDefinition.cancellationText} +

+
+
-
+ ) : null} {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -296,7 +318,7 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
)} - {breakfastText !== null && ( + {breakfastPrice !== null && (
-

{breakfastText}

+

{breakfastPrice}

@@ -368,22 +390,18 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {

{bedType.mainBed.description} {bedType.mainBed.widthRange.min === - bedType.mainBed.widthRange.max + bedType.mainBed.widthRange.max ? // eslint-disable-next-line formatjs/no-literal-string-in-jsx - ` (${mainBedWidthValueMsg})` + ` (${mainBedWidthValueMsg})` : // eslint-disable-next-line formatjs/no-literal-string-in-jsx - ` (${mainBedWidthRangeMsg})`} + ` (${mainBedWidthRangeMsg})`}

- +
@@ -421,9 +439,10 @@ export function SingleRoom({ bedType, image, hotel, user }: RoomProps) {
- +
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css similarity index 99% rename from apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css rename to apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css index 805089901..ee54a0f64 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/room.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/SingleRoom/room.module.css @@ -31,6 +31,7 @@ flex-direction: column; overflow: hidden; } + .content { display: grid; gap: var(--Spacing-x2); @@ -160,6 +161,7 @@ } .price { + align-items: center; display: flex; gap: var(--Spacing-x1); justify-content: space-between; @@ -187,51 +189,63 @@ background-color: transparent; padding: 0; } + .roomName { padding: 0; } + .roomHeader { justify-content: space-between; align-items: center; flex-direction: row; padding: 0; } + .sidePeek { display: block; } + .booking { border-radius: var(--Corner-radius-Large); background-color: var(--Base-Background-Primary-Normal); } + .content { padding: var(--Spacing-x2); grid-template-columns: 3fr 2fr; width: var(--max-width-content); } + .packages { top: 620px; left: 25px; } + .imageContainer { height: 640px; } + .image { height: 100%; border-radius: var(--Corner-radius-Medium); } + .bookingDetails { padding: 0; } + .row { border-bottom: 1px solid var(--Base-Border-Subtle); flex-direction: row; align-items: center; justify-content: space-between; } + .rowTitle svg { width: 20px; height: 20px; } + .bookingInformation { flex-direction: row; justify-content: space-between; @@ -241,17 +255,21 @@ border-radius: 0; border: none; } + .priceDetails { margin: 0 0 0 auto; width: auto; align-items: flex-end; } + .price { justify-content: flex-end; } + .guestDetailsMobileWrapper { display: none; } + .guestDetailsDesktopWrapper { display: block; margin-top: auto; diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx new file mode 100644 index 000000000..3d5451000 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice.tsx @@ -0,0 +1,26 @@ +"use client" +import { useMyStayStore } from "@/stores/my-stay" + +import PriceType from "../PriceType" + +import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay" + +export default function TotalPrice() { + const { bookedRoom, formattedTotalPrice } = useMyStayStore((state) => ({ + bookedRoom: state.bookedRoom, + formattedTotalPrice: state.totalPrice, + })) + + return ( + + ) +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx deleted file mode 100644 index fd4308ce0..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/TotalPrice/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" -import { useMyStayTotalPriceStore } from "@/stores/my-stay/myStayTotalPrice" - -import Points from "../../Points" -import Price from "../../Price" - -import styles from "./totalPrice.module.css" - -import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay" - -export type Variant = - | "Title/Subtitle/lg" - | "Title/Subtitle/md" - | "Body/Paragraph/mdBold" - -interface TotalPriceProps { - variant: Variant - type?: PriceType -} - -export default function TotalPrice({ - variant, - type = "money", -}: TotalPriceProps) { - const totalPrice = useMyStayTotalPriceStore((state) => state.totalPrice) - const totalPoints = useMyStayTotalPriceStore((state) => state.totalPoints) - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - - const { vouchers, cheques } = bookedRoom - - const intl = useIntl() - if (type === "money") { - return - } - - if (type === "voucher") { - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - } - - if (type === "cheque") { - return ( -
- - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} -

{cheques} CC +

-
- -
- ) - } - - if (totalPrice && totalPrice > 0 && type === "points") { - return ( -
- {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - +{" "} - -
- ) - } - - return -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx index cc308e55c..e895f3e81 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Rooms/index.tsx @@ -1,54 +1,37 @@ -import { Suspense } from "react" +"use client" +import { useIntl } from "react-intl" import { Typography } from "@scandic-hotels/design-system/Typography" -import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" +import { useMyStayStore } from "@/stores/my-stay" -import { getIntl } from "@/i18n" - -import MultiRoom from "../MultiRoom" -import MultiRoomSkeleton from "../MultiRoom/MultiRoomSkeleton" import PriceDetails from "../PriceDetails" -import { SingleRoom } from "../SingleRoom" -import { getPriceType } from "../utils/getPriceType" +import MultiRoom from "./MultiRoom" +import SingleRoom from "./SingleRoom" import TotalPrice from "./TotalPrice" import styles from "./rooms.module.css" -import { type Hotel, type Room } from "@/types/hotel" -import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" interface RoomsProps { - booking: BookingConfirmation["booking"] - room: - | (Room & { - bedType: Room["roomTypes"][number] - }) - | null - hotel: Hotel - user: User | null + user: SafeUser } -export default async function Rooms({ - booking, - room, - hotel, - user, -}: RoomsProps) { - const intl = await getIntl() +export default function Rooms({ user }: RoomsProps) { + const intl = useIntl() + const { allRoomsAreCancelled, room, rooms } = useMyStayStore((state) => ({ + allRoomsAreCancelled: state.allRoomsAreCancelled, + hotel: state.hotel, + room: state.bookedRoom.room, + rooms: state.rooms, + })) if (!room) { return null } - const linkedBookingPromises = booking.linkedReservations - ? booking.linkedReservations.map((linkedBooking) => { - return getBookingConfirmation(linkedBooking.confirmationNumber) - }) - : [] - - const isMultiRoom = booking.linkedReservations.length > 0 + const isMultiRoom = rooms.length > 1 return (
@@ -66,24 +49,16 @@ export default async function Rooms({ ) : (
- - {booking.linkedReservations.map((linkedRes, index) => ( + {rooms.map((booking, index) => (
- }> - - +
))}
@@ -101,17 +76,10 @@ export default async function Rooms({ {":"}

- +
- + {allRoomsAreCancelled ? null : } )} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx b/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx deleted file mode 100644 index f16f0b9a1..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/SingleRoom/PriceType.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client" -import { useIntl } from "react-intl" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" - -import Cheques from "../Cheques" -import Points from "../Points" -import Price from "../Price" - -import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" - -export default function PriceType() { - const intl = useIntl() - const { - cheques, - isCancelled, - priceType, - rateDefinition, - roomPoints, - totalPrice, - vouchers, - } = useMyStayRoomDetailsStore((state) => ({ - cheques: state.bookedRoom.cheques, - isCancelled: state.bookedRoom.isCancelled, - priceType: state.bookedRoom.priceType, - rateDefinition: state.bookedRoom.rateDefinition, - roomPoints: state.bookedRoom.roomPoints, - totalPrice: state.bookedRoom.totalPrice, - vouchers: state.bookedRoom.vouchers, - })) - - switch (priceType) { - case PriceTypeEnum.cheque: - return - case PriceTypeEnum.money: - return ( - - ) - case PriceTypeEnum.points: - return - case PriceTypeEnum.voucher: - return ( - -

- {intl.formatMessage( - { - defaultMessage: "{count} voucher", - }, - { count: vouchers } - )} -

-
- ) - default: - return null - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx similarity index 90% rename from apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx rename to apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx index 598c86507..d64afa4bd 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/GuaranteeLateArrivalCallback/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/TrackGuarantee.tsx @@ -16,7 +16,7 @@ import { buildAncillaries } from "@/utils/tracking/myStay" import { buildAncillaryPackages, getAncillarySessionData, -} from "../../Ancillaries/utils" +} from "@/components/HotelReservation/MyStay/utils/ancillaries" interface TrackGuaranteeProps { status: string @@ -93,21 +93,21 @@ export default function TrackGuarantee({ case PaymentCallbackStatusEnum.Cancel: isAncillaryFlow ? trackAncillaryPaymentEvent( - "GuaranteeCancelAncillary", - "glacardsavecancelled" - ) + "GuaranteeCancelAncillary", + "glacardsavecancelled" + ) : trackGuaranteePaymentEvent( - "glaCardSaveCancelled", - "glacardsavecancelled" - ) + "glaCardSaveCancelled", + "glacardsavecancelled" + ) break case PaymentCallbackStatusEnum.Error: isAncillaryFlow ? trackAncillaryPaymentEvent( - "GuaranteeFailAncillary", - "glacardsavefailed" - ) + "GuaranteeFailAncillary", + "glacardsavefailed" + ) : trackGuaranteePaymentEvent("glaCardSaveFailed", "glacardsavefailed") break } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx deleted file mode 100644 index 677a42488..000000000 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { cookies } from "next/headers" -import { notFound } from "next/navigation" - -import { Typography } from "@scandic-hotels/design-system/Typography" - -import { env } from "@/env/server" -import { dt } from "@/lib/dt" -import { - getAncillaryPackages, - getBookingConfirmation, - getPackages, - getProfileSafely, - getSavedPaymentCardsSafely, -} from "@/lib/trpc/memoizedRequests" -import { decrypt } from "@/server/routers/utils/encryption" - -import Image from "@/components/Image" -import { getIntl } from "@/i18n" -import { getLang } from "@/i18n/serverContext" -import { getCurrentWebUrl } from "@/utils/url" - -import AdditionalInfoForm from "../FindMyBooking/AdditionalInfoForm" -import accessBooking, { - ACCESS_GRANTED, - ERROR_BAD_REQUEST, - ERROR_UNAUTHORIZED, -} from "./accessBooking" -import { Ancillaries } from "./Ancillaries" -import BookingSummary from "./BookingSummary" -import { Header } from "./Header" -import Promo from "./Promo" -import { ReferenceCard } from "./ReferenceCard" -import Rooms from "./Rooms" - -import styles from "./myStay.module.css" - -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - -export async function MyStay({ refId }: { refId: string }) { - const value = decrypt(refId) - if (!value) { - return notFound() - } - const [confirmationNumber, lastName] = value.split(",") - const bookingConfirmation = await getBookingConfirmation(confirmationNumber) - if (!bookingConfirmation) { - return notFound() - } - - const { booking, hotel, additionalData, room } = bookingConfirmation - const user = await getProfileSafely() - const bv = cookies().get("bv")?.value - const intl = await getIntl() - - const access = accessBooking(booking.guest, lastName, user, bv) - - if (access === ACCESS_GRANTED) { - const lang = getLang() - const ancillaryPackages = await getAncillaryPackages({ - fromDate: dt(booking.checkInDate).format("YYYY-MM-DD"), - hotelId: hotel.operaId, - toDate: dt(booking.checkOutDate).format("YYYY-MM-DD"), - }) - - const packages = await getPackages({ - startDate: dt(booking.checkInDate).format("YYYY-MM-DD"), - hotelId: hotel.operaId, - endDate: dt(booking.checkOutDate).format("YYYY-MM-DD"), - adults: booking.adults, - children: booking.childrenAges.length, - packageCodes: [ - BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, - BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, - BreakfastPackageEnum.FREE_CHILD_BREAKFAST, - ], - lang, - }) - - const supportedCards = hotel.merchantInformationData.cards - const savedCreditCards = await getSavedPaymentCardsSafely({ - supportedCards, - }) - - const imageSrc = - hotel.hotelContent.images.imageSizes.large ?? - additionalData.gallery?.heroImages[0]?.imageSizes.large ?? - hotel.galleryImages[0]?.imageSizes.large - - const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" - const promoUrl = env.HIDE_FOR_NEXT_RELEASE - ? new URL(getCurrentWebUrl({ path: "/", lang, baseUrl })) - : new URL(`${baseUrl}/${lang}/`) - - promoUrl.searchParams.set("hotel", hotel.operaId) - - return ( -
-
-
- {imageSrc && ( - {hotel.name} - )} -
-
-
-
- -
- {booking.showAncillaries && ( - - )} - - - - - -
-
- ) - } - - if (access === ERROR_BAD_REQUEST) { - return ( -
-
- -
-
- ) - } - - if (access === ERROR_UNAUTHORIZED) { - return ( -
-
- -

- {intl.formatMessage({ - defaultMessage: "You need to be logged in to view your booking", - })} -

-
- -

- {intl.formatMessage({ - defaultMessage: - "And you need to be logged in with the same member account that made the booking.", - })} -

-
-
-
- ) - } - - return notFound() -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css index 86c76c1fb..9ac3f70e9 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/myStay.module.css @@ -1,38 +1,3 @@ -.main { - background-color: var(--Base-Surface-Primary-light-Normal); -} - -.imageContainer { - position: absolute; - width: 100%; - height: 480px; -} - -.blurOverlay { - position: absolute; - inset: 0; - backdrop-filter: blur(12px); - pointer-events: none; - z-index: 1; - mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%); - background: linear-gradient( - to bottom, - rgba(0, 0, 0, 0.5) 0%, - transparent 100% - ); -} - -.image { - object-fit: cover; - object-position: center; -} - -.headerContainer { - display: flex; - flex-direction: column; - gap: var(--Spacing-x4); -} - .content { width: 100%; display: flex; @@ -44,13 +9,6 @@ padding-bottom: var(--Spacing-x3); } -.form { - max-width: 640px; - margin-left: auto; - margin-right: auto; - padding: var(--Spacing-x5) 0; -} - .headerSkeleton { display: flex; flex-direction: column; @@ -59,6 +17,12 @@ padding: var(--Spacing-x6) var(--Spacing-x2) 0; } +.cardSkeleton { + max-width: 100%; + margin: -30px auto 0; + padding: 0 var(--Spacing-x2); +} + .section { display: flex; flex-direction: column; @@ -66,39 +30,12 @@ padding: 0 var(--Spacing-x2); } -.cardSkeleton { - max-width: 100%; - margin: -30px auto 0; - padding: 0 var(--Spacing-x2); -} - .ancillariesSkeleton { display: flex; flex-direction: column; gap: var(--Spacing-x2); } -.paymentDetailsSkeleton { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.hotelDetailsSkeleton { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.logIn { - padding: var(--Spacing-x9) var(--Spacing-x2); - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - align-items: center; - color: var(--Scandic-Grey-100); -} - @media (min-width: 768px) { .content { width: var(--max-width-content); diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts similarity index 94% rename from apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts rename to apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts index 97cd3bc9f..46d294ad5 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils/ancillaries.ts @@ -2,7 +2,7 @@ import type { Ancillary, SelectedAncillary, } from "@/types/components/myPages/myStay/ancillaries" -import type { AncillaryFormData } from "./AddAncillaryFlow/schema" +import type { AncillaryFormData } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema" export const generateDeliveryOptions = () => { const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"] diff --git a/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts index 1672a9939..185075a63 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils/mapRoomDetails.ts @@ -1,27 +1,29 @@ -import { BookingStatusEnum } from "@/constants/booking" +import { BookingStatusEnum, CancellationRuleEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { formatChildBedPreferences } from "../utils" import { convertToChildType } from "./convertToChildType" import { getPriceType } from "./getPriceType" -import { getBreakfastPackagesFromBookingFlow } from "./hasBreakfastPackage" import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { PackageTypeEnum } from "@/types/enums/packages" +import type { RateEnum } from "@/types/enums/rate" import type { Room } from "@/types/hotel" +import type { Room as MyStayRoom } from "@/types/stores/my-stay" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" -import type { Room as MyStayRoom } from "@/stores/my-stay/myStayRoomDetailsStore" interface MapRoomDetailsParams { booking: BookingConfirmation["booking"] + rates: Record room: (Room & { bedType: Room["roomTypes"][number] }) | null roomNumber: number } export function mapRoomDetails({ booking, + rates, room, roomNumber, }: MapRoomDetailsParams): MyStayRoom { @@ -29,45 +31,29 @@ export function mapRoomDetails({ .startOf("day") .diff(dt(booking.checkInDate).startOf("day"), "days") - const breakfastPackages = getBreakfastPackagesFromBookingFlow( - booking.packages - ) - const featuresPackages = booking.packages.filter( - (pkg) => - pkg.code === RoomPackageCodeEnum.PET_ROOM || - pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM || - pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM + const validBreakfastPackages: string[] = [ + BreakfastPackageEnum.REGULAR_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + ] + const breakfastPackage = booking.packages.find((pkg) => + validBreakfastPackages.includes(pkg.code) ) - const breakfast: BreakfastPackage | null = breakfastPackages?.length - ? { - code: BreakfastPackageEnum.REGULAR_BREAKFAST, - description: breakfastPackages[0].description, - localPrice: { - currency: breakfastPackages[0].currency, - price: breakfastPackages.reduce( - (acc, curr) => acc + curr.unitPrice, - 0 - ), - totalPrice: breakfastPackages.reduce( - (acc, curr) => acc + curr.totalPrice, - 0 - ), - }, - requestedPrice: { - currency: breakfastPackages[0].currency, - price: breakfastPackages.reduce( - (acc, curr) => acc + curr.unitPrice, - 0 - ), - totalPrice: breakfastPackages.reduce( - (acc, curr) => acc + curr.totalPrice, - 0 - ), - }, - packageType: PackageTypeEnum.BreakfastAdult, - } - : null + // We don't get `requestedPrice` in packages + const breakfast: Omit | null = + breakfastPackage + ? { + code: breakfastPackage.code, + description: breakfastPackage.description, + localPrice: { + currency: breakfastPackage.currency, + price: breakfastPackage.unitPrice, + totalPrice: breakfastPackage.totalPrice, + }, + packageType: PackageTypeEnum.BreakfastAdult, + } + : null const isCancelled = booking.reservationStatus === BookingStatusEnum.Cancelled @@ -87,55 +73,80 @@ export function mapRoomDetails({ booking.vouchers ) + let rate = "" + if (booking.rateDefinition.cancellationRule) { + switch (booking.rateDefinition.cancellationRule) { + case CancellationRuleEnum.CancellableBefore6PM: + rate = rates.flex + break + case CancellationRuleEnum.Changeable: + rate = rates.change + break + case CancellationRuleEnum.NonCancellable: + rate = rates.save + break + } + } + + const featuresPackages = booking.packages.filter( + (pkg) => + pkg.code === RoomPackageCodeEnum.PET_ROOM || + pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM || + pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM + ) + + const packages = featuresPackages.map((pkg) => ({ + code: pkg.code as RoomPackageCodeEnum, + description: pkg.description, + inventories: [], + itemCode: "", + localPrice: { + currency: pkg.currency, + price: pkg.unitPrice, + totalPrice: pkg.totalPrice, + }, + requestedPrice: { + currency: pkg.currency, + price: pkg.unitPrice, + totalPrice: pkg.totalPrice, + }, + })) + return { - hotelId: booking.hotelId, - roomTypeCode: booking.roomTypeCode, adults: booking.adults, - childrenAges: booking.childrenAges, - checkInDate: booking.checkInDate, - checkOutDate: booking.checkOutDate, - confirmationNumber: booking.confirmationNumber, - cancellationNumber: booking.cancellationNumber, - createDateTime: booking.createDateTime, - rateDefinition: booking.rateDefinition, - guaranteeInfo: booking.guaranteeInfo, - linkedReservations: booking.linkedReservations, - bookingCode: booking.bookingCode, - cheques: booking.cheques, - vouchers: booking.vouchers, - isCancelable: booking.isCancelable, - multiRoom: booking.multiRoom, - canChangeDate: booking.canChangeDate, - guest: booking.guest, - currencyCode: booking.currencyCode, - vatPercentage: booking.vatPercentage, - mainRoom: booking.mainRoom, - roomName: room?.name ?? "", - roomNumber, - isCancelled, - childrenInRoom, - childrenAsString, - terms: booking.rateDefinition.cancellationText, - packages: featuresPackages.map((pkg) => ({ - code: pkg.code as RoomPackageCodeEnum, - description: pkg.description, - inventories: [], - itemCode: "", - localPrice: { - currency: pkg.currency, - price: pkg.unitPrice, - totalPrice: pkg.totalPrice, - }, - requestedPrice: { - currency: pkg.currency, - price: pkg.unitPrice, - totalPrice: pkg.totalPrice, - }, - })), bedType: { description: room?.bedType.mainBed.description ?? "", roomTypeCode: room?.bedType.code ?? "", }, + bookingCode: booking.bookingCode, + breakfast, + canChangeDate: booking.canChangeDate, + cancellationNumber: booking.cancellationNumber, + checkInDate: booking.checkInDate, + checkOutDate: booking.checkOutDate, + cheques: booking.cheques, + childrenAges: booking.childrenAges, + childrenAsString, + childrenInRoom, + confirmationNumber: booking.confirmationNumber, + createDateTime: booking.createDateTime, + currencyCode: booking.currencyCode, + guaranteeInfo: booking.guaranteeInfo, + guest: booking.guest, + hotelId: booking.hotelId, + isCancelable: booking.isCancelable, + isCancelled, + linkedReservations: booking.linkedReservations, + mainRoom: booking.mainRoom, + multiRoom: booking.multiRoom, + packages, + priceType, + rate, + rateDefinition: booking.rateDefinition, + reservationStatus: booking.reservationStatus, + room, + roomName: room?.name ?? "", + roomNumber, roomPoints: booking.roomPoints, roomPrice: { perNight: { @@ -153,10 +164,13 @@ export function mapRoomDetails({ requested: undefined, }, }, - totalPriceExVat: booking.totalPriceExVat, + roomTypeCode: booking.roomTypeCode, + terms: booking.rateDefinition.cancellationText, + totalPoints: booking.totalPoints, totalPrice: booking.totalPrice, + totalPriceExVat: booking.totalPriceExVat, vatAmount: booking.vatAmount, - breakfast, - priceType, + vatPercentage: booking.vatPercentage, + vouchers: booking.vouchers, } } diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx b/apps/scandic-web/components/HotelReservation/PaymentOption/index.tsx similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx rename to apps/scandic-web/components/HotelReservation/PaymentOption/index.tsx diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css b/apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css rename to apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.module.css diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts b/apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.ts similarity index 100% rename from apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts rename to apps/scandic-web/components/HotelReservation/PaymentOption/paymentOption.ts diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx index 6bdc7498c..2f4cff893 100644 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/Breakfast.tsx @@ -12,7 +12,7 @@ import type { Child } from "@/types/components/hotelReservation/selectRate/selec interface BreakfastProps { adults: number - breakfast: BreakfastPackage | false | undefined | null + breakfast: Omit | false | undefined | null breakfastIncluded: boolean childrenInRoom: Child[] | undefined currency: string diff --git a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx index b801b14c1..b2dbf9d68 100644 --- a/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx +++ b/apps/scandic-web/components/HotelReservation/PriceDetailsModal/PriceDetailsTable/index.tsx @@ -42,7 +42,7 @@ type RoomPrice = export interface Room { adults: number bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | false | undefined | null + breakfast: Omit | false | undefined | null breakfastIncluded: boolean childrenInRoom: Child[] | undefined packages: Packages | null diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx index 654306d7c..f91b44ff8 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsHeader/RoomPackageFilter/Form/index.tsx @@ -19,7 +19,7 @@ import styles from "./form.module.css" import type { PackageEnum } from "@/types/requests/packages" import type { FormValues } from "./formValues" -export default function Form({ close }: { close: VoidFunction }) { +export default function Form({ close }: { close: () => void }) { const intl = useIntl() const lang = useLang() const utils = trpc.useUtils() diff --git a/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx b/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx index 5f39a877d..177abf728 100644 --- a/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SidePeek/index.tsx @@ -10,14 +10,21 @@ import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek" import useLang from "@/hooks/useLang" export default function HotelReservationSidePeek() { - const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek) - const hotelId = useSidePeekStore((state) => state.hotelId) - const confirmationNumber = useSidePeekStore( - (state) => state.confirmationNumber - ) - const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode) - const showCTA = useSidePeekStore((state) => state.showCTA) - const user = useSidePeekStore((state) => state.user) + const { + activeSidePeek, + confirmationNumber, + hotelId, + roomTypeCode, + showCTA, + user, + } = useSidePeekStore((state) => ({ + activeSidePeek: state.activeSidePeek, + confirmationNumber: state.confirmationNumber, + hotelId: state.hotelId, + roomTypeCode: state.roomTypeCode, + showCTA: state.showCTA, + user: state.user, + })) const close = useSidePeekStore((state) => state.closeSidePeek) const lang = useLang() diff --git a/apps/scandic-web/components/ImageGallery/index.tsx b/apps/scandic-web/components/ImageGallery/index.tsx index 5e0850743..93a0e3f0a 100644 --- a/apps/scandic-web/components/ImageGallery/index.tsx +++ b/apps/scandic-web/components/ImageGallery/index.tsx @@ -1,5 +1,6 @@ "use client" +import { cx } from "class-variance-authority" import { memo, useState } from "react" import { Button } from "react-aria-components" import { useIntl } from "react-intl" @@ -13,7 +14,6 @@ import Lightbox from "@/components/Lightbox" import styles from "./imageGallery.module.css" import type { ImageGalleryProps } from "@/types/components/imageGallery" -import { cx } from "class-variance-authority" function ImageGallery({ images, diff --git a/apps/scandic-web/components/Modal/modal.ts b/apps/scandic-web/components/Modal/modal.ts index f49c6102a..a86764a88 100644 --- a/apps/scandic-web/components/Modal/modal.ts +++ b/apps/scandic-web/components/Modal/modal.ts @@ -9,7 +9,7 @@ export enum AnimationStateEnum { export type AnimationState = keyof typeof AnimationStateEnum export type ModalProps = { - onAnimationComplete?: VoidFunction + onAnimationComplete?: () => void title?: string subtitle?: string withActions?: boolean diff --git a/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx b/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx index 1d29b4a01..30cebc3c6 100644 --- a/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx +++ b/apps/scandic-web/components/ParkingInformation/ParkingPrices/index.tsx @@ -52,6 +52,7 @@ export default function ParkingPrices({
{intl.formatMessage({ defaultMessage: "From" })}
+ {/* eslint-disable formatjs/no-literal-string-in-jsx */}
{`${parking.startTime}-${parking.endTime}`}
diff --git a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx index 4badbb548..b9a980ecd 100644 --- a/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx +++ b/apps/scandic-web/components/SidePeeks/BookedRoomSidePeek/index.tsx @@ -5,12 +5,11 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { Typography } from "@scandic-hotels/design-system/Typography" import { dt } from "@/lib/dt" -import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore" +import { useMyStayStore } from "@/stores/my-stay" import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails" -import Price from "@/components/HotelReservation/MyStay/Price" +import PriceType from "@/components/HotelReservation/MyStay/PriceType" import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils" -import { hasBreakfastPackageFromBookingFlow } from "@/components/HotelReservation/MyStay/utils/hasBreakfastPackage" import ImageGallery from "@/components/ImageGallery" import Accordion from "@/components/TempDesignSystem/Accordion" import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" @@ -18,6 +17,7 @@ import IconChip from "@/components/TempDesignSystem/IconChip" import SidePeek from "@/components/TempDesignSystem/SidePeek" import useLang from "@/hooks/useLang" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" +import { formatPrice } from "@/utils/numberFormatting" import RoomDetails from "./RoomDetails" @@ -36,40 +36,32 @@ export default function BookedRoomSidePeek({ }: BookedRoomSidePeekProps) { const intl = useIntl() const lang = useLang() - const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom) - const linkedReservationRooms = useMyStayRoomDetailsStore( - (state) => state.linkedReservationRooms - ) - const updateBookedRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateBookedRoom - ) - const updateLinkedReservationRoom = useMyStayRoomDetailsStore( - (state) => state.actions.updateLinkedReservationRoom - ) + const rooms = useMyStayStore((state) => state.rooms) - const allRooms = [bookedRoom, ...linkedReservationRooms] - - const matchingRoomBooking = allRooms.find( + const bookingRoom = rooms.find( (r) => r.confirmationNumber === confirmationNumber ) - if (!matchingRoomBooking) { + if (!bookingRoom) { return null } const { - roomNumber, - cancellationNumber, adults, - childrenInRoom, - terms, - packages, bedType, - checkInDate, bookingCode, - roomPrice, + breakfast, + cancellationNumber, + checkInDate, + childrenInRoom, + currencyCode, isCancelled, - } = matchingRoomBooking + packages, + rateDefinition, + roomNumber, + terms, + totalPrice, + } = bookingRoom const fromDate = dt(checkInDate).locale(lang) @@ -96,6 +88,24 @@ export default function BookedRoomSidePeek({ const adultsOnlyMsg = adultsMsg const adultsAndChildrenMsg = [adultsMsg, childrenMsg].join(", ") + + const formattedTotalPrice = formatPrice(intl, totalPrice, currencyCode) + + let breakfastPrice = intl.formatMessage({ + defaultMessage: "No breakfast", + }) + if (rateDefinition.breakfastIncluded) { + breakfastPrice = intl.formatMessage({ + defaultMessage: "Included", + }) + } else if (breakfast) { + breakfastPrice = formatPrice( + intl, + breakfast.localPrice.totalPrice, + breakfast.localPrice.currency + ) + } + return ( - {hasModifiableRate( - matchingRoomBooking.rateDefinition.cancellationRule - ) && ( + {hasModifiableRate(rateDefinition.cancellationRule) && (
@@ -256,20 +264,7 @@ export default function BookedRoomSidePeek({
-

- {packages && - hasBreakfastPackageFromBookingFlow( - packages.map((pkg) => ({ - code: pkg.code, - })) - ) - ? intl.formatMessage({ - defaultMessage: "Included", - }) - : intl.formatMessage({ - defaultMessage: "Not included", - })} -

+

{breakfastPrice}

@@ -338,9 +333,15 @@ export default function BookedRoomSidePeek({

- @@ -368,16 +369,7 @@ export default function BookedRoomSidePeek({ )} - + >(function Checkbox( - { className, name, children, registerOptions, hideError, topAlign = false }, + { + className = "", + name, + children, + registerOptions, + hideError, + topAlign = false, + }, ref ) { const { control } = useFormContext() diff --git a/apps/scandic-web/contexts/MyStay.ts b/apps/scandic-web/contexts/MyStay.ts new file mode 100644 index 000000000..c5f0c1bf4 --- /dev/null +++ b/apps/scandic-web/contexts/MyStay.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { MyStayStore } from "@/types/contexts/my-stay" + +export const MyStayContext = createContext(null) diff --git a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts index 4c9b83303..7e77da181 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteeBooking.ts @@ -12,15 +12,10 @@ import { trackEvent } from "@/utils/tracking/base" const maxRetries = 15 const retryInterval = 2000 -export function useGuaranteeBooking({ - confirmationNumber, - handleBookingCompleted = () => {}, - isAncillaryFlow, -}: { - confirmationNumber: string - handleBookingCompleted?: () => void - isAncillaryFlow?: boolean -}) { +export function useGuaranteeBooking( + confirmationNumber: string, + isAncillaryFlow = false +) { const intl = useIntl() const router = useRouter() const [isPollingForBookingStatus, setIsPollingForBookingStatus] = @@ -51,15 +46,13 @@ export function useGuaranteeBooking({ const utils = trpc.useUtils() const guaranteeBooking = trpc.booking.guarantee.useMutation({ - onSuccess: (result, variables) => { + onSuccess: (result) => { if (result) { if (result.reservationStatus == BookingStatusEnum.BookingCompleted) { - handleBookingCompleted() + utils.booking.get.invalidate({ confirmationNumber }) } else { setIsPollingForBookingStatus(true) - utils.booking.status.invalidate({ - confirmationNumber: variables.confirmationNumber, - }) + utils.booking.status.invalidate({ confirmationNumber }) } } else { handleGuaranteeError() @@ -81,6 +74,7 @@ export function useGuaranteeBooking({ useEffect(() => { if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) { router.push(bookingStatus.data.paymentUrl) + utils.booking.get.invalidate({ confirmationNumber }) setIsPollingForBookingStatus(false) } else if (bookingStatus.isTimeout) { handleGuaranteeError("Timeout") @@ -91,6 +85,8 @@ export function useGuaranteeBooking({ handleGuaranteeError, setIsPollingForBookingStatus, isPollingForBookingStatus, + confirmationNumber, + utils.booking.get, ]) const isLoading = diff --git a/apps/scandic-web/lib/dt.ts b/apps/scandic-web/lib/dt.ts index e52f7cf57..5f8a90eac 100644 --- a/apps/scandic-web/lib/dt.ts +++ b/apps/scandic-web/lib/dt.ts @@ -6,6 +6,7 @@ import "dayjs/locale/sv" import d from "dayjs" import nb from "dayjs/locale/nb" import advancedFormat from "dayjs/plugin/advancedFormat" +import customParseFormat from "dayjs/plugin/customParseFormat" import duration from "dayjs/plugin/duration" import isSameOrAfter from "dayjs/plugin/isSameOrAfter" import isSameOrBefore from "dayjs/plugin/isSameOrBefore" @@ -33,5 +34,6 @@ d.extend(utc) d.extend(isSameOrAfter) d.extend(isSameOrBefore) d.extend(duration) +d.extend(customParseFormat) export const dt = d diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts index 72f0f8723..9dd7eb0ba 100644 --- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts +++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts @@ -17,6 +17,7 @@ import type { HotelInput, } from "@/types/trpc/routers/hotel/hotel" import type { Lang } from "@/constants/languages" +import type { LinkedReservationsInput } from "@/server/routers/booking/input" import type { GetHotelsByCSFilterInput } from "@/server/routers/hotels/input" import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" @@ -140,6 +141,12 @@ export const getBookingConfirmation = cache( } ) +export const getLinkedReservations = cache( + async function getMemoizedLinkedReservations(input: LinkedReservationsInput) { + return serverClient().booking.linkedReservations(input) + } +) + export const getCityCoordinates = cache( async function getMemoizedCityCoordinates(input: CityCoordinatesInput) { return serverClient().hotel.map.city(input) diff --git a/apps/scandic-web/providers/AddAncillaryProvider.tsx b/apps/scandic-web/providers/AddAncillaryProvider.tsx index c8590767d..58645ea46 100644 --- a/apps/scandic-web/providers/AddAncillaryProvider.tsx +++ b/apps/scandic-web/providers/AddAncillaryProvider.tsx @@ -6,7 +6,7 @@ import { createAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" -import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/Ancillaries/utils" +import { getAncillarySessionData } from "@/components/HotelReservation/MyStay/utils/ancillaries" import { AddAncillaryContext } from "@/contexts/AddAncillary" import type { Ancillaries } from "@/types/components/myPages/myStay/ancillaries" diff --git a/apps/scandic-web/providers/MyStay.tsx b/apps/scandic-web/providers/MyStay.tsx new file mode 100644 index 000000000..6a3fa6e2c --- /dev/null +++ b/apps/scandic-web/providers/MyStay.tsx @@ -0,0 +1,110 @@ +"use client" +import { notFound } from "next/navigation" +import { use, useRef } from "react" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" +import { createMyStayStore } from "@/stores/my-stay" + +import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton" +import { MyStayContext } from "@/contexts/MyStay" + +import type { Packages } from "@/types/components/myPages/myStay/ancillaries" +import type { MyStayStore } from "@/types/contexts/my-stay" +import type { RoomCategories } from "@/types/hotel" +import type { + BookingConfirmation, + BookingConfirmationSchema, +} from "@/types/trpc/routers/booking/confirmation" +import type { CreditCard } from "@/types/user" +import type { Lang } from "@/constants/languages" + +interface MyStayProviderProps { + bookingConfirmation: BookingConfirmation + breakfastPackages: Packages | null + lang: Lang + linkedReservationsPromise: Promise + refId: string + roomCategories: RoomCategories + savedCreditCards: CreditCard[] | null +} + +export default function MyStayProvider({ + bookingConfirmation, + breakfastPackages, + children, + lang, + linkedReservationsPromise, + refId, + roomCategories, + savedCreditCards, +}: React.PropsWithChildren) { + const storeRef = useRef() + const intl = useIntl() + + const { data, error, isFetching, isFetchedAfterMount } = + trpc.booking.get.useQuery( + { + confirmationNumber: bookingConfirmation.booking.confirmationNumber, + lang, + }, + { + initialData: bookingConfirmation, + refetchOnMount: false, + refetchOnWindowFocus: false, + } + ) + + // We need this two-step business since `use` must resolve + // the promise passed from the server whereas `useQuery` + // needs to own the data when in the client so that invalidations + // actually triggers a refetch of the data + const linkedReservationsResponses = use(linkedReservationsPromise) + const { + data: linkedReservations, + error: linkedReservationsError, + isFetching: linkedReservationsIsFetching, + isFetchedAfterMount: linkedReservationsIsFetchedAfterMount, + } = trpc.booking.linkedReservations.useQuery( + { + lang, + rooms: bookingConfirmation.booking.linkedReservations, + }, + { + initialData: linkedReservationsResponses, + refetchOnMount: false, + refetchOnWindowFocus: false, + } + ) + + if (isFetching || linkedReservationsIsFetching) { + return + } + + if (!data || error || linkedReservationsError) { + return notFound() + } + + const rooms = [data.booking, ...linkedReservations] + + const hasInvalidatedQueryAndRefetched = + (isFetchedAfterMount && data) || + (linkedReservationsIsFetchedAfterMount && linkedReservations) + if (!storeRef.current || hasInvalidatedQueryAndRefetched) { + storeRef.current = createMyStayStore({ + breakfastPackages, + hotel: bookingConfirmation.hotel, + intl, + refId, + roomCategories, + rooms, + savedCreditCards, + }) + } + + return ( + + {children} + + ) +} diff --git a/apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 b/apps/scandic-web/public/_static/fonts/material-symbols/rounded-112272ae.woff2 deleted file mode 100644 index 7a133cedc3569e23abcb4d049a6ee45f55d470b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34088 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 literal 0 HcmV?d00001 diff --git a/apps/scandic-web/server/routers/booking/input.ts b/apps/scandic-web/server/routers/booking/input.ts index 6b374a91d..8a87965a1 100644 --- a/apps/scandic-web/server/routers/booking/input.ts +++ b/apps/scandic-web/server/routers/booking/input.ts @@ -131,6 +131,11 @@ export const cancelBookingInput = z.object({ language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), }) +export const cancelManyBookingsInput = z.object({ + confirmationNumbers: z.array(z.string()), + language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), +}) + export const guaranteeBookingInput = z.object({ confirmationNumber: z.string(), card: z @@ -175,5 +180,15 @@ const confirmationNumberInput = z.object({ }) export const getBookingInput = confirmationNumberInput +export const getLinkedReservationsInput = z.object({ + lang: z.nativeEnum(Lang).optional(), + rooms: z.array( + z.object({ + confirmationNumber: z.string(), + }) + ), +}) + +export type LinkedReservationsInput = z.input export const getBookingStatusInput = confirmationNumberInput diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 4b50a87e9..f0d15af5a 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -6,6 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc" import { addPackageInput, cancelBookingInput, + cancelManyBookingsInput, createBookingInput, guaranteeBookingInput, priceChangeInput, @@ -13,6 +14,7 @@ import { updateBookingInput, } from "./input" import { bookingConfirmationSchema, createBookingSchema } from "./output" +import { cancelBooking } from "./utils" export const bookingMutationRouter = router({ create: safeProtectedServiceProcedure @@ -113,52 +115,40 @@ export const bookingMutationRouter = router({ cancel: safeProtectedServiceProcedure .input(cancelBookingInput) .mutation(async function ({ ctx, input }) { - const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken + const token = ctx.session?.token.access_token ?? ctx.serviceToken const { confirmationNumber, language } = input + return await cancelBooking(confirmationNumber, language, token) + }), + cancelMany: safeProtectedServiceProcedure + .input(cancelManyBookingsInput) + .mutation(async function ({ ctx, input }) { + const token = ctx.session?.token.access_token ?? ctx.serviceToken + const { confirmationNumbers, language } = input - const cancelBookingCounter = createCounter("trpc.booking", "cancel") - const metricsCancelBooking = cancelBookingCounter.init({ - confirmationNumber, - language, - }) - - metricsCancelBooking.start() - - const headers = { - Authorization: `Bearer ${accessToken}`, - } - - const cancellationReason = { - reasonCode: "WEB-CANCEL", - reason: "WEB-CANCEL", - } - - const apiResponse = await api.remove( - api.endpoints.v1.Booking.cancel(confirmationNumber), - { - headers, - body: JSON.stringify(cancellationReason), - } as RequestInit, - { language } + const responses = await Promise.allSettled( + confirmationNumbers.map((confirmationNumber) => + cancelBooking(confirmationNumber, language, token) + ) ) - if (!apiResponse.ok) { - await metricsCancelBooking.httpError(apiResponse) - return false + const cancelledRoomsSuccessfully = [] + for (const [idx, response] of responses.entries()) { + if (response.status === "fulfilled") { + if (response.value) { + cancelledRoomsSuccessfully.push(true) + continue + } + } else { + console.info( + `Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}` + ) + console.error(response.reason) + } + + cancelledRoomsSuccessfully.push(false) } - const apiJson = await apiResponse.json() - - const verifiedData = createBookingSchema.safeParse(apiJson) - - if (!verifiedData.success) { - metricsCancelBooking.validationError(verifiedData.error) - return null - } - - metricsCancelBooking.success() - - return verifiedData.data + return cancelledRoomsSuccessfully }), packages: safeProtectedServiceProcedure .input(addPackageInput) diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index d648e34cc..072b2681b 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -201,7 +201,7 @@ export const bookingConfirmationSchema = z attributes: z.object({ adults: z.number().int(), ancillary: ancillarySchema, - cancellationNumber: z.string().nullable().default(""), + cancelationNumber: z.string().nullable().default(""), checkInDate: z.date({ coerce: true }), checkOutDate: z.date({ coerce: true }), childBedPreferences: z.array(childBedPreferencesSchema).default([]), @@ -263,4 +263,6 @@ export const bookingConfirmationSchema = z isCancelable: !!data.links.cancel, isModifiable: !!data.links.modify, canModifyAncillaries: !!data.links.addAncillary, + // Typo from API + cancellationNumber: data.attributes.cancelationNumber, })) diff --git a/apps/scandic-web/server/routers/booking/query.ts b/apps/scandic-web/server/routers/booking/query.ts index e39913dc0..280a49769 100644 --- a/apps/scandic-web/server/routers/booking/query.ts +++ b/apps/scandic-web/server/routers/booking/query.ts @@ -13,68 +13,54 @@ import { createRefIdInput, getBookingInput, getBookingStatusInput, + getLinkedReservationsInput, } from "./input" -import { bookingConfirmationSchema, createBookingSchema } from "./output" -import { getBookedHotelRoom } from "./utils" +import { createBookingSchema } from "./output" +import { getBookedHotelRoom, getBooking } from "./utils" export const bookingQueryRouter = router({ get: safeProtectedServiceProcedure .input(getBookingInput) - .query(async function ({ - ctx, - input: { confirmationNumber, lang: inputLang }, - }) { + .use(async ({ ctx, input, next }) => { + const lang = input.lang ?? ctx.lang + const token = ctx.session?.token.access_token ?? ctx.serviceToken + return next({ + ctx: { + lang, + token, + }, + }) + }) + .query(async function ({ ctx, input: { confirmationNumber } }) { const getBookingCounter = createCounter("trpc.booking", "get") const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) metricsGetBooking.start() - let lang = ctx.lang ?? inputLang + const booking = await getBooking(confirmationNumber, ctx.lang, ctx.token) - const token = ctx.session?.token.access_token ?? ctx.serviceToken - - const apiResponse = await api.get( - api.endpoints.v1.Booking.booking(confirmationNumber), - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ) - - if (!apiResponse.ok) { - await metricsGetBooking.httpError(apiResponse) - - // If the booking is not found, return null. - // This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them. - if (apiResponse.status === 400) { - return null - } - - throw serverErrorByStatus(apiResponse.status, apiResponse) - } - - const apiJson = await apiResponse.json() - const booking = bookingConfirmationSchema.safeParse(apiJson) - if (!booking.success) { - metricsGetBooking.validationError(booking.error) - throw badRequestError() + if (!booking) { + metricsGetBooking.dataError( + `Fail to get booking data for ${confirmationNumber}`, + { confirmationNumber } + ) + return null } const hotelData = await getHotel( { - hotelId: booking.data.hotelId, + hotelId: booking.hotelId, isCardOnlyPayment: false, - language: lang, + language: ctx.lang, }, ctx.serviceToken ) if (!hotelData) { metricsGetBooking.dataError( - `Failed to get hotel data for ${booking.data.hotelId}`, + `Failed to get hotel data for ${booking.hotelId}`, { - hotelId: booking.data.hotelId, + hotelId: booking.hotelId, } ) @@ -85,13 +71,62 @@ export const bookingQueryRouter = router({ return { ...hotelData, - booking: booking.data, + booking, room: getBookedHotelRoom( hotelData.roomCategories, - booking.data.roomTypeCode + booking.roomTypeCode ), } }), + linkedReservations: safeProtectedServiceProcedure + .input(getLinkedReservationsInput) + .use(async ({ ctx, input, next }) => { + const lang = input.lang ?? ctx.lang + const token = ctx.session?.token.access_token ?? ctx.serviceToken + return next({ + ctx: { + lang, + token, + }, + }) + }) + .query(async function ({ ctx, input: { rooms } }) { + const getLinkedReservationsCounter = createCounter( + "trpc.booking", + "linkedReservations" + ) + const metricsGetLinkedReservations = getLinkedReservationsCounter.init({ + confirmationNumbers: rooms, + }) + + metricsGetLinkedReservations.start() + + const linkedReservationsResult = await Promise.allSettled( + rooms.map((room) => + getBooking(room.confirmationNumber, ctx.lang, ctx.token) + ) + ) + const linkedReservations = [] + for (const booking of linkedReservationsResult) { + if (booking.status === "fulfilled") { + if (booking.value) { + linkedReservations.push(booking.value) + } else { + metricsGetLinkedReservations.dataError( + `Unexpected value for linked reservation` + ) + } + } else { + metricsGetLinkedReservations.dataError( + `Failed to get linked reservation` + ) + } + } + + metricsGetLinkedReservations.success() + + return linkedReservations + }), status: serviceProcedure.input(getBookingStatusInput).query(async function ({ ctx, input, diff --git a/apps/scandic-web/server/routers/booking/utils.ts b/apps/scandic-web/server/routers/booking/utils.ts index 3068453ae..5149a618c 100644 --- a/apps/scandic-web/server/routers/booking/utils.ts +++ b/apps/scandic-web/server/routers/booking/utils.ts @@ -1,5 +1,13 @@ +import * as api from "@/lib/api" +import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" +import { createCounter } from "@/server/telemetry" +import { toApiLang } from "@/server/utils" + +import { bookingConfirmationSchema, createBookingSchema } from "./output" + import type { Room } from "@/types/hotel" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { Lang } from "@/constants/languages" export function getBookedHotelRoom( rooms: Room[] | undefined, @@ -25,3 +33,96 @@ export function getBookedHotelRoom( bedType, } } + +export async function getBooking( + confirmationNumber: string, + lang: Lang, + token: string +) { + const getBookingCounter = createCounter("booking", "get") + const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) + + metricsGetBooking.start() + + const apiResponse = await api.get( + api.endpoints.v1.Booking.booking(confirmationNumber), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + { language: toApiLang(lang) } + ) + + if (!apiResponse.ok) { + await metricsGetBooking.httpError(apiResponse) + + // If the booking is not found, return null. + // This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them. + if (apiResponse.status === 400) { + return null + } + + throw serverErrorByStatus(apiResponse.status, apiResponse) + } + + const apiJson = await apiResponse.json() + const booking = bookingConfirmationSchema.safeParse(apiJson) + if (!booking.success) { + metricsGetBooking.validationError(booking.error) + throw badRequestError() + } + + metricsGetBooking.success() + + return booking.data +} + +export async function cancelBooking( + confirmationNumber: string, + language: string, + token: string +) { + const cancellationReason = { + reasonCode: "WEB-CANCEL", + reason: "WEB-CANCEL", + } + + const cancelBookingCounter = createCounter("booking", "cancel") + const metricsCancelBooking = cancelBookingCounter.init({ + cancellationReason, + confirmationNumber, + language, + }) + + metricsCancelBooking.start() + + const headers = { + Authorization: `Bearer ${token}`, + } + + const apiResponse = await api.remove( + api.endpoints.v1.Booking.cancel(confirmationNumber), + { + headers, + body: JSON.stringify(cancellationReason), + } as RequestInit, + { language } + ) + + if (!apiResponse.ok) { + await metricsCancelBooking.httpError(apiResponse) + return false + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + metricsCancelBooking.validationError(verifiedData.error) + return null + } + + metricsCancelBooking.success() + + return verifiedData.data +} diff --git a/apps/scandic-web/server/routers/hotels/utils.ts b/apps/scandic-web/server/routers/hotels/utils.ts index 0a22c90bc..5b1c16354 100644 --- a/apps/scandic-web/server/routers/hotels/utils.ts +++ b/apps/scandic-web/server/routers/hotels/utils.ts @@ -1031,6 +1031,7 @@ export async function getRoomsAvailability( const apiResponse = await api.get( api.endpoints.v1.Availability.hotel(hotelId), { + cache: undefined, // overwrite default headers: { Authorization: `Bearer ${token}`, }, diff --git a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts index bed6f315e..a2724988f 100644 --- a/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts +++ b/apps/scandic-web/stores/my-stay/add-ancillary-flow.ts @@ -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( diff --git a/apps/scandic-web/stores/my-stay/helpers.ts b/apps/scandic-web/stores/my-stay/helpers.ts new file mode 100644 index 000000000..4f3edb746 --- /dev/null +++ b/apps/scandic-web/stores/my-stay/helpers.ts @@ -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) +} diff --git a/apps/scandic-web/stores/my-stay/index.ts b/apps/scandic-web/stores/my-stay/index.ts new file mode 100644 index 000000000..fd6544ebc --- /dev/null +++ b/apps/scandic-web/stores/my-stay/index.ts @@ -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()((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(selector: (store: MyStayState) => T) { + const store = useContext(MyStayContext) + + if (!store) { + throw new Error("useMyStayStore must be used within MyStayProvider") + } + + return useStore(store, selector) +} diff --git a/apps/scandic-web/stores/my-stay/manageStayStore.ts b/apps/scandic-web/stores/my-stay/manageStayStore.ts deleted file mode 100644 index 8430a248f..000000000 --- a/apps/scandic-web/stores/my-stay/manageStayStore.ts +++ /dev/null @@ -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((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", - }), - }, -})) diff --git a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts deleted file mode 100644 index 221608f7f..000000000 --- a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts +++ /dev/null @@ -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( - (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, - } - }) - }, - }, - }) -) diff --git a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts deleted file mode 100644 index 533018a1b..000000000 --- a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts +++ /dev/null @@ -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( - (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, - } - }) - }, - }, - }) -) diff --git a/apps/scandic-web/stores/sidepeek.ts b/apps/scandic-web/stores/sidepeek.ts index 777e82544..85736ed48 100644 --- a/apps/scandic-web/stores/sidepeek.ts +++ b/apps/scandic-web/stores/sidepeek.ts @@ -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 diff --git a/apps/scandic-web/types/components/blocks/surprises.ts b/apps/scandic-web/types/components/blocks/surprises.ts index 5bb9e2459..397cb5e51 100644 --- a/apps/scandic-web/types/components/blocks/surprises.ts +++ b/apps/scandic-web/types/components/blocks/surprises.ts @@ -17,7 +17,7 @@ export interface CardProps extends React.PropsWithChildren { export interface InitialProps { totalSurprises: number - onOpen: VoidFunction + onOpen: () => void } export interface SlideProps { @@ -25,5 +25,5 @@ export interface SlideProps { } export interface HeaderProps extends React.PropsWithChildren { - onClose: VoidFunction + onClose: () => void } diff --git a/apps/scandic-web/types/components/datepicker.ts b/apps/scandic-web/types/components/datepicker.ts index 60d5f03a7..f75770e07 100644 --- a/apps/scandic-web/types/components/datepicker.ts +++ b/apps/scandic-web/types/components/datepicker.ts @@ -1,17 +1,11 @@ -import type { Locale } from "date-fns" import type { DateRange } from "react-day-picker" -import type { Lang } from "@/constants/languages" - export interface DatePickerFormProps { name?: string } -type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv - interface DatePickerProps { close: () => void - locales: Record startMonth?: Date hideHeader?: boolean } diff --git a/apps/scandic-web/types/components/header/headerLink.ts b/apps/scandic-web/types/components/header/headerLink.ts index 0da0fe649..4370d048d 100644 --- a/apps/scandic-web/types/components/header/headerLink.ts +++ b/apps/scandic-web/types/components/header/headerLink.ts @@ -6,5 +6,5 @@ export interface HeaderLinkProps extends React.PropsWithChildren { href: LinkProps["href"] iconName: IconName | null iconSize?: number - onClick?: VoidFunction + onClick?: () => void } diff --git a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts index 40cf94a7f..609fe53d2 100644 --- a/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts +++ b/apps/scandic-web/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -12,7 +12,8 @@ export interface BookingConfirmationRoom extends Room { bedType: Room["roomTypes"][number] } -export interface ConfirmationProps extends BookingConfirmation { +export interface ConfirmationProps + extends Pick { room: BookingConfirmationRoom refId: string } diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts index 20faabb33..d65e7d00b 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/cancelStay.ts @@ -1,23 +1,22 @@ import { z } from "zod" import type { Hotel } from "@/types/hotel" -import type { Room } from "@/stores/my-stay/myStayRoomDetailsStore" export const cancelStaySchema = z.object({ rooms: z.array( z.object({ - checked: z.boolean().optional(), + checked: z.boolean(), confirmationNumber: z.string(), }) ), }) export interface CancelStayProps { - hotel: Hotel handleCloseModal: () => void + hotel: Hotel } -export type CancelStayFormValues = z.infer +export type CancelStayFormValues = z.output export interface RoomDetails { id: string @@ -43,17 +42,3 @@ export interface StayDetails { adultsText: string childrenText: string } - -export interface CancelStayConfirmationProps { - hotel: Hotel - stayDetails: StayDetails -} - -export interface FinalConfirmationProps { - stayDetails: StayDetails -} - -export interface PriceContainerProps { - roomDetails: Room - stayDetails: StayDetails -} diff --git a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts b/apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts similarity index 62% rename from apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts rename to apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts index 92490382e..ad4c3df84 100644 --- a/apps/scandic-web/types/components/hotelReservation/myStay/modifyDate.ts +++ b/apps/scandic-web/types/components/hotelReservation/myStay/changeDates.ts @@ -2,12 +2,12 @@ import { z } from "zod" import { Lang } from "@/constants/languages" -export const modifyDateSchema = z.object({ +export const changeDatesSchema = z.object({ checkInDate: z.string(), checkOutDate: z.string(), }) -export type ModifyDateSchema = z.infer +export type ChangeDatesSchema = z.output export interface QueryInput { hotelId: string @@ -33,6 +33,12 @@ export const DEFAULT_QUERY_INPUT: QueryInput = { lang: Lang.en, } -export interface ModifyStayProps { - isLoggedIn: boolean +export interface ChangeDatesStepsProps { + closeModal: () => void +} + +export interface ChangeDatesFormProps { + checkAvailability: (fromDate: string, toDate: string) => Promise + closeModal: () => void + noAvailability: boolean } diff --git a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts index 42734b8bc..3e63b9e0e 100644 --- a/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/toggleSidePeekProps.ts @@ -1,10 +1,10 @@ -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" export type ToggleSidePeekProps = { hotelId: string roomTypeCode?: string intent?: "text" | "textInverted" title?: string - user?: User + user?: SafeUser confirmationNumber?: string } diff --git a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts index d47879ad0..6e372a5bd 100644 --- a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts +++ b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts @@ -70,6 +70,6 @@ export interface StepsProps { export interface ActionButtonsProps { isPriceDetailsOpen: boolean - togglePriceDetails: VoidFunction + togglePriceDetails: () => void isSubmitting: boolean } diff --git a/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts index dbdefadce..69e0d785b 100644 --- a/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts +++ b/apps/scandic-web/types/components/sidePeeks/bookedRoomSidePeek.ts @@ -1,12 +1,12 @@ import type { Room } from "@/types/hotel" -import type { User } from "@/types/user" +import type { SafeUser } from "@/types/user" import type { SidePeekEnum } from "../hotelReservation/sidePeek" export type BookedRoomSidePeekProps = { room: Room activeSidePeek: SidePeekEnum | null close: () => void - user: User | null + user: SafeUser confirmationNumber: string } diff --git a/apps/scandic-web/types/contexts/my-stay.ts b/apps/scandic-web/types/contexts/my-stay.ts new file mode 100644 index 000000000..18ed9499c --- /dev/null +++ b/apps/scandic-web/types/contexts/my-stay.ts @@ -0,0 +1,3 @@ +import type { createMyStayStore } from "@/stores/my-stay" + +export type MyStayStore = ReturnType diff --git a/apps/scandic-web/types/hotel.ts b/apps/scandic-web/types/hotel.ts index 7e2d0cd3c..b499f6efa 100644 --- a/apps/scandic-web/types/hotel.ts +++ b/apps/scandic-web/types/hotel.ts @@ -62,6 +62,8 @@ export type RestaurantOpeningHoursDay = z.output< typeof openingHoursDetailsSchema > export type Room = ReturnType +export type RoomCategory = Room +export type RoomCategories = RoomCategory[] export type PoiMapMarkersProps = { activePoi?: string | null diff --git a/apps/scandic-web/types/stores/my-stay.ts b/apps/scandic-web/types/stores/my-stay.ts new file mode 100644 index 000000000..a9492ec2b --- /dev/null +++ b/apps/scandic-web/types/stores/my-stay.ts @@ -0,0 +1,89 @@ +import type { IntlShape } from "react-intl" + +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 type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Packages } from "@/types/components/myPages/myStay/ancillaries" +import type { Hotel, Room as HotelRoom, RoomCategories } from "@/types/hotel" +import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" +import type { CreditCard } from "@/types/user" + +export type Room = Pick< + BookingConfirmation["booking"], + | "adults" + | "bookingCode" + | "canChangeDate" + | "cancellationNumber" + | "checkInDate" + | "checkOutDate" + | "cheques" + | "childrenAges" + | "confirmationNumber" + | "createDateTime" + | "currencyCode" + | "guaranteeInfo" + | "guest" + | "hotelId" + | "isCancelable" + | "linkedReservations" + | "multiRoom" + | "rateDefinition" + | "reservationStatus" + | "roomPoints" + | "roomTypeCode" + | "totalPoints" + | "totalPrice" + | "totalPriceExVat" + | "vatAmount" + | "vatPercentage" + | "vouchers" +> & { + bedType: BedTypeSchema + breakfast: Omit | null + childrenAsString: string + childrenInRoom: Child[] + isCancelled: boolean + mainRoom: boolean + packages: Packages | null + priceType: PriceTypeEnum + rate: string + room: (HotelRoom & { bedType: HotelRoom["roomTypes"][number] }) | null + roomName: string + roomNumber: number + roomPrice: RoomPrice + terms: string | null +} + +export type BookingRoom = BookingConfirmation["booking"] + +interface Actions { + closeManageStay: () => void + openManageStay: () => void +} + +export interface MyStayState { + actions: Actions + allRoomsAreCancelled: boolean + bookedRoom: Room + breakfastPackages: Packages | null + hotel: Hotel + mainRoom: Room + manageStay: boolean + refId: string + rooms: Room[] + savedCreditCards: CreditCard[] | null + totalPoints: number + totalPrice: string +} + +export interface InitialState + extends Pick< + MyStayState, + "breakfastPackages" | "hotel" | "refId" | "savedCreditCards" + > { + intl: IntlShape + roomCategories: RoomCategories + rooms: BookingRoom[] +} diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index c7baeac12..182ef4683 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -22,10 +22,10 @@ interface Actions { appendRegularRates: ( roomConfigurations: RoomConfiguration[] | undefined ) => void - closeSection: VoidFunction - modifyRate: VoidFunction + closeSection: () => void + modifyRate: () => void removeSelectedPackage: (code: PackageEnum) => void - removeSelectedPackages: VoidFunction + removeSelectedPackages: () => void selectFilter: (filter: BookingCodeFilterEnum) => void selectPackages: (codes: PackageEnum[]) => void selectRate: (rate: SelectedRate, isUserLoggedIn: boolean) => void diff --git a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts index 1da9d2387..4b6e6f5eb 100644 --- a/apps/scandic-web/types/trpc/routers/booking/confirmation.ts +++ b/apps/scandic-web/types/trpc/routers/booking/confirmation.ts @@ -1,6 +1,6 @@ import type { z } from "zod" -import type { Hotel, Room } from "@/types/hotel" +import type { HotelData, Room } from "@/types/hotel" import type { bookingConfirmationSchema, packageSchema, @@ -11,9 +11,8 @@ export interface BookingConfirmationSchema export interface PackageSchema extends z.output {} -export interface BookingConfirmation { +export interface BookingConfirmation extends HotelData { booking: BookingConfirmationSchema - hotel: Hotel room: | (Room & { bedType: Room["roomTypes"][number] diff --git a/packages/design-system/lib/components/RateCard/Modal/modal.ts b/packages/design-system/lib/components/RateCard/Modal/modal.ts index 1a0fcca6b..5fdb306d4 100644 --- a/packages/design-system/lib/components/RateCard/Modal/modal.ts +++ b/packages/design-system/lib/components/RateCard/Modal/modal.ts @@ -9,7 +9,7 @@ export enum AnimationStateEnum { export type AnimationState = keyof typeof AnimationStateEnum export type ModalProps = { - onAnimationComplete?: VoidFunction + onAnimationComplete?: () => void title?: string subtitle?: string withActions?: boolean diff --git a/packages/design-system/lib/fonts.css b/packages/design-system/lib/fonts.css index dcfcdf893..036360d2d 100644 --- a/packages/design-system/lib/fonts.css +++ b/packages/design-system/lib/fonts.css @@ -269,7 +269,7 @@ font-style: normal; font-weight: 400; font-display: block; - src: url(/_static/fonts/material-symbols/rounded-112272ae.woff2) + src: url(/_static/fonts/material-symbols/rounded-a03ed056.woff2) format('woff2'); } diff --git a/packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 b/packages/design-system/public/_static/fonts/material-symbols/rounded-112272ae.woff2 deleted file mode 100644 index 7a133cedc3569e23abcb4d049a6ee45f55d470b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34088 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 literal 0 HcmV?d00001 diff --git a/scripts/material-symbols-update.mjs b/scripts/material-symbols-update.mjs index 1bfb80633..22839bf0e 100644 --- a/scripts/material-symbols-update.mjs +++ b/scripts/material-symbols-update.mjs @@ -90,6 +90,7 @@ const icons = [ 'download', 'dresser', 'edit', + 'edit_calendar', 'edit_square', 'electric_bike', 'electric_car', @@ -163,6 +164,7 @@ const icons = [ 'pets', 'phone', 'pool', + 'print', 'radio', 'recommend', 'redeem', @@ -191,6 +193,7 @@ const icons = [ 'star', 'straighten', 'styler', + 'support_agent', 'swipe', 'sync_saved_locally', 'table_bar',