diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index 96ae698..234c2f8 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -2,35 +2,34 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import isPropValid from '@emotion/is-prop-valid'; import { StyleSheetManager } from 'styled-components'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useEffect, useMemo, useReducer } from 'react'; +// import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // Components import AppLayout from './components/AppLayout'; +import Toast from './components/Toast'; + +// Pages import User from './pages/User'; -import Dashboard from './pages/Dashboard'; +import Booking from './pages/Booking'; import Room from './pages/Room'; import NotFound from './pages/NotFound'; // Constants import * as PATH from './constants/path'; -import Toast from './components/Toast'; + +// Hooks import { UserRoomAvailableContext, initialState, reducer, } from '@context/UserRoomAvailableContext'; -import { useEffect, useMemo, useReducer } from 'react'; // Services -import { getUserNotBooked } from '@service/userServices'; -import { getRoomsAvailable } from '@service/roomServices'; +import { getAllUsers } from '@service/userServices'; +import { getAllRooms } from '@service/roomServices'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, - }, - }, -}); +const queryClient = new QueryClient(); function App() { const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer( @@ -41,16 +40,26 @@ function App() { // Init list user and room available useEffect(() => { const load = async () => { - const tempUser = await getUserNotBooked(); + const tempUser = await getAllUsers({ + sortBy: 'id', + orderBy: 'asc', + phoneSearch: '', + page: 0, + }); if (tempUser) { - dispatch({ type: 'initUser', payload: tempUser }); + dispatch({ type: 'initUser', payload: tempUser.data }); } - const tempRoom = await getRoomsAvailable(); + const tempRoom = await getAllRooms({ + sortBy: 'id', + orderBy: 'asc', + roomSearch: '', + page: 0, + }); if (tempRoom) { - dispatch({ type: 'initRoom', payload: tempRoom }); + dispatch({ type: 'initRoom', payload: tempRoom.data }); } }; @@ -63,16 +72,14 @@ function App() { return ( + {/* */} }> - } - /> - } /> + } /> + } /> } /> } /> diff --git a/hotel-management/src/components/Search/styled.ts b/hotel-management/src/components/Search/styled.ts index cf468b2..7fa8653 100644 --- a/hotel-management/src/components/Search/styled.ts +++ b/hotel-management/src/components/Search/styled.ts @@ -4,7 +4,8 @@ const StyledSearch = styled.input` border-radius: var(--radius-sm); border: 1px solid var(--border-color); font-size: var(--fs-sm-x); - padding: 5px 10px; + padding: 10px 10px; + `; export { StyledSearch }; diff --git a/hotel-management/src/constants/commons.ts b/hotel-management/src/constants/commons.ts index fc82f1f..e488048 100644 --- a/hotel-management/src/constants/commons.ts +++ b/hotel-management/src/constants/commons.ts @@ -48,6 +48,7 @@ const FORM = { DELETE: 'delete', USER: 'user-form', ROOM: 'room-form', + BOOKING: 'booking-form', CHECKOUT: 'checkout', }; @@ -57,10 +58,4 @@ const REGEX = { NUMBER: /^[0-9]*$/, }; -export { - USER_PAGE, - ROOM_PAGE, - FORM, - REGEX, - ORDERBY_OPTIONS, -}; +export { USER_PAGE, ROOM_PAGE, FORM, REGEX, ORDERBY_OPTIONS }; diff --git a/hotel-management/src/pages/Booking/BookingForm.tsx b/hotel-management/src/pages/Booking/BookingForm.tsx new file mode 100644 index 0000000..6040472 --- /dev/null +++ b/hotel-management/src/pages/Booking/BookingForm.tsx @@ -0,0 +1,300 @@ +import { useCallback, useMemo } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; + +// Styled +import Input from '@commonStyle/Input.ts'; +import { FormBtn } from './styled.ts'; + +// Constants +import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage.ts'; + +// Components +import Form from '@component/Form/index.tsx'; +import Select, { ISelectOptions } from '@component/Select/index.tsx'; + +// Hooks +import { useCreateBooking } from '@hook/bookings/useCreateBooking.ts'; +import { useUpdateBooking } from '@hook/bookings/useUpdateBooking.ts'; +import { useUserRoomAvailable } from '@hook/useUserRoomAvailable.ts'; + +// Helpers +import { + convertCurrencyToNumber, + formatCurrency, + getDayDiff, +} from '@helper/helper.ts'; + +// Services +import { getRoomById, updateRoomStatus } from '@service/roomServices.ts'; +import { updateUserBookedStatus } from '@service/userServices.ts'; + +// Types +import { IBooking, TBookingResponse } from '@type/booking.ts'; + +interface IBookingFormProp { + onCloseModal?: () => void; + booking?: TBookingResponse; +} + +interface IBookingForm extends Omit { + amount: string; +} + +const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => { + const { roomsAvailable, usersAvailable, dispatch } = useUserRoomAvailable(); + const { isCreating, createBooking } = useCreateBooking(); + const { isUpdating, updateBooking } = useUpdateBooking(); + const isLoading = isCreating || isUpdating; + const { id: editId, ...editValues } = { ...booking }; + const formMethods = useForm({ + defaultValues: editId + ? { + startDate: editValues.startDate, + endDate: editValues.endDate, + roomId: editValues.rooms?.id, + userId: editValues.users?.id, + amount: formatCurrency(editValues.amount!), + } + : {}, + }); + const { + register, + handleSubmit, + reset, + formState: { errors, isDirty, isValid }, + trigger, + getValues, + setValue, + } = formMethods; + + // Submit form + const onSubmit = async (newBooking: IBookingForm) => { + if (!editId) { + // Add request + createBooking( + { ...newBooking, amount: convertCurrencyToNumber(newBooking.amount) }, + { + onSuccess: async () => { + reset(); + onCloseModal?.(); + + // Update room, user available in global state + dispatch!({ + type: 'updateStatusUser', + payload: [{ id: newBooking.userId, isBooked: true }], + }); + dispatch!({ + type: 'updateStatusRoom', + payload: [{ id: newBooking.roomId, status: true }], + }); + + // Update room, user available in server + await updateRoomStatus(newBooking.roomId, true); + await updateUserBookedStatus(newBooking.userId, true); + }, + } + ); + } else { + // Edit request + newBooking.id = editId!; + updateBooking( + { ...newBooking, amount: convertCurrencyToNumber(newBooking.amount) }, + { + onSuccess: async () => { + reset(); + onCloseModal?.(); + + // Update room in global state + // Old room + dispatch!({ + type: 'updateStatusRoom', + payload: [{ id: booking!.rooms!.id, status: false }], + }); + + // New room + dispatch!({ + type: 'updateStatusRoom', + payload: [{ id: newBooking.roomId, status: true }], + }); + + // Update room available in server + // Old room + await updateRoomStatus(booking!.rooms!.id, false); + + // New room + await updateRoomStatus(newBooking.roomId, true); + }, + } + ); + } + }; + + // Init user, room available to options choice + const userOptions = useMemo(() => { + const options: ISelectOptions[] = []; + + usersAvailable?.forEach((item) => { + if (booking?.users?.id === item.id || !item.isBooked) { + options.push({ + label: item.name!, + value: item.id.toString(), + }); + } + }); + + return options; + }, [usersAvailable, booking?.users?.id]); + + const roomOptions = useMemo(() => { + const options: ISelectOptions[] = []; + + roomsAvailable?.forEach((item) => { + if (booking?.rooms?.id === item.id || !item.status) { + options.push({ + label: item.name!, + value: item.id.toString(), + }); + } + }); + + return options; + }, [roomsAvailable, booking?.rooms?.id]); + + // Calculate the final price + const computePrice = useCallback(async () => { + setValue('amount', 'Loading...'); + const startDateValue = getValues('startDate'); + const endDateValue = getValues('endDate'); + const roomValue = getValues('roomId'); + + if (startDateValue && endDateValue && roomValue) { + const daysDiff = getDayDiff( + new Date(startDateValue), + new Date(endDateValue) + ); + + // Fetch room data + const room = await getRoomById(roomValue.toString()); + const amount = daysDiff * room.price; + + setValue('amount', formatCurrency(amount), { shouldValidate: true }); + } + }, [getValues, setValue]); + + const startDateValidate = useMemo( + () => new Date().toISOString().split('T')[0], + [] + ); + + const endDateValidate = () => { + if (getValues('startDate')) { + const date = new Date(getValues('startDate')); + date.setDate(date.getDate() + 1); + + return date.toISOString().split('T')[0]; + } + + return; + }; + + return ( + +
+ + {userOptions.length ? ( + trigger('roomId'), + }} + /> + ) : ( +

No room available

+ )} +
+ + + { + trigger('startDate'); + computePrice(); + }, + })} + /> + + + + { + trigger('endDate'); + computePrice(); + }, + })} + /> + + + + trigger('amount'), + })} + readOnly + /> + + + + + { + !editId + ? 'Add' + : 'Save' + } + + + Close + + +
+
+ ); +}; + +export default BookingForm; diff --git a/hotel-management/src/pages/Booking/BookingRow.tsx b/hotel-management/src/pages/Booking/BookingRow.tsx new file mode 100644 index 0000000..f1d900f --- /dev/null +++ b/hotel-management/src/pages/Booking/BookingRow.tsx @@ -0,0 +1,112 @@ +import toast from 'react-hot-toast'; +import { useCallback, useMemo } from 'react'; + +// Helpers +import { formatCurrency } from '@helper/helper'; + +// Types +import { TBookingResponse } from '@type/booking'; + +// Components +import Modal from '@component/Modal'; +import Table from '@component/Table'; +import Menus from '@component/Menus'; +import { RiEditBoxFill } from 'react-icons/ri'; +import { TbArrowNarrowRight } from 'react-icons/tb'; +import { ImExit } from 'react-icons/im'; +import BookingForm from './BookingForm'; +import ConfirmMessage from '@component/ConfirmMessage'; + +// Hooks +import useCheckOut from '@hook/bookings/useCheckout'; + +// Constants +import { FORM } from '@constant/commons'; + +interface IBookingRow { + booking: TBookingResponse; +} + +const BookingRow = ({ booking }: IBookingRow) => { + const { checkOutBooking } = useCheckOut(); + const { id, users, startDate, endDate, rooms, amount, status } = booking; + const statusText = status + ? 'Check in' + : 'Check out'; + const formattedPrice = useMemo(() => formatCurrency(amount), [amount]); + const renderEditBtn = useCallback( + (onCloseModal: () => void) => ( + }> + Edit + + ), + [] + ); + + const renderCheckOutBtn = useCallback( + (onCloseModal: () => void) => ( + }> + Checkout + + ), + [] + ); + + const handleClickCheckOutBtn = useCallback(() => { + if (booking.status) { + checkOutBooking({ + idBooking: booking.id, + roomId: booking!.rooms!.id, + userId: booking!.users!.id, + }); + } else { + toast.error('User already checkout!'); + } + }, [booking, checkOutBooking]); + + return ( + +
{users?.name}
+
+ {startDate}   +   {endDate} +
+
{rooms?.name}
+
{formattedPrice}
+
{statusText}
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default BookingRow; diff --git a/hotel-management/src/pages/Booking/BookingTable.tsx b/hotel-management/src/pages/Booking/BookingTable.tsx new file mode 100644 index 0000000..14a93ad --- /dev/null +++ b/hotel-management/src/pages/Booking/BookingTable.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; + +// Components +import Menus from '@component/Menus'; +import Table from '@component/Table'; +import Message from '@component/Message'; +import Search from '@component/Search'; +import Pagination from '@component/Pagination'; +import BookingRow from './BookingRow'; + +// Styled +import { StyledOperationTable } from './styled'; +import Direction from '@commonStyle/Direction'; +import Spinner from '@commonStyle/Spinner'; + +// Hooks +import { useBookings } from '@hook/bookings/useBookings'; + +// Types +import { TBookingResponse } from '@type/booking'; + +const BookingTable = () => { + const columnName = ['User', 'Date', 'Room', 'Amount', 'Status']; + const { isLoading, bookings, count } = useBookings(); + + const renderBookingRow = useCallback( + (booking: TBookingResponse) => ( + + ), + [] + ); + + return ( + <> + + + + + + {isLoading && } + + {bookings && bookings.length ? ( + + + + + data={bookings} + render={renderBookingRow} + /> + + + +
+
+ ) : ( + !isLoading && No data to show here! + )} +
+ + ); +}; + +export default BookingTable; diff --git a/hotel-management/src/pages/Booking/index.tsx b/hotel-management/src/pages/Booking/index.tsx new file mode 100644 index 0000000..0edb7b4 --- /dev/null +++ b/hotel-management/src/pages/Booking/index.tsx @@ -0,0 +1,38 @@ +// Styled +import Direction from '@commonStyle/Direction'; +import { StyledBooking, Title } from './styled'; +import Button from '@commonStyle/Button'; + +// Components +import Modal from '@component/Modal'; +import BookingTable from './BookingTable'; +import BookingForm from './BookingForm'; + +// Constants +import { FORM } from '@constant/commons'; + +const Booking = () => { + return ( + + + List Booking + + + ( + + )} + /> + + + + + + + + + ); +}; + +export default Booking; diff --git a/hotel-management/src/pages/Booking/styled.ts b/hotel-management/src/pages/Booking/styled.ts new file mode 100644 index 0000000..6899c24 --- /dev/null +++ b/hotel-management/src/pages/Booking/styled.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +// Components +import Button from '@commonStyle/Button'; + +const StyledBooking = styled.main` + padding: 20px; + padding-bottom: 100px; + + display: flex; + flex-direction: column; + gap: 30px; +`; + +const Title = styled.h2` + font-size: var(--fs-md); + color: var(--dark-text); + text-transform: capitalize; +`; + +const FormBtn = styled(Button)` + width: 100%; + + &:disabled, + &[disabled] { + background-color: var(--disabled-btn-color); + } +`; + +const StyledOperationTable = styled.div` + display: flex; + gap: 30px; + justify-content: flex-end; +`; + +export { StyledBooking, Title, StyledOperationTable, FormBtn }; diff --git a/hotel-management/src/pages/Dashboard.tsx b/hotel-management/src/pages/Dashboard.tsx deleted file mode 100644 index 592fd32..0000000 --- a/hotel-management/src/pages/Dashboard.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import styled from 'styled-components'; - -// Hooks -import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; - -const StyledDashboard = styled.main` - padding: 20px; -`; - -const Dashboard = () => { - const { roomsAvailable, usersAvailable } = useUserRoomAvailable(); - - console.log('Rooms Available: ', roomsAvailable); - console.log('Users available: ', usersAvailable); - - return ( - -

