diff --git a/client/src/components/BookingList/BookingCard/BookingCard.tsx b/client/src/components/BookingList/BookingCard/BookingCard.tsx index 774dc95614b8ec8cae317e555cb84c564cd43696..ebc11f7076edbb33f5f4e109c9da3c2135aadfd3 100644 --- a/client/src/components/BookingList/BookingCard/BookingCard.tsx +++ b/client/src/components/BookingList/BookingCard/BookingCard.tsx @@ -3,7 +3,6 @@ import { IFlight } from "../../../services/Dashboard/CustomerDashboard"; import "./BookingCard.scss"; import { BookingOrder, - bookFlight, } from "../../../services/BookingList/BookingOrder"; interface IBookingCard { diff --git a/client/src/components/BookingView/BookingSeatCard.tsx b/client/src/components/BookingView/BookingSeatCard.tsx index 2693fcfd873ef39923f59ffaf45f1ae5c4a3bd86..9d312a73e69a5f3906710b625a86a1781a9a330f 100644 --- a/client/src/components/BookingView/BookingSeatCard.tsx +++ b/client/src/components/BookingView/BookingSeatCard.tsx @@ -1,10 +1,50 @@ import { useEffect, useState } from "react"; -import { GetBookingSeat } from "../../services/BookingView/BookingView"; +import { GetBookingSeat, IFullBookingInfo, updateSeat } from "../../services/BookingView/BookingView"; import { ISeat } from "../../services/Dashboard/CustomerDashboard"; +import { useLoaderData } from "react-router-dom"; +import { useForm } from "react-hook-form"; +import { AxiosError } from "axios"; + +interface ISeatForm { + seat: string; +} const BookingSeatCard = ({ seatId }: { seatId?: number }) => { + const data = useLoaderData() as IFullBookingInfo; + const { register, watch } = useForm<ISeatForm>({ mode: 'onChange', defaultValues: { seat: '' } }); const [seatData, setSeatData] = useState<ISeat | undefined>(); + useEffect(() => { + const { unsubscribe } = watch(async (form) => { + if (form.seat) { + try { + const result = await updateSeat(data.booking.id, +form.seat); + + if (result.data.bookedSeat) { + GetBookingSeat(+form.seat).then((data) => { + setSeatData(data); + alert('succesfully booked seat'); + }); + } else { + alert('Unable to book seat') + } + + } catch (error) { + const errorMessage = (error as AxiosError).response?.data; + + if (typeof errorMessage == 'string') { + alert(errorMessage); + } else { + alert('An unexpected error has occurred'); + } + } + } + }); + + return () => unsubscribe() + }, [watch]); + + useEffect(() => { if (!seatId) { return; @@ -14,10 +54,26 @@ const BookingSeatCard = ({ seatId }: { seatId?: number }) => { }); }, [seatId]); + const seats = data.seats.$values.filter((seat) => seat.classType === data.booking.bookingClass && seat.isAvailable); return ( <div className="card booking-view-card"> <h3>Seat Information</h3> - <div>Seat number: {seatData ? seatData.seatNumber : "N/A"}</div> + + <form> + <label>Seat:</label> + <select className="seat-selector" {...register('seat', { required: true })} disabled={!!seatData}> + { + !seatData ? + <option value='' disabled>Select a Seat:</option> : + <option value={seatData.id}>{seatData.seatNumber}</option> + } + { + seats.map((seat) => { + return <option key={seat.id} value={seat.id}>{seat.seatNumber}</option> + }) + } + </select> + </form> </div> ); }; diff --git a/client/src/components/BookingView/BookingView.scss b/client/src/components/BookingView/BookingView.scss index 81b2252909fd315e4130ed5b1260b4397c692b48..55b4a4fc211ead1eaa0ff142ddbf2d762626ff10 100644 --- a/client/src/components/BookingView/BookingView.scss +++ b/client/src/components/BookingView/BookingView.scss @@ -11,3 +11,8 @@ min-width: 350px; min-height: 200px; } + +.seat-selector { + margin-left: 0.5rem; + padding: 4px; +} diff --git a/client/src/components/BookingView/BookingView.tsx b/client/src/components/BookingView/BookingView.tsx index 0923ff7ac1ec2584193dbe4b5490b21c80349af6..6e98e1c5fa1093b069b6ae36816ca4ad640000a5 100644 --- a/client/src/components/BookingView/BookingView.tsx +++ b/client/src/components/BookingView/BookingView.tsx @@ -2,15 +2,15 @@ import { useLoaderData } from "react-router-dom"; import BookingFlightCard from "./BookingFlightCard"; import BookingSeatCard from "./BookingSeatCard"; import "./BookingView.scss"; -import { IBookingInfo } from "../../services/BookingView/BookingView"; +import { IFullBookingInfo } from "../../services/BookingView/BookingView"; const BookingView = () => { - const flightData = useLoaderData() as IBookingInfo | undefined; + const flightData = useLoaderData() as IFullBookingInfo; return ( <div className="booking-view"> - {flightData && <BookingFlightCard flightId={flightData?.flightId} />} - {flightData && <BookingSeatCard seatId={flightData?.seatId} />} + {flightData && <BookingFlightCard flightId={flightData.booking.flightId} />} + {flightData && <BookingSeatCard seatId={flightData.booking.seatId} />} </div> ); }; diff --git a/client/src/components/CustomerBookings/CustomerBookings.tsx b/client/src/components/CustomerBookings/CustomerBookings.tsx index df74fe7e72c711b92bcf73f49102a164d7f5dcac..62078c26f9d61f09870a677a3a7dac0a6d04f105 100644 --- a/client/src/components/CustomerBookings/CustomerBookings.tsx +++ b/client/src/components/CustomerBookings/CustomerBookings.tsx @@ -19,6 +19,7 @@ const CustomerBookings = () => { flightId={booking.flightId} flightType={flightType} bookingId={booking.id} + key={booking.id} /> ))} </div> diff --git a/client/src/components/PaymentForm/PaymentForm.tsx b/client/src/components/PaymentForm/PaymentForm.tsx index 64ac254857ba1b73f92687d71f6e22d4973ec76c..c70666afa78d8aef8e4887d2a1116775e63e0992 100644 --- a/client/src/components/PaymentForm/PaymentForm.tsx +++ b/client/src/components/PaymentForm/PaymentForm.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useLocation, useNavigate } from "react-router-dom"; import Api from "../../helpers/Api"; import { CardElement, useStripe, useElements, Elements } from '@stripe/react-stripe-js'; -import { loadStripe } from '@stripe/stripe-js'; +import { StripeCardElement, loadStripe } from '@stripe/stripe-js'; import {bookFlight} from "../../services/BookingList/BookingOrder"; import './PaymentForm.scss'; @@ -14,7 +14,7 @@ const PaymentForm = () => { const location = useLocation(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); - const [bookingInfo, setBookingInfo] = useState(null); + const [bookingInfo, setBookingInfo] = useState<any>(null); const [message, setMessage] = useState(""); const [clientSecret, setClientSecret] = useState(""); @@ -41,7 +41,7 @@ const PaymentForm = () => { fetchClientSecret(); }, [stripe, bookingInfo]); - const handleSubmit = async (event) => { + const handleSubmit = async (event: any) => { event.preventDefault(); if (!stripe || !elements || !clientSecret) { console.log('Stripe.js has not yet loaded or client secret not available.'); @@ -52,7 +52,7 @@ const PaymentForm = () => { try { const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { payment_method: { - card: elements.getElement(CardElement), + card: elements.getElement(CardElement) as StripeCardElement, }, }); @@ -89,7 +89,6 @@ const PaymentForm = () => { <h3 id="form-heading">Last step!</h3> <p id="form-description">Enter your payment info below.</p> <CardElement className="StripeElement" onChange={event => { - setDisabled(event.empty); setMessage(event.error ? event.error.message : ""); }} /> <button type="submit" className="button" disabled={!stripe || loading}> diff --git a/client/src/components/ProtectedRoute/AirlineProtectedRoute.tsx b/client/src/components/ProtectedRoute/AirlineProtectedRoute.tsx index da4acae44d5c5794910750bc6f5857d4208ed6b1..af6d64b22e75de030db686a6765551c18c352739 100644 --- a/client/src/components/ProtectedRoute/AirlineProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute/AirlineProtectedRoute.tsx @@ -1,5 +1,6 @@ -import { Navigate, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useAuth } from '../../hooks/useAuth'; +import ErrorPage from '../ErrorPage/ErrorPage'; function AirlineProtectedRoute() { const { user } = useAuth(); @@ -8,7 +9,7 @@ function AirlineProtectedRoute() { return <Outlet></Outlet> } - return <Navigate to={'/error'}></Navigate> + return <ErrorPage></ErrorPage> } export default AirlineProtectedRoute; diff --git a/client/src/components/ProtectedRoute/CustomerProtectedRoute.tsx b/client/src/components/ProtectedRoute/CustomerProtectedRoute.tsx index 6cb20e36177a517685d498625e16a64789b03474..60e30662aa6c522bf74a857d387d97cddd9faa23 100644 --- a/client/src/components/ProtectedRoute/CustomerProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute/CustomerProtectedRoute.tsx @@ -1,5 +1,6 @@ -import { Navigate, Outlet } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { useAuth } from '../../hooks/useAuth'; +import ErrorPage from '../ErrorPage/ErrorPage'; function CustomerProtectedRoute() { const { user } = useAuth(); @@ -8,7 +9,7 @@ function CustomerProtectedRoute() { return <Outlet></Outlet> } - return <Navigate to={'/error'}></Navigate> + return <ErrorPage></ErrorPage> } export default CustomerProtectedRoute; diff --git a/client/src/main.tsx b/client/src/main.tsx index 82fc0c40b6edef05e26b0def7c42bc59e5d5b592..28bec0b56c893f06c731bad5b0cc430ed47c937b 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -31,10 +31,8 @@ import AirlineProtectedRoute from "./components/ProtectedRoute/AirlineProtectedR import ErrorPage404 from "./components/ErrorPage/ErrorPage404.tsx"; import ErrorPage from "./components/ErrorPage/ErrorPage.tsx"; import { loadStripe } from '@stripe/stripe-js'; -import { Elements } from '@stripe/react-stripe-js'; - -const stripePromise = loadStripe('pk_test_51P5UhOExQclpActcEGkHdut1X1k6uQnEXQ2cKKTD5l9FS9a1TyB2ap1lRSQXZt35Dpd7mh8gHOwFyVb4TiqpZfJr00bDRrD4vF'); +loadStripe('pk_test_51P5UhOExQclpActcEGkHdut1X1k6uQnEXQ2cKKTD5l9FS9a1TyB2ap1lRSQXZt35Dpd7mh8gHOwFyVb4TiqpZfJr00bDRrD4vF'); const router = createBrowserRouter([ { diff --git a/client/src/services/BookingView/BookingView.ts b/client/src/services/BookingView/BookingView.ts index 2aa5ef16b883f9b70b156ce5d0ca02e6ad36cfd0..509ede07860189b2ae5bb54c23cd5ac5c62320db 100644 --- a/client/src/services/BookingView/BookingView.ts +++ b/client/src/services/BookingView/BookingView.ts @@ -1,6 +1,6 @@ import { Params } from "react-router-dom"; import Api from "../../helpers/Api"; -import { IFlight, ISeat } from "../Dashboard/CustomerDashboard"; +import { IFlight, ISeat, ISeats } from "../Dashboard/CustomerDashboard"; export interface IBookingInfo { id: number; @@ -10,6 +10,16 @@ export interface IBookingInfo { seatId?: number; } +export interface IFullBookingInfo { + booking: IBookingInfo; + seats: ISeats; +} + +interface ISeatBookingResponse { + bookedSeat: boolean; + message: string; +} + export interface IBookingData { flight: IFlight; seat: ISeat; @@ -19,12 +29,18 @@ export async function GetBookingInformation({ params, }: { params: Params; -}): Promise<IBookingInfo> { +}): Promise<IFullBookingInfo> { const url = `Booking/${params.id}`; try { - const booking = await Api.get(`${url}`, { withCredentials: true }); + const bookingCall = Api.get(`${url}`, { withCredentials: true }); + const seatsCall = Api.get(`Flight/${params.id}/seats`, { withCredentials: true }); + + const [booking, seats] = await Promise.all([bookingCall, seatsCall]); - return booking.data as IBookingInfo; + return { + booking: booking.data, + seats: seats.data + }; } catch (error) { throw error; } @@ -77,3 +93,7 @@ export async function GetFullBookingData( throw error; } } + +export async function updateSeat(booking: number, seatId: number) { + return Api.put<ISeatBookingResponse>(`Booking/${booking}`, { SeatId: seatId }, { withCredentials: true }); +}