forked from Somnia/SomniaRoomsApp
Booking process - Add and remove rooms from booking.
This commit is contained in:
@@ -10,18 +10,37 @@ import { DateRange } from "react-day-picker";
|
|||||||
import { FareFilter } from "@/model/RoomSearch";
|
import { FareFilter } from "@/model/RoomSearch";
|
||||||
import { useAPIRoomPrices } from "@/api/api-roomSearch";
|
import { useAPIRoomPrices } from "@/api/api-roomSearch";
|
||||||
import { NumberSelector } from "../ui/number-selector";
|
import { NumberSelector } from "../ui/number-selector";
|
||||||
|
import { NewBookingRoom } from "@/model/Bookings";
|
||||||
|
|
||||||
function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: number, availableAmount: number }) {
|
function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: number, availableAmount: number }) {
|
||||||
|
|
||||||
const [selectedAmount, setSelectedAmount] = useState<number>(0);
|
const [selectedAmount, setSelectedAmount] = useState<Map<number, number>>(new Map());
|
||||||
|
|
||||||
const currentDates: DateRange = useAppStore((state) => (state.searchFilter.dateRange));
|
const currentDates: DateRange = useAppStore((state) => (state.searchFilter.dateRange));
|
||||||
|
const addNewRoom = useAppStore((state) => (state.addNewBookingRoom));
|
||||||
|
|
||||||
function onSelectedAmountUpdate(guestNumber: number, newCount: number) {
|
function onSelectedAmountUpdate(guestNumber: number, newCount: number, price: number) {
|
||||||
console.log("Room " + roomCategoryId + " Guestnumber: " + guestNumber + " Count: " + newCount);
|
console.log("Room " + roomCategoryId + " Guestnumber: " + guestNumber + " Count: " + newCount);
|
||||||
setSelectedAmount(newCount);
|
|
||||||
|
const newAmountMap = new Map(selectedAmount);
|
||||||
|
newAmountMap.set(guestNumber, newCount);
|
||||||
|
setSelectedAmount(newAmountMap)
|
||||||
|
|
||||||
|
const newRoom: NewBookingRoom = {
|
||||||
|
roomCategory: roomCategoryId,
|
||||||
|
guestNumber: guestNumber,
|
||||||
|
amount: newCount,
|
||||||
|
price: price
|
||||||
|
};
|
||||||
|
addNewRoom(newRoom);
|
||||||
}
|
}
|
||||||
const canAddMore: boolean = availableAmount > selectedAmount;
|
|
||||||
|
const totalSelected = () => {
|
||||||
|
let sum = 0;
|
||||||
|
selectedAmount.forEach((v) => sum += v)
|
||||||
|
return sum;
|
||||||
|
};
|
||||||
|
const canAddMore: boolean = availableAmount > totalSelected();
|
||||||
|
|
||||||
//This seems a little bit overkill, but IDK.
|
//This seems a little bit overkill, but IDK.
|
||||||
const fareFilter: FareFilter | null = useMemo(
|
const fareFilter: FareFilter | null = useMemo(
|
||||||
@@ -69,11 +88,11 @@ function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: numb
|
|||||||
if (hasPrices) {
|
if (hasPrices) {
|
||||||
const tableRows = roomPriceList?.map((el) => {
|
const tableRows = roomPriceList?.map((el) => {
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow key={el.guestNumber}>
|
||||||
<TableCell>{el.guestNumber}</TableCell>
|
<TableCell>{el.guestNumber}</TableCell>
|
||||||
<TableCell>{el.price / 100} €</TableCell>
|
<TableCell>{el.price / 100} €</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<NumberSelector updateCount={(count) => onSelectedAmountUpdate(el.guestNumber, count)} addEnabled={canAddMore}></NumberSelector>
|
<NumberSelector updateCount={(count) => onSelectedAmountUpdate(el.guestNumber, count, el.price)} addEnabled={canAddMore}></NumberSelector>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@@ -116,7 +135,7 @@ function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: numb
|
|||||||
{hasPrices ? priceTable : <Typography variant={"largeText"}>No Prices available</Typography>}
|
{hasPrices ? priceTable : <Typography variant={"largeText"}>No Prices available</Typography>}
|
||||||
<div>
|
<div>
|
||||||
</div>
|
</div>
|
||||||
<p>Selected: {selectedAmount}</p>
|
<p>Selected: {totalSelected()}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useAppStore } from "@/store"
|
||||||
|
import { Card, CardContent, CardHeader } from "../ui/card"
|
||||||
|
import { Typography } from "../ui/typography"
|
||||||
|
import { differenceInDays } from "date-fns";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export function BookingDetails() {
|
||||||
|
|
||||||
|
const bookingData = useAppStore((state) => state.newBookingData);
|
||||||
|
|
||||||
|
if (!bookingData || bookingData.rooms.length <= 0) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Typography variant={"h2"} as="h2">Your Booking</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant={"h3"}>No rooms added yet</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomList = bookingData.rooms.map((room) => {
|
||||||
|
return (
|
||||||
|
<TableRow key={room.roomCategory + "_" + room.guestNumber}>
|
||||||
|
<TableCell>{room.roomCategory}</TableCell>
|
||||||
|
<TableCell>{room.guestNumber}</TableCell>
|
||||||
|
<TableCell>{room.amount}</TableCell>
|
||||||
|
<TableCell>{room.price / 100} €</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const roomTable = (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Room</TableHead>
|
||||||
|
<TableHead>Guests</TableHead>
|
||||||
|
<TableHead>Quantity</TableHead>
|
||||||
|
<TableHead>Price</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{roomList}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const totalPrice = bookingData.rooms.reduce((acc, room) => (acc + room.price), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Typography variant={"h2"} as="h2">Your Booking</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant={"h3"}>Your Dates</Typography>
|
||||||
|
<Typography variant={"largeText"}>
|
||||||
|
Arrival: {bookingData.arrivalDate.toLocaleDateString()} -
|
||||||
|
Departure: {bookingData.departureDate.toLocaleDateString()} <br></br>
|
||||||
|
Number of nights: {differenceInDays(bookingData.departureDate, bookingData.arrivalDate)}
|
||||||
|
</Typography>
|
||||||
|
<Separator className="my-2"></Separator>
|
||||||
|
<Typography variant={"h3"}>Your Rooms</Typography>
|
||||||
|
{roomTable}
|
||||||
|
<Separator className="my-2"></Separator>
|
||||||
|
<Typography variant={"h3"}>Total Price</Typography>
|
||||||
|
<Typography variant={"h3"}>{totalPrice / 100} €</Typography>
|
||||||
|
<Button className="my-2"><Typography variant={"largeText"} className="text-primary-foreground">Book it!</Typography></Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
export type NewBooking = {
|
||||||
|
arrivalDate: Date,
|
||||||
|
departureDate: Date,
|
||||||
|
customerDetails: NewBookingCustomerDetails,
|
||||||
|
rooms: NewBookingRoom[]
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewBookingCustomerDetails = {
|
||||||
|
name: string | null,
|
||||||
|
surname: string | null,
|
||||||
|
email: string | null,
|
||||||
|
phoneNumber: string | null
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewBookingRoom = {
|
||||||
|
roomCategory: number,
|
||||||
|
guestNumber: number,
|
||||||
|
amount: number,
|
||||||
|
price: number
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { useAPIRoomAvailability } from "@/api/api-roomSearch"
|
|||||||
import RoomBooking from "@/components/Rooms/room-booking"
|
import RoomBooking from "@/components/Rooms/room-booking"
|
||||||
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
|
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
|
||||||
import BackendConnectionStatus from "@/components/backend-conection-status"
|
import BackendConnectionStatus from "@/components/backend-conection-status"
|
||||||
|
import { BookingDetails } from "@/components/booking/booking-details"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { Typography } from "@/components/ui/typography"
|
import { Typography } from "@/components/ui/typography"
|
||||||
@@ -15,12 +16,19 @@ function SearchPage() {
|
|||||||
|
|
||||||
//const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>();
|
//const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>();
|
||||||
const searchFilter = useAppStore((state) => (state.searchFilter));
|
const searchFilter = useAppStore((state) => (state.searchFilter));
|
||||||
|
const resetBooking = useAppStore((state) => (state.resetBookingState));
|
||||||
|
|
||||||
const [isSearched, setIsSearched] = useState<boolean>(false);
|
const [isSearched, setIsSearched] = useState<boolean>(false);
|
||||||
|
|
||||||
const onBtSearchClick = (newFilter: SearchFilter) => {
|
const onBtSearchClick = (newFilter: SearchFilter) => {
|
||||||
console.log("onBtSearchClick: ");
|
console.log("onBtSearchClick: ");
|
||||||
console.log(newFilter);
|
console.log(newFilter);
|
||||||
setIsSearched(true);
|
|
||||||
|
if (newFilter.dateRange && newFilter.dateRange.from && newFilter.dateRange.to) {
|
||||||
|
setIsSearched(true);
|
||||||
|
|
||||||
|
resetBooking(newFilter.dateRange.from, newFilter.dateRange.to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//This seems a little bit overkill, but IDK.
|
//This seems a little bit overkill, but IDK.
|
||||||
@@ -71,7 +79,7 @@ function SearchPage() {
|
|||||||
{hasRooms ? roomList : (isSearched && noRoomsAvailable)}
|
{hasRooms ? roomList : (isSearched && noRoomsAvailable)}
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-10" />
|
<Separator className="my-10" />
|
||||||
<RoomBooking key={99} availableAmount={5} roomCategoryId={6} />
|
<BookingDetails></BookingDetails>
|
||||||
<Separator className="my-10" />
|
<Separator className="my-10" />
|
||||||
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
|
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+65
-1
@@ -1,16 +1,80 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { SearchFilter } from './model/SearchFilter'
|
import { SearchFilter } from './model/SearchFilter'
|
||||||
|
import { NewBooking, NewBookingRoom } from './model/Bookings'
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
searchFilter: SearchFilter
|
searchFilter: SearchFilter
|
||||||
setSearchFilter: (newFilter: SearchFilter) => void
|
setSearchFilter: (newFilter: SearchFilter) => void
|
||||||
|
newBookingData: NewBooking | null
|
||||||
|
setNewBooking: (newBooking: NewBooking) => void
|
||||||
|
resetBookingState: (arrival: Date, departure: Date) => void
|
||||||
|
addNewBookingRoom: (newRoom: NewBookingRoom) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSearchFilter: SearchFilter = {
|
const defaultSearchFilter: SearchFilter = {
|
||||||
dateRange: { from: new Date() }
|
dateRange: { from: new Date() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultNewBooking: NewBooking = {
|
||||||
|
arrivalDate: new Date(),
|
||||||
|
departureDate: new Date(),
|
||||||
|
customerDetails: {
|
||||||
|
name: null,
|
||||||
|
surname: null,
|
||||||
|
email: null,
|
||||||
|
phoneNumber: null
|
||||||
|
},
|
||||||
|
rooms: []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultBookingData(arrivalDate: Date, departureDate: Date) {
|
||||||
|
return {
|
||||||
|
...defaultNewBooking,
|
||||||
|
arrivalDate: arrivalDate,
|
||||||
|
departureDate: departureDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewBookingRoomToCurrentState(newRoom: NewBookingRoom, currentState: NewBooking | null): NewBooking | null {
|
||||||
|
|
||||||
|
if (currentState == null) {
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Remove rooms with "same key".
|
||||||
|
const filteredRooms = currentState.rooms.filter((room) => {
|
||||||
|
const diffRoom = room.roomCategory != newRoom.roomCategory;
|
||||||
|
const diffAmount = room.guestNumber != newRoom.guestNumber;
|
||||||
|
if(diffRoom || (!diffRoom && diffAmount)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
console.log(filteredRooms);
|
||||||
|
|
||||||
|
if (newRoom.amount > 0) {
|
||||||
|
filteredRooms.push(newRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const newState: NewBooking = {
|
||||||
|
...currentState,
|
||||||
|
rooms: filteredRooms ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("AddRoomNewState:");
|
||||||
|
console.log(newState);
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
export const useAppStore = create<AppState>()((set) => ({
|
export const useAppStore = create<AppState>()((set) => ({
|
||||||
searchFilter: defaultSearchFilter,
|
searchFilter: defaultSearchFilter,
|
||||||
setSearchFilter: (newFilter) => set(() => ({ searchFilter: newFilter }))
|
setSearchFilter: (newFilter) => set(() => ({ searchFilter: newFilter })),
|
||||||
|
newBookingData: null,
|
||||||
|
setNewBooking: (newBooking) => set(() => ({ newBookingData: newBooking })),
|
||||||
|
resetBookingState: (arrival, departure) =>
|
||||||
|
set(() => ({ newBookingData: getDefaultBookingData(arrival, departure) })),
|
||||||
|
addNewBookingRoom: (newRoom: NewBookingRoom) =>
|
||||||
|
set((state) => ({ newBookingData: addNewBookingRoomToCurrentState(newRoom, state.newBookingData) }))
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user