This page currently still develop. Please try again later!

-
- ); -}; - -export default Dashboard; diff --git a/hotel-management/src/pages/Room/RoomRow.tsx b/hotel-management/src/pages/Room/RoomRow.tsx index ae7aaf4..1a6709a 100644 --- a/hotel-management/src/pages/Room/RoomRow.tsx +++ b/hotel-management/src/pages/Room/RoomRow.tsx @@ -27,7 +27,7 @@ interface IRoomRow { const RoomRow = ({ room }: IRoomRow) => { const { id, name, price, status } = room; - const { isDeleting, deleteRoom } = useDeleteRoom(); + const { deleteRoom } = useDeleteRoom(); const statusText = status ? 'Unavailable' : 'Available'; @@ -78,7 +78,6 @@ const RoomRow = ({ room }: IRoomRow) => { deleteRoom(id)} /> diff --git a/hotel-management/src/pages/Room/index.tsx b/hotel-management/src/pages/Room/index.tsx index d2cbde3..ddde771 100644 --- a/hotel-management/src/pages/Room/index.tsx +++ b/hotel-management/src/pages/Room/index.tsx @@ -10,27 +10,25 @@ import Button from '@commonStyle/Button'; const Room = () => { return ( - <> - - - List Room + + + List Room - - ( - - )} - /> - - - - - + + ( + + )} + /> + + + + + - - - + + ); }; diff --git a/hotel-management/src/pages/User/UserRow.tsx b/hotel-management/src/pages/User/UserRow.tsx index 0ca4aca..59644fa 100644 --- a/hotel-management/src/pages/User/UserRow.tsx +++ b/hotel-management/src/pages/User/UserRow.tsx @@ -5,9 +5,13 @@ import Table from '@component/Table'; import Menus from '@component/Menus'; import UserForm from './UserForm'; - // Types import { IUser } from '@type/users'; +import { FORM } from '@constant/commons'; +import { HiTrash } from 'react-icons/hi'; +import { useCallback } from 'react'; +import ConfirmMessage from '@component/ConfirmMessage'; +import { useDeleteUser } from '@hook/users/useDeleteUser'; interface IUserRow { user: IUser; @@ -15,17 +19,35 @@ interface IUserRow { const UserRow = ({ user }: IUserRow) => { const { id, name, phone, isBooked } = user; + const { deleteUser } = useDeleteUser(); + const renderEditBtn = useCallback( + (onCloseModal: () => void) => ( + }> + Edit + + ), + [] + ); + + const renderDeleteBtn = useCallback( + (onCloseModal: () => void) => ( + } onClick={onCloseModal}> + Delete + + ), + [] + ); return (
{id}
{name}
{phone}
{ - isBooked - ? 'Yes' + isBooked + ? 'Yes' : 'No' - }
+ }
@@ -33,18 +55,26 @@ const UserRow = ({ user }: IUserRow) => { ( - }> - Edit - - )} + modalName={FORM.EDIT} + renderChildren={renderEditBtn} + /> + + - + + + + deleteUser(id)} + /> +