From 830d19b586e0f2df537ffcdf4200db281737ae4f Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 14:23:09 +0700 Subject: [PATCH 01/26] Update config --- hotel-management/.gitignore | 1 + hotel-management/package.json | 3 ++- hotel-management/pnpm-lock.yaml | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/hotel-management/.gitignore b/hotel-management/.gitignore index d06f115..13549f4 100644 --- a/hotel-management/.gitignore +++ b/hotel-management/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +coverage *.local .vscode coverage diff --git a/hotel-management/package.json b/hotel-management/package.json index 584c75f..7634a5f 100644 --- a/hotel-management/package.json +++ b/hotel-management/package.json @@ -8,7 +8,7 @@ "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", - "test": "jest" + "test": "jest --coverage" }, "coverageReporters": [ "html" @@ -17,6 +17,7 @@ "@emotion/is-prop-valid": "^1.2.1", "@supabase/supabase-js": "^2.38.4", "@tanstack/react-query": "^5.8.4", + "@tanstack/react-query-devtools": "^5.8.4", "@types/react-test-renderer": "^18.0.6", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/hotel-management/pnpm-lock.yaml b/hotel-management/pnpm-lock.yaml index 6950453..1ea06fe 100644 --- a/hotel-management/pnpm-lock.yaml +++ b/hotel-management/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@tanstack/react-query': specifier: ^5.8.4 version: 5.8.4(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query-devtools': + specifier: ^5.8.4 + version: 5.8.4(@tanstack/react-query@5.8.4)(react-dom@18.2.0)(react@18.2.0) '@types/react-test-renderer': specifier: ^18.0.6 version: 18.0.6 @@ -1132,6 +1135,23 @@ packages: resolution: {integrity: sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==} dev: false + /@tanstack/query-devtools@5.8.4: + resolution: {integrity: sha512-F1dRbITNt9tMUoM9WCH8WQ2c54116hv52m/PKK8ZiN/pO2wGVzTZtKuLanF8pFpwmNchjIixcMw/a57HY5ivcw==} + dev: false + + /@tanstack/react-query-devtools@5.8.4(@tanstack/react-query@5.8.4)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mffs51FJqXU/5rwhbwv393DccL6et7uK2pRLwOcmMrWbPyW8vpxr9oidaghHX4cdVeP/7u5owW9yMpBhBAJfcQ==} + peerDependencies: + '@tanstack/react-query': ^5.8.4 + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@tanstack/query-devtools': 5.8.4 + '@tanstack/react-query': 5.8.4(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/react-query@5.8.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==} peerDependencies: From d843667b012fb8232487d63ed4753cd7463d4b38 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 14:31:06 +0700 Subject: [PATCH 02/26] Add pagination components and update some other components --- hotel-management/src/commons/styles/Button.ts | 2 +- .../src/components/OrderBy/index.tsx | 1 - .../src/components/OrderBy/styled.ts | 6 +- .../src/components/Pagination/index.tsx | 68 +++++++++++++++++++ .../src/components/Pagination/styled.ts | 50 ++++++++++++++ .../src/components/Table/index.tsx | 19 ++++-- .../src/components/Table/styled.ts | 17 ++++- hotel-management/src/constants/config.ts | 13 +++- 8 files changed, 163 insertions(+), 13 deletions(-) create mode 100644 hotel-management/src/components/Pagination/index.tsx create mode 100644 hotel-management/src/components/Pagination/styled.ts diff --git a/hotel-management/src/commons/styles/Button.ts b/hotel-management/src/commons/styles/Button.ts index 2324d3a..00c8db1 100644 --- a/hotel-management/src/commons/styles/Button.ts +++ b/hotel-management/src/commons/styles/Button.ts @@ -36,7 +36,7 @@ const Button = styled.button` &:disabled, &[disabled] { - cursor: no-drop; + cursor: not-allowed; } `; diff --git a/hotel-management/src/components/OrderBy/index.tsx b/hotel-management/src/components/OrderBy/index.tsx index f4ceaa8..1461c95 100644 --- a/hotel-management/src/components/OrderBy/index.tsx +++ b/hotel-management/src/components/OrderBy/index.tsx @@ -26,7 +26,6 @@ const OrderBy = ({ options }: IOrderProps) => { handleClick(option.value)} - active={option.value === currentOrder} disabled={option.value === currentOrder} > {option.label} diff --git a/hotel-management/src/components/OrderBy/styled.ts b/hotel-management/src/components/OrderBy/styled.ts index bfbfa56..80e3f51 100644 --- a/hotel-management/src/components/OrderBy/styled.ts +++ b/hotel-management/src/components/OrderBy/styled.ts @@ -9,7 +9,7 @@ const StyledOrder = styled.div` `; interface IOrderBtn { - active: boolean; + disabled: boolean; } const OrderButton = styled.button` @@ -25,11 +25,11 @@ const OrderButton = styled.button` padding: 5px 10px; ${(props) => - props.active && + props.disabled && css` background-color: var(--primary-color); color: var(--light-text); - cursor: no-drop; + cursor: not-allowed; `} &:hover:not(:disabled) { diff --git a/hotel-management/src/components/Pagination/index.tsx b/hotel-management/src/components/Pagination/index.tsx new file mode 100644 index 0000000..2f73a23 --- /dev/null +++ b/hotel-management/src/components/Pagination/index.tsx @@ -0,0 +1,68 @@ +import { DEFAULT_PAGE_SIZE } from '@constant/config'; +import { useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Buttons, PaginationBtn, StyledPagination } from './styled'; +import { MdKeyboardArrowRight, MdKeyboardArrowLeft } from 'react-icons/md'; + +interface IPagination { + count: number; +} + +const Pagination = ({ count }: IPagination) => { + const [searchParams, setSearchParams] = useSearchParams(); + const currentPage = !searchParams.get('page') + ? 1 + : Number(searchParams.get('page')); + + const totalPage = Math.ceil(count / DEFAULT_PAGE_SIZE); + + const nextPage = useCallback(() => { + const next = currentPage === totalPage ? currentPage : currentPage + 1; + + searchParams.set('page', next.toString()); + setSearchParams(searchParams); + }, [currentPage, searchParams, totalPage, setSearchParams]); + + const previousPage = useCallback(() => { + const previous = currentPage === 1 ? currentPage : currentPage - 1; + + searchParams.set('page', previous.toString()); + setSearchParams(searchParams); + }, [currentPage, searchParams, setSearchParams]); + + const fromIndex = useMemo( + () => (currentPage - 1) * DEFAULT_PAGE_SIZE + 1, + [currentPage] + ); + + const toIndex = useMemo( + () => (currentPage === totalPage ? count : currentPage * DEFAULT_PAGE_SIZE), + [currentPage, count, totalPage] + ); + + return ( + totalPage > 1 && ( + +

+ Showing {fromIndex} to {toIndex} of {count} results +

+ + + + Previous + + + + Next + + + +
+ ) + ); +}; + +export default Pagination; diff --git a/hotel-management/src/components/Pagination/styled.ts b/hotel-management/src/components/Pagination/styled.ts new file mode 100644 index 0000000..83074b5 --- /dev/null +++ b/hotel-management/src/components/Pagination/styled.ts @@ -0,0 +1,50 @@ +import styled from 'styled-components'; + +interface IPaginationBtn { + disabled: boolean; +} + +const StyledPagination = styled.div` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + font-size: var(--fs-sm-x); +`; + +const Buttons = styled.div` + display: flex; + gap: 30px; +`; + +const PaginationBtn = styled.button` + background-color: var(--secondary-btn-color); + border: none; + border-radius: var(--radius-sm); + font-weight: 500; + + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + + padding: 5px 10px; + + transition: all 0.3s; + + & svg { + height: 20px; + width: 20px; + } + + &:hover:not(:disabled) { + background-color: var(--primary-color); + color: var(--light-text); + } + + &:disabled { + cursor: not-allowed; + } +`; + +export { StyledPagination, PaginationBtn, Buttons }; diff --git a/hotel-management/src/components/Table/index.tsx b/hotel-management/src/components/Table/index.tsx index 1cea173..3dca4cd 100644 --- a/hotel-management/src/components/Table/index.tsx +++ b/hotel-management/src/components/Table/index.tsx @@ -1,7 +1,13 @@ import { ReactNode, useContext } from 'react'; // Components -import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled'; +import { + StyledBody, + StyledHeader, + StyledRow, + StyledTable, + StyledFooter, +} from './styled'; // Contexts import TableContext from '@context/TableContext'; @@ -33,9 +39,13 @@ const Table = ({ columns, children }: ITable) => { const Header = ({ headerColumn }: IHeader) => { const { columns } = useContext(TableContext); - return - {headerColumn.map((item) =>
{item}
)} -
; + return ( + + {headerColumn.map((item) => ( +
{item}
+ ))} +
+ ); }; const Body = ({ data, render }: ITableBody) => { @@ -55,5 +65,6 @@ const Row = ({ children }: ITable) => { Table.Header = Header; Table.Body = Body; Table.Row = Row; +Table.Footer = StyledFooter; export default Table; diff --git a/hotel-management/src/components/Table/styled.ts b/hotel-management/src/components/Table/styled.ts index 9b1f31e..7c10749 100644 --- a/hotel-management/src/components/Table/styled.ts +++ b/hotel-management/src/components/Table/styled.ts @@ -43,4 +43,19 @@ const StyledHeader = styled(CommonRow)` font-weight: 600; `; -export { StyledBody, StyledHeader, StyledRow, StyledTable }; +const StyledFooter = styled.div` + display: flex; + justify-content: center; + padding: 15px; + background-color: var(--footer-table-color); + border-bottom-left-radius: var(--radius-md); + border-bottom-right-radius: var(--radius-md); + + border-top: 1px solid var(--border-color); + + &:not(:has(*)) { + display: none; + } +`; + +export { StyledBody, StyledHeader, StyledRow, StyledTable, StyledFooter }; diff --git a/hotel-management/src/constants/config.ts b/hotel-management/src/constants/config.ts index 2ce25b2..cfffd62 100644 --- a/hotel-management/src/constants/config.ts +++ b/hotel-management/src/constants/config.ts @@ -1,6 +1,13 @@ const DEFAULT_SORT_BY = 'id'; const DEFAULT_ORDER_BY = 'asc'; -const supabaseUrl = 'https://pjqujsjzzdrlrgqbxepe.supabase.co' -const supabaseKey = process.env.VITE_SUPABASE_KEY +const DEFAULT_PAGE_SIZE = 5; +const supabaseUrl = 'https://pjqujsjzzdrlrgqbxepe.supabase.co'; +const supabaseKey = process.env.VITE_SUPABASE_KEY; -export { DEFAULT_SORT_BY, DEFAULT_ORDER_BY, supabaseKey, supabaseUrl }; +export { + DEFAULT_SORT_BY, + DEFAULT_ORDER_BY, + supabaseKey, + supabaseUrl, + DEFAULT_PAGE_SIZE, +}; From 0aa341c12c252ee270264feae445de5ad36221ef Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 14:39:32 +0700 Subject: [PATCH 03/26] Format code style and remove unnecessary code --- .../src/components/Modal/styled.ts | 7 ------- .../src/components/Pagination/index.tsx | 20 +++++++++++++++---- .../src/components/Table/index.tsx | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/hotel-management/src/components/Modal/styled.ts b/hotel-management/src/components/Modal/styled.ts index 1b1cc0d..d7f850d 100644 --- a/hotel-management/src/components/Modal/styled.ts +++ b/hotel-management/src/components/Modal/styled.ts @@ -6,13 +6,6 @@ const StyledModal = styled.div` left: 50%; transform: translate(-50%, -50%); - background-color: var(--color-grey-0); - - border-radius: var(--border-radius-lg); - box-shadow: var(--shadow-lg); - - padding: 3.2rem 4rem; - transition: all 0.5s; `; diff --git a/hotel-management/src/components/Pagination/index.tsx b/hotel-management/src/components/Pagination/index.tsx index 2f73a23..a21fc53 100644 --- a/hotel-management/src/components/Pagination/index.tsx +++ b/hotel-management/src/components/Pagination/index.tsx @@ -1,7 +1,13 @@ -import { DEFAULT_PAGE_SIZE } from '@constant/config'; import { useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; + +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + +// Styled import { Buttons, PaginationBtn, StyledPagination } from './styled'; + +// Components import { MdKeyboardArrowRight, MdKeyboardArrowLeft } from 'react-icons/md'; interface IPagination { @@ -17,14 +23,18 @@ const Pagination = ({ count }: IPagination) => { const totalPage = Math.ceil(count / DEFAULT_PAGE_SIZE); const nextPage = useCallback(() => { - const next = currentPage === totalPage ? currentPage : currentPage + 1; + const next = currentPage === totalPage + ? currentPage + : currentPage + 1; searchParams.set('page', next.toString()); setSearchParams(searchParams); }, [currentPage, searchParams, totalPage, setSearchParams]); const previousPage = useCallback(() => { - const previous = currentPage === 1 ? currentPage : currentPage - 1; + const previous = currentPage === 1 + ? currentPage + : currentPage - 1; searchParams.set('page', previous.toString()); setSearchParams(searchParams); @@ -36,7 +46,9 @@ const Pagination = ({ count }: IPagination) => { ); const toIndex = useMemo( - () => (currentPage === totalPage ? count : currentPage * DEFAULT_PAGE_SIZE), + () => (currentPage === totalPage + ? count + : currentPage * DEFAULT_PAGE_SIZE), [currentPage, count, totalPage] ); diff --git a/hotel-management/src/components/Table/index.tsx b/hotel-management/src/components/Table/index.tsx index 3dca4cd..76024e3 100644 --- a/hotel-management/src/components/Table/index.tsx +++ b/hotel-management/src/components/Table/index.tsx @@ -1,6 +1,6 @@ import { ReactNode, useContext } from 'react'; -// Components +// Styled import { StyledBody, StyledHeader, From a7bd3bf48b55e697df828cbd3b886b2f1eca9b5a Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 15:21:04 +0700 Subject: [PATCH 04/26] Update hooks and services --- .../src/hooks/rooms/useCreateRoom.ts | 8 +++- .../src/hooks/rooms/useDeleteRoom.ts | 8 +++- hotel-management/src/hooks/rooms/useRooms.ts | 43 ++++++++++++++++--- .../src/hooks/rooms/useUpdateRoom.ts | 10 +++-- .../src/hooks/users/useCreateUser.ts | 8 +++- .../src/hooks/users/useUpdateUser.ts | 10 +++-- hotel-management/src/hooks/users/useUsers.ts | 40 ++++++++++++++--- hotel-management/src/services/roomServices.ts | 17 +++++--- hotel-management/src/services/userServices.ts | 18 +++++--- .../src/styles/abstracts/variables.css | 1 + 10 files changed, 130 insertions(+), 33 deletions(-) diff --git a/hotel-management/src/hooks/rooms/useCreateRoom.ts b/hotel-management/src/hooks/rooms/useCreateRoom.ts index 045cd10..1eaee6a 100644 --- a/hotel-management/src/hooks/rooms/useCreateRoom.ts +++ b/hotel-management/src/hooks/rooms/useCreateRoom.ts @@ -18,7 +18,11 @@ const useCreateRoom = () => { const queryClient = useQueryClient(); const { dispatch } = useUserRoomAvailable(); - const { mutate: createRoom, isPending: isCreating } = useMutation({ + const { + mutate: createRoom, + isPending: isCreating, + isSuccess, + } = useMutation({ mutationFn: createRoomFn, onSuccess: (room) => { toast.success(ADD_SUCCESS); @@ -33,7 +37,7 @@ const useCreateRoom = () => { onError: (err) => toast.error(err.message), }); - return { isCreating, createRoom }; + return { isCreating, createRoom, isSuccess }; }; export { useCreateRoom }; diff --git a/hotel-management/src/hooks/rooms/useDeleteRoom.ts b/hotel-management/src/hooks/rooms/useDeleteRoom.ts index 693d3d3..56d3aeb 100644 --- a/hotel-management/src/hooks/rooms/useDeleteRoom.ts +++ b/hotel-management/src/hooks/rooms/useDeleteRoom.ts @@ -14,7 +14,11 @@ import * as messages from '@constant/messages'; const useDeleteRoom = () => { const queryClient = useQueryClient(); - const { isPending: isDeleting, mutate: deleteRoom } = useMutation({ + const { + isPending: isDeleting, + mutate: deleteRoom, + isSuccess + } = useMutation({ mutationFn: deleteRoomFn, onSuccess: () => { toast.success(messages.DELETE_SUCCESS); @@ -25,7 +29,7 @@ const useDeleteRoom = () => { onError: (err) => toast.error(err.message), }); - return { isDeleting, deleteRoom }; + return { isDeleting, deleteRoom, isSuccess }; }; export { useDeleteRoom }; diff --git a/hotel-management/src/hooks/rooms/useRooms.ts b/hotel-management/src/hooks/rooms/useRooms.ts index f6ffc75..84258c8 100644 --- a/hotel-management/src/hooks/rooms/useRooms.ts +++ b/hotel-management/src/hooks/rooms/useRooms.ts @@ -1,34 +1,65 @@ import toast from 'react-hot-toast'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useSearchParams } from 'react-router-dom'; // Services import { getAllRooms } from '@service/roomServices'; +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + /** * Fetch room from database * @returns The status of loading rooms from database and data of rooms */ const useRooms = () => { + const queryClient = useQueryClient(); const [searchParams] = useSearchParams(); + const sortByValue = searchParams.get('sortBy') || 'id'; const orderByValue = searchParams.get('orderBy') || 'asc'; - const phoneSearch = searchParams.get('search') || ''; + const roomSearch = searchParams.get('search') || ''; + const page = searchParams.get('page') + ? Number(searchParams.get('page')) + : 1; const { isLoading, - data: rooms, + data: { data: rooms, count } = {}, error, } = useQuery({ - queryKey: ['rooms', sortByValue, orderByValue, phoneSearch], - queryFn: () => getAllRooms(sortByValue, orderByValue, phoneSearch), + queryKey: ['rooms', sortByValue, orderByValue, roomSearch, page], + queryFn: () => getAllRooms(sortByValue, orderByValue, roomSearch, page), }); if (error) { toast.error(error.message); } - return { isLoading, rooms }; + // Pre-fetching + const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE); + + if (page < totalPage) { + const nextPage = page + 1; + + queryClient.prefetchQuery({ + queryKey: ['rooms', sortByValue, orderByValue, roomSearch, nextPage], + queryFn: () => + getAllRooms(sortByValue, orderByValue, roomSearch, nextPage), + }); + } + + if (page > 1) { + const previousPage = page - 1; + + queryClient.prefetchQuery({ + queryKey: ['rooms', sortByValue, orderByValue, roomSearch, previousPage], + queryFn: () => + getAllRooms(sortByValue, orderByValue, roomSearch, previousPage), + }); + } + + return { isLoading, rooms, count }; }; export { useRooms }; diff --git a/hotel-management/src/hooks/rooms/useUpdateRoom.ts b/hotel-management/src/hooks/rooms/useUpdateRoom.ts index 6b6d6e6..7560074 100644 --- a/hotel-management/src/hooks/rooms/useUpdateRoom.ts +++ b/hotel-management/src/hooks/rooms/useUpdateRoom.ts @@ -18,14 +18,18 @@ const useUpdateRoom = () => { const queryClient = useQueryClient(); const { dispatch } = useUserRoomAvailable(); - const { mutate: updateRoom, isPending: isUpdating } = useMutation({ + const { + mutate: updateRoom, + isPending: isUpdating, + isSuccess + } = useMutation({ mutationFn: updateRoomFn, onSuccess: (room) => { toast.success(UPDATE_SUCCESS); queryClient.invalidateQueries({ queryKey: ['rooms'] }); // Update name room in global state - dispatch!({ + dispatch?.({ type: 'updateRoomName', payload: [{ id: room.id, name: room.name }], }); @@ -33,7 +37,7 @@ const useUpdateRoom = () => { onError: (err) => toast.error(err.message), }); - return { isUpdating, updateRoom }; + return { isUpdating, updateRoom, isSuccess }; }; export { useUpdateRoom }; diff --git a/hotel-management/src/hooks/users/useCreateUser.ts b/hotel-management/src/hooks/users/useCreateUser.ts index 7f89db0..e51c76e 100644 --- a/hotel-management/src/hooks/users/useCreateUser.ts +++ b/hotel-management/src/hooks/users/useCreateUser.ts @@ -18,7 +18,11 @@ const useCreateUser = () => { const queryClient = useQueryClient(); const { dispatch } = useUserRoomAvailable(); - const { mutate: createUser, isPending: isCreating } = useMutation({ + const { + mutate: createUser, + isPending: isCreating, + isSuccess, + } = useMutation({ mutationFn: createUserFn, onSuccess: (user) => { toast.success(ADD_SUCCESS); @@ -33,7 +37,7 @@ const useCreateUser = () => { onError: (err) => toast.error(err.message), }); - return { isCreating, createUser }; + return { isCreating, createUser, isSuccess }; }; export { useCreateUser }; diff --git a/hotel-management/src/hooks/users/useUpdateUser.ts b/hotel-management/src/hooks/users/useUpdateUser.ts index 3c4a029..ec0d2fc 100644 --- a/hotel-management/src/hooks/users/useUpdateUser.ts +++ b/hotel-management/src/hooks/users/useUpdateUser.ts @@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; // Services import { updateUser as updateUserFn } from '@service/userServices'; -// Messages +// Constants import { UPDATE_SUCCESS } from '@constant/messages'; // Hooks @@ -18,7 +18,11 @@ const useUpdateUser = () => { const queryClient = useQueryClient(); const { dispatch } = useUserRoomAvailable(); - const { mutate: updateUser, isPending: isUpdating } = useMutation({ + const { + mutate: updateUser, + isPending: isUpdating, + isSuccess, + } = useMutation({ mutationFn: updateUserFn, onSuccess: (user) => { toast.success(UPDATE_SUCCESS); @@ -33,7 +37,7 @@ const useUpdateUser = () => { onError: (err) => toast.error(err.message), }); - return { isUpdating, updateUser }; + return { isUpdating, updateUser, isSuccess }; }; export { useUpdateUser }; diff --git a/hotel-management/src/hooks/users/useUsers.ts b/hotel-management/src/hooks/users/useUsers.ts index 876aa99..392f9a0 100644 --- a/hotel-management/src/hooks/users/useUsers.ts +++ b/hotel-management/src/hooks/users/useUsers.ts @@ -1,34 +1,64 @@ import toast from 'react-hot-toast'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useSearchParams } from 'react-router-dom'; // Services import { getAllUsers } from '@service/userServices'; +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + /** * Fetch data of users from database * @returns The status of loading users and data of users */ const useUsers = () => { const [searchParams] = useSearchParams(); + const queryClient = useQueryClient(); const sortByValue = searchParams.get('sortBy') || 'id'; const orderByValue = searchParams.get('orderBy') || 'asc'; const phoneSearch = searchParams.get('search') || ''; + const page = searchParams.get('page') + ? Number(searchParams.get('page')) + : 1; const { isLoading, - data: users, + data: { data: users, count } = {}, error, } = useQuery({ - queryKey: ['users', sortByValue, orderByValue, phoneSearch], - queryFn: () => getAllUsers(sortByValue, orderByValue, phoneSearch), + queryKey: ['users', sortByValue, orderByValue, phoneSearch, page], + queryFn: () => getAllUsers(sortByValue, orderByValue, phoneSearch, page), }); if (error) { toast.error(error.message); } - return { isLoading, users }; + // Pre-fetching + const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE); + + if (page < totalPage) { + const nextPage = page + 1; + + queryClient.prefetchQuery({ + queryKey: ['users', sortByValue, orderByValue, phoneSearch, nextPage], + queryFn: () => + getAllUsers(sortByValue, orderByValue, phoneSearch, nextPage), + }); + } + + if (page > 1) { + const previousPage = page - 1; + + queryClient.prefetchQuery({ + queryKey: ['users', sortByValue, orderByValue, phoneSearch, previousPage], + queryFn: () => + getAllUsers(sortByValue, orderByValue, phoneSearch, previousPage), + }); + } + + return { isLoading, users, count }; }; export { useUsers }; diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index ed0a843..c9da185 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -6,6 +6,8 @@ import { IDataState } from '@type/common'; import supabase from './supabaseService'; // Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + const ROOMS_TABLE = 'rooms'; const ERROR_FETCHING = "Can't fetch room data!"; const ERROR_UPDATE_ROOM = "Can't update room!"; @@ -19,11 +21,16 @@ const ERROR_DELETE_ROOM = "Can't delete room!"; const getAllRooms = async ( sortBy: string, orderBy: string, - roomName: string -): Promise => { - const { data, error } = await supabase + roomName: string, + page: number +): Promise<{ data: IRoom[]; count: number | null }> => { + const from = (page - 1) * DEFAULT_PAGE_SIZE; + const to = from + DEFAULT_PAGE_SIZE - 1; + + const { data, error, count } = await supabase .from(ROOMS_TABLE) - .select('*') + .select('*', { count: 'exact' }) + .range(from, to) .order(sortBy, { ascending: orderBy === 'asc' }) .ilike('name', `%${roomName}%`); @@ -32,7 +39,7 @@ const getAllRooms = async ( throw new Error(ERROR_FETCHING); } - return data; + return { data, count }; }; /** diff --git a/hotel-management/src/services/userServices.ts b/hotel-management/src/services/userServices.ts index 62d2fc9..d63e763 100644 --- a/hotel-management/src/services/userServices.ts +++ b/hotel-management/src/services/userServices.ts @@ -5,6 +5,9 @@ import { IDataState } from '@type/common'; // Services import supabase from './supabaseService'; +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + const USERS_TABLE = 'users'; const ERROR_FETCHING = "Users can't be loaded!"; const ERROR_CREATE_USER = "Can't create user!"; @@ -59,11 +62,16 @@ const updateUser = async (user: IUser): Promise => { const getAllUsers = async ( sortBy: string, orderBy: string, - phoneSearch: string -): Promise => { - const { data, error } = await supabase + phoneSearch: string, + page: number +): Promise<{ data: IUser[]; count: number | null }> => { + const from = (page - 1) * DEFAULT_PAGE_SIZE; + const to = from + DEFAULT_PAGE_SIZE - 1; + + const { data, error, count } = await supabase .from(USERS_TABLE) - .select('*') + .select('*', { count: 'exact' }) + .range(from, to) .order(sortBy, { ascending: orderBy === 'asc' }) .ilike('phone', `%${phoneSearch}%`); @@ -72,7 +80,7 @@ const getAllUsers = async ( throw new Error(ERROR_FETCHING); } - return data; + return { data, count }; }; const getUserNotBooked = async (): Promise => { diff --git a/hotel-management/src/styles/abstracts/variables.css b/hotel-management/src/styles/abstracts/variables.css index 8e38691..d9ae533 100644 --- a/hotel-management/src/styles/abstracts/variables.css +++ b/hotel-management/src/styles/abstracts/variables.css @@ -3,6 +3,7 @@ --header-table-color: #ebebeb; --border-color: #8c8c8c; + --footer-table-color: #ebebeb; --secondary-btn-color: #d1d1d1; --disabled-btn-color: #7f82a6; From 4742ff60ce8b7a22bca4a0dd953cb85469183d65 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 15:47:56 +0700 Subject: [PATCH 05/26] Format code style --- hotel-management/src/hooks/rooms/useRooms.ts | 34 +++++++++++++------ hotel-management/src/hooks/users/useUsers.ts | 34 +++++++++++++------ hotel-management/src/services/roomServices.ts | 19 +++++++---- hotel-management/src/services/userServices.ts | 19 +++++++---- 4 files changed, 74 insertions(+), 32 deletions(-) diff --git a/hotel-management/src/hooks/rooms/useRooms.ts b/hotel-management/src/hooks/rooms/useRooms.ts index 84258c8..8e0a56a 100644 --- a/hotel-management/src/hooks/rooms/useRooms.ts +++ b/hotel-management/src/hooks/rooms/useRooms.ts @@ -18,18 +18,22 @@ const useRooms = () => { const sortByValue = searchParams.get('sortBy') || 'id'; const orderByValue = searchParams.get('orderBy') || 'asc'; - const roomSearch = searchParams.get('search') || ''; - const page = searchParams.get('page') - ? Number(searchParams.get('page')) - : 1; + const searchValue = searchParams.get('search') || ''; + const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1; const { isLoading, data: { data: rooms, count } = {}, error, } = useQuery({ - queryKey: ['rooms', sortByValue, orderByValue, roomSearch, page], - queryFn: () => getAllRooms(sortByValue, orderByValue, roomSearch, page), + queryKey: ['rooms', sortByValue, orderByValue, searchValue, page], + queryFn: () => + getAllRooms({ + sortBy: sortByValue, + orderBy: orderByValue, + roomName: searchValue, + page, + }), }); if (error) { @@ -43,9 +47,14 @@ const useRooms = () => { const nextPage = page + 1; queryClient.prefetchQuery({ - queryKey: ['rooms', sortByValue, orderByValue, roomSearch, nextPage], + queryKey: ['rooms', sortByValue, orderByValue, searchValue, nextPage], queryFn: () => - getAllRooms(sortByValue, orderByValue, roomSearch, nextPage), + getAllRooms({ + sortBy: sortByValue, + orderBy: orderByValue, + roomName: searchValue, + page: nextPage, + }), }); } @@ -53,9 +62,14 @@ const useRooms = () => { const previousPage = page - 1; queryClient.prefetchQuery({ - queryKey: ['rooms', sortByValue, orderByValue, roomSearch, previousPage], + queryKey: ['rooms', sortByValue, orderByValue, searchValue, previousPage], queryFn: () => - getAllRooms(sortByValue, orderByValue, roomSearch, previousPage), + getAllRooms({ + sortBy: sortByValue, + orderBy: orderByValue, + roomName: searchValue, + page: previousPage, + }), }); } diff --git a/hotel-management/src/hooks/users/useUsers.ts b/hotel-management/src/hooks/users/useUsers.ts index 392f9a0..e79c4be 100644 --- a/hotel-management/src/hooks/users/useUsers.ts +++ b/hotel-management/src/hooks/users/useUsers.ts @@ -17,18 +17,22 @@ const useUsers = () => { const queryClient = useQueryClient(); const sortByValue = searchParams.get('sortBy') || 'id'; const orderByValue = searchParams.get('orderBy') || 'asc'; - const phoneSearch = searchParams.get('search') || ''; - const page = searchParams.get('page') - ? Number(searchParams.get('page')) - : 1; + const searchValue = searchParams.get('search') || ''; + const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1; const { isLoading, data: { data: users, count } = {}, error, } = useQuery({ - queryKey: ['users', sortByValue, orderByValue, phoneSearch, page], - queryFn: () => getAllUsers(sortByValue, orderByValue, phoneSearch, page), + queryKey: ['users', sortByValue, orderByValue, searchValue, page], + queryFn: () => + getAllUsers({ + sortBy: sortByValue, + orderBy: orderByValue, + phoneSearch: searchValue, + page, + }), }); if (error) { @@ -42,9 +46,14 @@ const useUsers = () => { const nextPage = page + 1; queryClient.prefetchQuery({ - queryKey: ['users', sortByValue, orderByValue, phoneSearch, nextPage], + queryKey: ['users', sortByValue, orderByValue, searchValue, nextPage], queryFn: () => - getAllUsers(sortByValue, orderByValue, phoneSearch, nextPage), + getAllUsers({ + sortBy: sortByValue, + orderBy: orderByValue, + phoneSearch: searchValue, + page: nextPage, + }), }); } @@ -52,9 +61,14 @@ const useUsers = () => { const previousPage = page - 1; queryClient.prefetchQuery({ - queryKey: ['users', sortByValue, orderByValue, phoneSearch, previousPage], + queryKey: ['users', sortByValue, orderByValue, searchValue, previousPage], queryFn: () => - getAllUsers(sortByValue, orderByValue, phoneSearch, previousPage), + getAllUsers({ + sortBy: sortByValue, + orderBy: orderByValue, + phoneSearch: searchValue, + page: previousPage, + }), }); } diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index c9da185..b0fd13e 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -14,16 +14,23 @@ const ERROR_UPDATE_ROOM = "Can't update room!"; const ERROR_CREATE_ROOM = "Can't create room!"; const ERROR_DELETE_ROOM = "Can't delete room!"; +interface IGetAllRooms { + sortBy: string; + orderBy: string; + roomName: string; + page: number; +} + /** * Get all rooms from database * @returns Return all rooms in database */ -const getAllRooms = async ( - sortBy: string, - orderBy: string, - roomName: string, - page: number -): Promise<{ data: IRoom[]; count: number | null }> => { +const getAllRooms = async ({ + sortBy, + orderBy, + roomName, + page, +}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => { const from = (page - 1) * DEFAULT_PAGE_SIZE; const to = from + DEFAULT_PAGE_SIZE - 1; diff --git a/hotel-management/src/services/userServices.ts b/hotel-management/src/services/userServices.ts index d63e763..0a77f62 100644 --- a/hotel-management/src/services/userServices.ts +++ b/hotel-management/src/services/userServices.ts @@ -52,6 +52,13 @@ const updateUser = async (user: IUser): Promise => { return data; }; +interface IGetAllUsers { + sortBy: string; + orderBy: string; + phoneSearch: string; + page: number; +} + /** * Return data of users from database * @param sortBy Sort by column @@ -59,12 +66,12 @@ const updateUser = async (user: IUser): Promise => { * @param phoneSearch The phone need to be search * @returns The data of users from database */ -const getAllUsers = async ( - sortBy: string, - orderBy: string, - phoneSearch: string, - page: number -): Promise<{ data: IUser[]; count: number | null }> => { +const getAllUsers = async ({ + sortBy, + orderBy, + phoneSearch, + page, +}: IGetAllUsers): Promise<{ data: IUser[]; count: number | null }> => { const from = (page - 1) * DEFAULT_PAGE_SIZE; const to = from + DEFAULT_PAGE_SIZE - 1; From 50e607850d3ca1192f0f3b6b36166395e7d6e647 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 16:48:24 +0700 Subject: [PATCH 06/26] Implement pagination for table user and room --- .../src/components/Form/index.tsx | 3 ++- .../src/components/Form/styled.ts | 12 ++++++++++- hotel-management/src/pages/Room/RoomForm.tsx | 21 ++++--------------- hotel-management/src/pages/Room/RoomTable.tsx | 6 +++++- hotel-management/src/pages/User/UserForm.tsx | 11 ++++------ hotel-management/src/pages/User/UserTable.tsx | 6 +++++- hotel-management/src/pages/User/styled.ts | 15 +------------ 7 files changed, 32 insertions(+), 42 deletions(-) diff --git a/hotel-management/src/components/Form/index.tsx b/hotel-management/src/components/Form/index.tsx index c0727ac..6dd2cc3 100644 --- a/hotel-management/src/components/Form/index.tsx +++ b/hotel-management/src/components/Form/index.tsx @@ -7,6 +7,7 @@ import { Error, Label, StyledFormRow, + FormBtn, } from './styled'; interface IFormProps { @@ -29,7 +30,6 @@ const Form = ({ children, onSubmit, id }: IFormProps) => { ); }; - const FormRow = ({ label, error, children }: IFormRow) => { return ( @@ -44,5 +44,6 @@ const FormRow = ({ label, error, children }: IFormRow) => { Form.Row = FormRow; Form.Action = StyledActionBtn; +Form.Button = FormBtn; export default Form; diff --git a/hotel-management/src/components/Form/styled.ts b/hotel-management/src/components/Form/styled.ts index 988dd38..002272c 100644 --- a/hotel-management/src/components/Form/styled.ts +++ b/hotel-management/src/components/Form/styled.ts @@ -1,3 +1,4 @@ +import Button from '@commonStyle/Button'; import styled from 'styled-components'; const StyledForm = styled.form` @@ -35,4 +36,13 @@ const Error = styled.p` width: 220px; `; -export { StyledForm, StyledActionBtn, StyledFormRow, Label, Error }; +const FormBtn = styled(Button)` + width: 100%; + + &:disabled, + &[disabled] { + background-color: var(--disabled-btn-color); + } +`; + +export { StyledForm, StyledActionBtn, StyledFormRow, Label, Error, FormBtn }; diff --git a/hotel-management/src/pages/Room/RoomForm.tsx b/hotel-management/src/pages/Room/RoomForm.tsx index d33bc64..24ba3d1 100644 --- a/hotel-management/src/pages/Room/RoomForm.tsx +++ b/hotel-management/src/pages/Room/RoomForm.tsx @@ -1,7 +1,4 @@ -import styled from 'styled-components'; - // Styled -import Button from '@commonStyle/Button.ts'; import Input from '@commonStyle/Input.ts'; // Types @@ -25,16 +22,6 @@ import { useForm } from 'react-hook-form'; import { useCreateRoom } from '@hook/rooms/useCreateRoom.ts'; import { useUpdateRoom } from '@hook/rooms/useUpdateRoom.ts'; -const FormBtn = styled(Button)` - width: 100%; - - &:disabled, - &[disabled] { - background-color: var(--disabled-btn-color); - cursor: no-drop; - } -`; - interface IRoomFormProp { onCloseModal?: () => void; room?: IRoom; @@ -115,7 +102,7 @@ const RoomForm = ({ onCloseModal, room }: IRoomFormProp) => { - { ? 'Add' : 'Save' } - - + + Close - + ); diff --git a/hotel-management/src/pages/Room/RoomTable.tsx b/hotel-management/src/pages/Room/RoomTable.tsx index daaa726..55796c2 100644 --- a/hotel-management/src/pages/Room/RoomTable.tsx +++ b/hotel-management/src/pages/Room/RoomTable.tsx @@ -22,10 +22,11 @@ import Spinner from '@commonStyle/Spinner'; // Hooks import { useRooms } from '@hook/rooms/useRooms'; +import Pagination from '@component/Pagination'; const RoomTable = () => { const columnName = ['Id', 'Name', 'Price', 'Status']; - const { isLoading, rooms } = useRooms(); + const { isLoading, rooms, count } = useRooms(); const renderRoomRow = useCallback( (room: IRoom) => , @@ -49,6 +50,9 @@ const RoomTable = () => { data={rooms} render={renderRoomRow} /> + + +
) : ( diff --git a/hotel-management/src/pages/User/UserForm.tsx b/hotel-management/src/pages/User/UserForm.tsx index 04b8b68..9ad7634 100644 --- a/hotel-management/src/pages/User/UserForm.tsx +++ b/hotel-management/src/pages/User/UserForm.tsx @@ -19,9 +19,6 @@ import { } from '@constant/formValidateMessage.ts'; import { REGEX } from '@constant/commons.ts'; -// Styled -import { FormBtn } from './styled.ts'; - // Types import { IUser } from '@type/users.ts'; @@ -106,7 +103,7 @@ const UserForm = ({ onCloseModal, user }: IUserFormProp) => { - { ? 'Add' : 'Save' } - - + + Close - + diff --git a/hotel-management/src/pages/User/UserTable.tsx b/hotel-management/src/pages/User/UserTable.tsx index f3179a8..9fc7207 100644 --- a/hotel-management/src/pages/User/UserTable.tsx +++ b/hotel-management/src/pages/User/UserTable.tsx @@ -22,10 +22,11 @@ import Spinner from '@commonStyle/Spinner'; // Hooks import { useUsers } from '@hook/users/useUsers'; +import Pagination from '@component/Pagination'; const UserTable = () => { const columnName = ['Id', 'Name', 'Phone', 'Is Booked']; - const { isLoading, users } = useUsers(); + const { isLoading, users, count } = useUsers(); const renderUserRow = useCallback( (user: IUser) => ( @@ -54,6 +55,9 @@ const UserTable = () => { data={users} render={renderUserRow} /> + + +
) : ( diff --git a/hotel-management/src/pages/User/styled.ts b/hotel-management/src/pages/User/styled.ts index 82229a4..803f1ec 100644 --- a/hotel-management/src/pages/User/styled.ts +++ b/hotel-management/src/pages/User/styled.ts @@ -1,8 +1,5 @@ import styled from 'styled-components'; -// Components -import Button from '../../commons/styles/Button'; - const StyledUser = styled.main` padding: 20px; padding-bottom: 100px; @@ -24,14 +21,4 @@ const StyledOperationTable = styled.div` justify-content: flex-end; `; -const FormBtn = styled(Button)` - width: 100%; - - &:disabled, - &[disabled] { - background-color: var(--disabled-btn-color); - cursor: no-drop; - } -`; - -export { StyledUser, Title, StyledOperationTable, FormBtn }; +export { StyledUser, Title, StyledOperationTable }; From cfe2cb72480bf2b357da02c33bb239c9da04530b Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 16:54:37 +0700 Subject: [PATCH 07/26] Addition comments section --- hotel-management/src/components/Form/styled.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hotel-management/src/components/Form/styled.ts b/hotel-management/src/components/Form/styled.ts index 002272c..08d47ed 100644 --- a/hotel-management/src/components/Form/styled.ts +++ b/hotel-management/src/components/Form/styled.ts @@ -1,6 +1,8 @@ -import Button from '@commonStyle/Button'; import styled from 'styled-components'; +// Styled +import Button from '@commonStyle/Button'; + const StyledForm = styled.form` display: flex; flex-direction: column; From 3d5da85fdb16f05bb3b4e838c07a286db83b4d87 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 16:57:29 +0700 Subject: [PATCH 08/26] Fix test case --- hotel-management/src/components/Form/Form.test.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/hotel-management/src/components/Form/Form.test.tsx b/hotel-management/src/components/Form/Form.test.tsx index 795c518..7e248e6 100644 --- a/hotel-management/src/components/Form/Form.test.tsx +++ b/hotel-management/src/components/Form/Form.test.tsx @@ -4,9 +4,6 @@ import renderer from 'react-test-renderer'; import Form from '.'; import Input from '@commonStyle/Input'; -// Styled -import { FormBtn } from '@page/User/styled'; - describe('Form ', () => { const isDisable = true; const errorMessage = 'This is error message'; @@ -24,12 +21,12 @@ describe('Form ', () => { - + Confirm - - + + Close - + ); From b8281c2dc584651c83a6ae756829f3ef41262261 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 17:11:53 +0700 Subject: [PATCH 09/26] Update common files --- hotel-management/.gitignore | 2 - hotel-management/src/commons/styles/Button.ts | 4 +- hotel-management/src/commons/styles/Input.ts | 3 +- hotel-management/src/constants/messages.ts | 48 +++++++++++++++++-- hotel-management/src/constants/path.ts | 4 +- .../src/contexts/UserRoomAvailableContext.ts | 35 +++++++------- hotel-management/src/types/booking.ts | 27 +++++++++++ hotel-management/src/types/common.ts | 4 +- 8 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 hotel-management/src/types/booking.ts diff --git a/hotel-management/.gitignore b/hotel-management/.gitignore index 13549f4..36e326e 100644 --- a/hotel-management/.gitignore +++ b/hotel-management/.gitignore @@ -16,8 +16,6 @@ coverage coverage .jest -src/db.json - # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/hotel-management/src/commons/styles/Button.ts b/hotel-management/src/commons/styles/Button.ts index 00c8db1..ad82be0 100644 --- a/hotel-management/src/commons/styles/Button.ts +++ b/hotel-management/src/commons/styles/Button.ts @@ -16,10 +16,10 @@ const variations: IVariations = { background-color: var(--danger-btn-color); color: var(--light-text); `, -} as const; +}; interface IButtonStyle { - variations?: keyof typeof variations; + variations?: keyof IVariations; } const Button = styled.button` diff --git a/hotel-management/src/commons/styles/Input.ts b/hotel-management/src/commons/styles/Input.ts index 32e5eb2..bd746fa 100644 --- a/hotel-management/src/commons/styles/Input.ts +++ b/hotel-management/src/commons/styles/Input.ts @@ -3,7 +3,7 @@ import styled, { css } from 'styled-components'; // Styled import CommonInput from './CommonInput'; -type TInput = 'text' | 'checkbox' | 'hidden'; +type TInput = 'text' | 'checkbox' | 'hidden' | 'date'; interface IInputTyped { type?: TInput; @@ -11,6 +11,7 @@ interface IInputTyped { const Input = styled.input` ${CommonInput} + width: 160px; ${(props) => props.type === 'checkbox' && diff --git a/hotel-management/src/constants/messages.ts b/hotel-management/src/constants/messages.ts index 73342ea..b8fefdd 100644 --- a/hotel-management/src/constants/messages.ts +++ b/hotel-management/src/constants/messages.ts @@ -1,9 +1,31 @@ -const ADD_SUCCESS = 'Add success'; -const UPDATE_SUCCESS = 'Update success'; +const ADD_SUCCESS = 'Add success!'; +const UPDATE_SUCCESS = 'Update success!'; const CONFIRM_MESSAGE = 'Do you want to checkout this user?'; -const CONFIRM_DELETE = 'Are you sure to delete it?' -const DELETE_SUCCESS = 'Delete success'; -const CHECKOUT_SUCCESS = 'Check out success'; +const CONFIRM_DELETE = 'Are you sure to delete it?'; +const DELETE_SUCCESS = 'Delete success!'; + +// Booking connection messages +const BOOKINGS_TABLE = 'bookings'; +const ERROR_FETCHING_BOOKING = "Can't fetch booking data!"; +const ERROR_UPDATE_BOOKING = "Can't update booking!"; +const ERROR_CREATE_BOOKING = "Can't create booking!"; +const ERROR_DELETE_BOOKING = "Can't delete booking!"; +const CHECKOUT_SUCCESS = 'Check out success!'; +const ERROR_CHECKOUT = "Can't checkout!"; + +// Room connection messages +const ROOMS_TABLE = 'rooms'; +const ERROR_FETCHING_ROOM = "Can't fetch room data!"; +const ERROR_UPDATE_ROOM = "Can't update room!"; +const ERROR_CREATE_ROOM = "Can't create room!"; +const ERROR_DELETE_ROOM = "Can't delete room!"; + +// User connection messages +const USERS_TABLE = 'users'; +const ERROR_FETCHING_USER = "Users can't be loaded!"; +const ERROR_CREATE_USER = "Can't create user!"; +const ERROR_UPDATE_USER = "Can't update user!"; +const ERROR_DELETE_USER = "Can't delete user!"; export { ADD_SUCCESS, @@ -12,4 +34,20 @@ export { CONFIRM_DELETE, DELETE_SUCCESS, CHECKOUT_SUCCESS, + ERROR_CHECKOUT, + BOOKINGS_TABLE, + ERROR_FETCHING_BOOKING, + ERROR_UPDATE_BOOKING, + ERROR_CREATE_BOOKING, + ERROR_DELETE_BOOKING, + ROOMS_TABLE, + ERROR_FETCHING_ROOM, + ERROR_UPDATE_ROOM, + ERROR_DELETE_ROOM, + ERROR_CREATE_ROOM, + USERS_TABLE, + ERROR_FETCHING_USER, + ERROR_CREATE_USER, + ERROR_UPDATE_USER, + ERROR_DELETE_USER, }; diff --git a/hotel-management/src/constants/path.ts b/hotel-management/src/constants/path.ts index c96883d..a83803e 100644 --- a/hotel-management/src/constants/path.ts +++ b/hotel-management/src/constants/path.ts @@ -1,4 +1,6 @@ -export const DASHBOARD = '/dashboard'; +export const BOOKING = '/booking'; export const USER = '/user'; export const ROOM = '/room'; export const OTHER_PATH = '*'; +export const USER_PATH = 'users'; +export const ROOM_PATH = 'rooms'; diff --git a/hotel-management/src/contexts/UserRoomAvailableContext.ts b/hotel-management/src/contexts/UserRoomAvailableContext.ts index 17371d3..13c5e28 100644 --- a/hotel-management/src/contexts/UserRoomAvailableContext.ts +++ b/hotel-management/src/contexts/UserRoomAvailableContext.ts @@ -12,9 +12,9 @@ type TAction = | 'initRoom' | 'initUser' | 'addRoom' - | 'removeRoom' + | 'updateStatusRoom' | 'addUser' - | 'removeUser' + | 'updateStatusUser' | 'updateUserName' | 'updateRoomName'; @@ -38,19 +38,16 @@ const initialState: IUserRoomState = { const reducer = (state: IUserRoomState, action: IAction) => { switch (action.type) { - case 'initRoom': return { ...state, roomsAvailable: action.payload, }; - case 'initUser': return { ...state, usersAvailable: action.payload, }; - case 'addUser': { const tempArr = state.usersAvailable; const itemExist = state.usersAvailable.find( @@ -66,7 +63,6 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } - case 'updateUserName': { const tempArr = state.usersAvailable; @@ -83,15 +79,18 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } + case 'updateStatusUser': { + const tempArr = state.usersAvailable; + const indexItemUpdate = tempArr.findIndex( + (item) => item.id === action.payload[0].id + ); + tempArr[indexItemUpdate].isBooked = action.payload[0].isBooked; - case 'removeUser': return { ...state, - usersAvailable: state.usersAvailable.filter( - (item) => item.id !== action.payload[0].id - ), + usersAvailable: tempArr, }; - + } case 'addRoom': { const tempArr = state.roomsAvailable; const itemExist = state.roomsAvailable.find( @@ -107,7 +106,6 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } - case 'updateRoomName': { const tempArr = state.roomsAvailable; @@ -124,15 +122,18 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } + case 'updateStatusRoom': { + const tempArr = state.roomsAvailable; + const indexItemUpdate = tempArr.findIndex( + (item) => item.id === action.payload[0].id + ); + tempArr[indexItemUpdate].status = action.payload[0].status; - case 'removeRoom': return { ...state, - roomsAvailable: state.roomsAvailable.filter( - (item) => item.id !== action.payload[0].id - ), + roomsAvailable: tempArr, }; - + } default: throw new Error('Action unknown'); } diff --git a/hotel-management/src/types/booking.ts b/hotel-management/src/types/booking.ts new file mode 100644 index 0000000..460b999 --- /dev/null +++ b/hotel-management/src/types/booking.ts @@ -0,0 +1,27 @@ +interface IBooking { + id: number; + startDate: string; + endDate: string; + status: boolean; + amount: number; + roomId: number; + userId: number; +} + +interface TBookingResponse { + id: number; + startDate: string; + endDate: string; + status: boolean; + amount: number; + rooms: { + id: number + name: string; + } | null; + users: { + id: number; + name: string; + } | null; +} + +export type { IBooking, TBookingResponse }; diff --git a/hotel-management/src/types/common.ts b/hotel-management/src/types/common.ts index 825c8f3..7445679 100644 --- a/hotel-management/src/types/common.ts +++ b/hotel-management/src/types/common.ts @@ -2,7 +2,9 @@ type Nullable = T | null; interface IDataState { id: number; - name: string; + name?: string; + status?: boolean; + isBooked?: boolean; } export type { Nullable, IDataState }; From 94508923c58ffb5f089193828aae164a206870dc Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 17:14:37 +0700 Subject: [PATCH 10/26] Format code style --- hotel-management/src/contexts/UserRoomAvailableContext.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hotel-management/src/contexts/UserRoomAvailableContext.ts b/hotel-management/src/contexts/UserRoomAvailableContext.ts index 13c5e28..2c0d36f 100644 --- a/hotel-management/src/contexts/UserRoomAvailableContext.ts +++ b/hotel-management/src/contexts/UserRoomAvailableContext.ts @@ -43,11 +43,13 @@ const reducer = (state: IUserRoomState, action: IAction) => { ...state, roomsAvailable: action.payload, }; + case 'initUser': return { ...state, usersAvailable: action.payload, }; + case 'addUser': { const tempArr = state.usersAvailable; const itemExist = state.usersAvailable.find( @@ -63,6 +65,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } + case 'updateUserName': { const tempArr = state.usersAvailable; @@ -79,6 +82,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } + case 'updateStatusUser': { const tempArr = state.usersAvailable; const indexItemUpdate = tempArr.findIndex( @@ -91,6 +95,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } + case 'addRoom': { const tempArr = state.roomsAvailable; const itemExist = state.roomsAvailable.find( @@ -106,6 +111,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } + case 'updateRoomName': { const tempArr = state.roomsAvailable; @@ -122,6 +128,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } + case 'updateStatusRoom': { const tempArr = state.roomsAvailable; const indexItemUpdate = tempArr.findIndex( @@ -134,6 +141,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } + default: throw new Error('Action unknown'); } From 139db982833e3cc3bfcec5634eae77481a945766 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 29 Nov 2023 19:33:37 +0700 Subject: [PATCH 11/26] Update code, create booking services --- .../ConfirmMessage/ConfirmMessage.test.tsx | 4 +- .../src/components/ConfirmMessage/index.tsx | 22 +-- .../src/components/ConfirmMessage/styled.ts | 2 +- .../src/components/Form/styled.ts | 2 - hotel-management/src/components/Nav/index.tsx | 10 +- .../src/components/Select/index.tsx | 3 + .../src/components/Select/styled.ts | 2 + hotel-management/src/constants/commons.ts | 1 + .../src/contexts/UserRoomAvailableContext.ts | 2 +- hotel-management/src/helpers/helper.ts | 14 +- .../src/services/bookingServices.ts | 150 ++++++++++++++++++ hotel-management/src/services/roomServices.ts | 79 +++++++-- hotel-management/src/services/userServices.ts | 75 +++++++-- 13 files changed, 309 insertions(+), 57 deletions(-) create mode 100644 hotel-management/src/services/bookingServices.ts diff --git a/hotel-management/src/components/ConfirmMessage/ConfirmMessage.test.tsx b/hotel-management/src/components/ConfirmMessage/ConfirmMessage.test.tsx index 233ce05..afcaf70 100644 --- a/hotel-management/src/components/ConfirmMessage/ConfirmMessage.test.tsx +++ b/hotel-management/src/components/ConfirmMessage/ConfirmMessage.test.tsx @@ -3,14 +3,12 @@ import renderer from 'react-test-renderer'; // Components import ConfirmMessage from '.'; -describe('ConfirmMessage', () => { - const isDisable = true; +describe('ConfirmMessage testing snapshot', () => { const message = 'Hello World!'; const handleOnConfirm = jest.fn(); const wrapper = renderer.create( diff --git a/hotel-management/src/components/ConfirmMessage/index.tsx b/hotel-management/src/components/ConfirmMessage/index.tsx index ce9b911..8ea6d85 100644 --- a/hotel-management/src/components/ConfirmMessage/index.tsx +++ b/hotel-management/src/components/ConfirmMessage/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useCallback } from 'react'; // Styled import { StyledConfirmDelete } from './styled'; @@ -8,27 +8,27 @@ import Button from '@commonStyle/Button'; interface IConfirmMessage { message: string; - disabled: boolean; onConfirm: () => void; onCloseModal?: () => void; } const ConfirmMessage = memo( - ({ message, disabled, onConfirm, onCloseModal }: IConfirmMessage) => { + ({ message, onConfirm, onCloseModal }: IConfirmMessage) => { + const handleConfirmBtn = useCallback(() => { + onConfirm(); + onCloseModal!(); + }, [onCloseModal, onConfirm]); + return (

{message}

- -
diff --git a/hotel-management/src/components/ConfirmMessage/styled.ts b/hotel-management/src/components/ConfirmMessage/styled.ts index 47590e1..5a772a5 100644 --- a/hotel-management/src/components/ConfirmMessage/styled.ts +++ b/hotel-management/src/components/ConfirmMessage/styled.ts @@ -13,7 +13,7 @@ const StyledConfirmDelete = styled.div` display: flex; justify-content: space-around; - padding-inline: 50px; + padding-inline: 100px; } `; diff --git a/hotel-management/src/components/Form/styled.ts b/hotel-management/src/components/Form/styled.ts index 08d47ed..01ed87f 100644 --- a/hotel-management/src/components/Form/styled.ts +++ b/hotel-management/src/components/Form/styled.ts @@ -34,8 +34,6 @@ const Error = styled.p` margin-top: 5px; padding-inline: 5px; - - width: 220px; `; const FormBtn = styled(Button)` diff --git a/hotel-management/src/components/Nav/index.tsx b/hotel-management/src/components/Nav/index.tsx index 7e0e452..b5bb011 100644 --- a/hotel-management/src/components/Nav/index.tsx +++ b/hotel-management/src/components/Nav/index.tsx @@ -1,5 +1,5 @@ // Components -import { MdSpaceDashboard } from 'react-icons/md'; +import { BiSolidCalendarAlt } from 'react-icons/bi'; import { HiUsers } from 'react-icons/hi2'; import { BsHouseDoorFill } from 'react-icons/bs'; @@ -7,14 +7,14 @@ import { BsHouseDoorFill } from 'react-icons/bs'; import { StyledNav, StyledNavLink } from './styled'; // Constants -import { DASHBOARD, ROOM, USER } from '@constant/path'; +import { BOOKING, ROOM, USER } from '@constant/path'; const Nav = () => { return ( - - - Dashboard + + + Booking diff --git a/hotel-management/src/components/Select/index.tsx b/hotel-management/src/components/Select/index.tsx index 720c0f0..622d80e 100644 --- a/hotel-management/src/components/Select/index.tsx +++ b/hotel-management/src/components/Select/index.tsx @@ -19,6 +19,7 @@ interface ISelect { onChange?: ChangeEventHandler | undefined; id?: string; ariaLabel: string; + disable?: boolean; } const Select = ({ @@ -28,6 +29,7 @@ const Select = ({ value, onChange, ariaLabel, + disable = false, }: ISelect) => { const { register } = useFormContext() ?? {}; @@ -38,6 +40,7 @@ const Select = ({ aria-label={ariaLabel} id={id} value={value} + disabled={disable} {...(register ? { ...register(id!, optionsConfigForm) } : { onChange: onChange })} diff --git a/hotel-management/src/components/Select/styled.ts b/hotel-management/src/components/Select/styled.ts index 93b089c..92ee349 100644 --- a/hotel-management/src/components/Select/styled.ts +++ b/hotel-management/src/components/Select/styled.ts @@ -6,6 +6,8 @@ const StyledSelect = styled.select` border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-weight: 500; + + width: 200px; `; export { StyledSelect }; diff --git a/hotel-management/src/constants/commons.ts b/hotel-management/src/constants/commons.ts index 9e62af2..fc82f1f 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', + CHECKOUT: 'checkout', }; const REGEX = { diff --git a/hotel-management/src/contexts/UserRoomAvailableContext.ts b/hotel-management/src/contexts/UserRoomAvailableContext.ts index 2c0d36f..d7e3bed 100644 --- a/hotel-management/src/contexts/UserRoomAvailableContext.ts +++ b/hotel-management/src/contexts/UserRoomAvailableContext.ts @@ -82,7 +82,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } - + case 'updateStatusUser': { const tempArr = state.usersAvailable; const indexItemUpdate = tempArr.findIndex( diff --git a/hotel-management/src/helpers/helper.ts b/hotel-management/src/helpers/helper.ts index 51e74f9..5f88c70 100644 --- a/hotel-management/src/helpers/helper.ts +++ b/hotel-management/src/helpers/helper.ts @@ -10,4 +10,16 @@ const formatCurrency = (value: number): string => { }).format(value); }; -export { formatCurrency }; +const getDayDiff = (startDate: Date, endDate: Date): number => { + const msInDay = 24 * 60 * 60 * 1000; + + return Math.round( + Math.abs(Number(endDate) - Number(startDate)) / msInDay + ); +} + +const convertCurrencyToNumber = (currency: string) => { + return Number(currency.replace(/[^0-9.-]+/g,"")) +} + +export { formatCurrency, getDayDiff, convertCurrencyToNumber }; diff --git a/hotel-management/src/services/bookingServices.ts b/hotel-management/src/services/bookingServices.ts new file mode 100644 index 0000000..68b719f --- /dev/null +++ b/hotel-management/src/services/bookingServices.ts @@ -0,0 +1,150 @@ +// Types +import { IBooking, TBookingResponse } from '@type/booking'; + +// Services +import supabase from './supabaseService'; + +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; +import { + BOOKINGS_TABLE, + ERROR_CHECKOUT, + ERROR_CREATE_BOOKING, + ERROR_DELETE_BOOKING, + ERROR_FETCHING_BOOKING, + ERROR_UPDATE_BOOKING, +} from '@constant/messages'; +import { updateRoomStatus } from './roomServices'; +import { updateUserBookedStatus } from './userServices'; + +interface ICheckOutBooking { + idBooking: number; + roomId: number; + userId: number; +} + +interface IGetAllBookings { + userNameSearch: string; + page: number; +} + +/** + * Get all bookings from database + * @returns Return all bookings in database + */ +const getAllBookings = async ({ + userNameSearch, + page, +}: IGetAllBookings): Promise<{ + data: TBookingResponse[]; + count: number | null; +}> => { + const from = (page - 1) * DEFAULT_PAGE_SIZE; + const to = from + DEFAULT_PAGE_SIZE - 1; + + const { data, error, count } = await supabase + .from(BOOKINGS_TABLE) + .select( + 'id, startDate, endDate, status, amount, rooms(id, name), users!inner(id, name)', + { + count: 'exact', + } + ) + .ilike('users.name', `%${userNameSearch}%`) + .range(from, to); + + if (error) { + console.error(error.message); + throw new Error(ERROR_FETCHING_BOOKING); + } + + return { data, count }; +}; + +/** + * Update booking into database + * @param booking Booking object need to be updated + */ +const updateBooking = async (booking: IBooking): Promise => { + const { data, error } = await supabase + .from(BOOKINGS_TABLE) + .update(booking) + .eq('id', booking.id) + .select() + .single(); + + if (error) { + console.error(error.message); + throw new Error(ERROR_UPDATE_BOOKING); + } + + return data; +}; + +/** + * Add room to database + * @param room The room object need to be add + */ +const createBooking = async (booking: IBooking): Promise => { + const { data, error: createBookingError } = await supabase + .from(BOOKINGS_TABLE) + .insert(booking) + .select() + .single(); + + if (createBookingError) { + console.error(createBookingError.message); + throw new Error(ERROR_CREATE_BOOKING); + } + + return data; +}; + +/** + * Delete room in database + * @param idRoom The id of room need to delete + */ +const deleteBooking = async (idBooking: number) => { + const { error } = await supabase + .from(BOOKINGS_TABLE) + .delete() + .eq('id', idBooking); + + if (error) { + console.error(error.message); + throw new Error(ERROR_DELETE_BOOKING); + } +}; + +const checkOutBooking = async ({ + idBooking, + roomId, + userId, +}: ICheckOutBooking) => { + // Update status booking + const { data, error } = await supabase + .from(BOOKINGS_TABLE) + .update({ status: false }) + .eq('id', idBooking) + .select() + .single(); + + if (error) { + console.error(error.message); + throw new Error(ERROR_CHECKOUT); + } + + // Update status room, user + await updateRoomStatus(roomId, false); + await updateUserBookedStatus(userId, false); + + return data; +}; + +export { + createBooking, + getAllBookings, + updateBooking, + deleteBooking, + checkOutBooking, +}; diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index b0fd13e..ac08893 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -7,17 +7,18 @@ import supabase from './supabaseService'; // Constants import { DEFAULT_PAGE_SIZE } from '@constant/config'; - -const ROOMS_TABLE = 'rooms'; -const ERROR_FETCHING = "Can't fetch room data!"; -const ERROR_UPDATE_ROOM = "Can't update room!"; -const ERROR_CREATE_ROOM = "Can't create room!"; -const ERROR_DELETE_ROOM = "Can't delete room!"; +import { + ERROR_CREATE_ROOM, + ERROR_DELETE_ROOM, + ERROR_FETCHING_ROOM, + ERROR_UPDATE_ROOM, + ROOMS_TABLE, +} from '@constant/messages'; interface IGetAllRooms { sortBy: string; orderBy: string; - roomName: string; + roomSearch: string; page: number; } @@ -28,22 +29,27 @@ interface IGetAllRooms { const getAllRooms = async ({ sortBy, orderBy, - roomName, + roomSearch, page, }: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => { const from = (page - 1) * DEFAULT_PAGE_SIZE; const to = from + DEFAULT_PAGE_SIZE - 1; - const { data, error, count } = await supabase + let query = supabase .from(ROOMS_TABLE) .select('*', { count: 'exact' }) - .range(from, to) .order(sortBy, { ascending: orderBy === 'asc' }) - .ilike('name', `%${roomName}%`); + .like('name', `%${roomSearch}%`); + + if(page) { + query = query.range(from, to); + } + + const { data, error, count } = await query; if (error) { console.error(error.message); - throw new Error(ERROR_FETCHING); + throw new Error(ERROR_FETCHING_ROOM); } return { data, count }; @@ -101,18 +107,57 @@ const deleteRoom = async (idRoom: number) => { } }; -const getRoomsAvailable = async (): Promise => { +const getRoomById = async (idRoom: string): Promise => { const { data, error } = await supabase .from(ROOMS_TABLE) - .select('id, name') - .eq('status', false); + .select('*') + .eq('id', idRoom) + .single(); if (error) { console.error(error.message); - throw new Error(ERROR_FETCHING); + throw new Error(ERROR_FETCHING_ROOM); } return data; }; -export { getAllRooms, updateRoom, createRoom, deleteRoom, getRoomsAvailable }; +const getRoomsAvailable = async (): Promise => { + const { data, error } = await supabase + .from(ROOMS_TABLE) + .select('id, name, status'); + + if (error) { + console.error(error.message); + throw new Error(ERROR_FETCHING_ROOM); + } + + return data; +}; + +const updateRoomStatus = async ( + id: number, + status: boolean +): Promise => { + const { error, status: statusConnection } = await supabase + .from(ROOMS_TABLE) + .update({ status }) + .eq('id', id); + + if (error) { + console.error(error.message); + throw new Error(ERROR_UPDATE_ROOM); + } + + return statusConnection; +}; + +export { + getAllRooms, + updateRoom, + createRoom, + deleteRoom, + getRoomsAvailable, + getRoomById, + updateRoomStatus, +}; diff --git a/hotel-management/src/services/userServices.ts b/hotel-management/src/services/userServices.ts index 0a77f62..e203f27 100644 --- a/hotel-management/src/services/userServices.ts +++ b/hotel-management/src/services/userServices.ts @@ -1,17 +1,17 @@ // Types import { IUser } from '@type/users'; -import { IDataState } from '@type/common'; // Services import supabase from './supabaseService'; - -// Constants +import { IDataState } from '@type/common'; import { DEFAULT_PAGE_SIZE } from '@constant/config'; - -const USERS_TABLE = 'users'; -const ERROR_FETCHING = "Users can't be loaded!"; -const ERROR_CREATE_USER = "Can't create user!"; -const ERROR_UPDATE_USER = "Can't update user!"; +import { + ERROR_CREATE_USER, + ERROR_DELETE_USER, + ERROR_FETCHING_USER, + ERROR_UPDATE_USER, + USERS_TABLE, +} from '@constant/messages'; /** * Create user to the database @@ -24,6 +24,8 @@ const createUser = async (user: IUser): Promise => { .select() .single(); + console.log('Create'); + if (error) { console.error(error.message); throw new Error(ERROR_CREATE_USER); @@ -52,6 +54,19 @@ const updateUser = async (user: IUser): Promise => { return data; }; +/** + * Delete user in database + * @param idUser The id of user need to delete + */ +const deleteUser = async (idUser: number) => { + const { error } = await supabase.from('users').delete().eq('id', idUser); + + if (error) { + console.error(error.message); + throw new Error(ERROR_DELETE_USER); + } +}; + interface IGetAllUsers { sortBy: string; orderBy: string; @@ -75,16 +90,21 @@ const getAllUsers = async ({ const from = (page - 1) * DEFAULT_PAGE_SIZE; const to = from + DEFAULT_PAGE_SIZE - 1; - const { data, error, count } = await supabase + let query = supabase .from(USERS_TABLE) .select('*', { count: 'exact' }) - .range(from, to) .order(sortBy, { ascending: orderBy === 'asc' }) - .ilike('phone', `%${phoneSearch}%`); + .like('phone', `%${phoneSearch}%`); + + if (page) { + query = query.range(from, to); + } + + const { data, error, count } = await query; if (error) { console.error(error.message); - throw new Error(ERROR_FETCHING); + throw new Error(ERROR_FETCHING_USER); } return { data, count }; @@ -93,15 +113,38 @@ const getAllUsers = async ({ const getUserNotBooked = async (): Promise => { const { data, error } = await supabase .from(USERS_TABLE) - .select('id, name') - .eq('isBooked', false); + .select('id, name, isBooked'); if (error) { console.error(error.message); - throw new Error(ERROR_FETCHING); + throw new Error(ERROR_FETCHING_USER); } return data; }; -export { updateUser, createUser, getAllUsers, getUserNotBooked }; +const updateUserBookedStatus = async ( + id: number, + isBooked: boolean +): Promise => { + const { error, status } = await supabase + .from(USERS_TABLE) + .update({ isBooked }) + .eq('id', id); + + if (error) { + console.error(error.message); + throw new Error(ERROR_UPDATE_USER); + } + + return status; +}; + +export { + updateUser, + createUser, + getAllUsers, + deleteUser, + getUserNotBooked, + updateUserBookedStatus, +}; From 67c3764864099461b77aae0ce408a8a27763628c Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 08:44:27 +0700 Subject: [PATCH 12/26] Update section comments --- hotel-management/src/helpers/helper.ts | 12 ++++++++++++ hotel-management/src/services/bookingServices.ts | 13 +++++++++---- hotel-management/src/services/roomServices.ts | 9 +++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/hotel-management/src/helpers/helper.ts b/hotel-management/src/helpers/helper.ts index 5f88c70..9aae61c 100644 --- a/hotel-management/src/helpers/helper.ts +++ b/hotel-management/src/helpers/helper.ts @@ -10,6 +10,13 @@ const formatCurrency = (value: number): string => { }).format(value); }; + +/** + * Calculate the day durations + * @param startDate The start of date + * @param endDate The end of date + * @returns The number of days between 2 date input + */ const getDayDiff = (startDate: Date, endDate: Date): number => { const msInDay = 24 * 60 * 60 * 1000; @@ -18,6 +25,11 @@ const getDayDiff = (startDate: Date, endDate: Date): number => { ); } +/** + * Convert from currency string to the number + * @param currency The currency string + * @returns The number + */ const convertCurrencyToNumber = (currency: string) => { return Number(currency.replace(/[^0-9.-]+/g,"")) } diff --git a/hotel-management/src/services/bookingServices.ts b/hotel-management/src/services/bookingServices.ts index 68b719f..b8cac0e 100644 --- a/hotel-management/src/services/bookingServices.ts +++ b/hotel-management/src/services/bookingServices.ts @@ -82,8 +82,8 @@ const updateBooking = async (booking: IBooking): Promise => { }; /** - * Add room to database - * @param room The room object need to be add + * Add booking to database + * @param booking The booking object need to be add */ const createBooking = async (booking: IBooking): Promise => { const { data, error: createBookingError } = await supabase @@ -101,8 +101,8 @@ const createBooking = async (booking: IBooking): Promise => { }; /** - * Delete room in database - * @param idRoom The id of room need to delete + * Delete booking in database + * @param idRoom The id of booking need to delete */ const deleteBooking = async (idBooking: number) => { const { error } = await supabase @@ -116,6 +116,11 @@ const deleteBooking = async (idBooking: number) => { } }; +/** + * Check out booking services + * @param param0 The ICheckOutBooking object + * @returns The data of booking after insert to database + */ const checkOutBooking = async ({ idBooking, roomId, diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index ac08893..e660376 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -107,6 +107,11 @@ const deleteRoom = async (idRoom: number) => { } }; +/** + * Get room by id in database + * @param idRoom The id room need to be get + * @returns The data of room + */ const getRoomById = async (idRoom: string): Promise => { const { data, error } = await supabase .from(ROOMS_TABLE) @@ -122,6 +127,10 @@ const getRoomById = async (idRoom: string): Promise => { return data; }; +/** + * + * @returns + */ const getRoomsAvailable = async (): Promise => { const { data, error } = await supabase .from(ROOMS_TABLE) From 350857c106ef8db992ab262b954e99dd72ce4f8c Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 09:01:15 +0700 Subject: [PATCH 13/26] Format code --- hotel-management/src/contexts/UserRoomAvailableContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hotel-management/src/contexts/UserRoomAvailableContext.ts b/hotel-management/src/contexts/UserRoomAvailableContext.ts index d7e3bed..36ef21b 100644 --- a/hotel-management/src/contexts/UserRoomAvailableContext.ts +++ b/hotel-management/src/contexts/UserRoomAvailableContext.ts @@ -82,7 +82,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { usersAvailable: tempArr, }; } - + case 'updateStatusUser': { const tempArr = state.usersAvailable; const indexItemUpdate = tempArr.findIndex( @@ -141,7 +141,7 @@ const reducer = (state: IUserRoomState, action: IAction) => { roomsAvailable: tempArr, }; } - + default: throw new Error('Action unknown'); } From 52cc76d66404c054b3ab7d55bdc01bea7faed87e Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 09:17:16 +0700 Subject: [PATCH 14/26] Create booking hooks and update room, user hooks --- .../src/hooks/bookings/useBookings.ts | 71 +++++++++++++++++++ .../src/hooks/bookings/useCheckout.ts | 43 +++++++++++ .../src/hooks/bookings/useCreateBooking.ts | 33 +++++++++ .../src/hooks/bookings/useDeleteBooking.ts | 35 +++++++++ .../src/hooks/bookings/useUpdateBooking.ts | 33 +++++++++ hotel-management/src/hooks/rooms/useRooms.ts | 6 +- .../src/hooks/users/useDeleteUser.ts | 35 +++++++++ 7 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 hotel-management/src/hooks/bookings/useBookings.ts create mode 100644 hotel-management/src/hooks/bookings/useCheckout.ts create mode 100644 hotel-management/src/hooks/bookings/useCreateBooking.ts create mode 100644 hotel-management/src/hooks/bookings/useDeleteBooking.ts create mode 100644 hotel-management/src/hooks/bookings/useUpdateBooking.ts create mode 100644 hotel-management/src/hooks/users/useDeleteUser.ts diff --git a/hotel-management/src/hooks/bookings/useBookings.ts b/hotel-management/src/hooks/bookings/useBookings.ts new file mode 100644 index 0000000..99473dc --- /dev/null +++ b/hotel-management/src/hooks/bookings/useBookings.ts @@ -0,0 +1,71 @@ +import toast from 'react-hot-toast'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useSearchParams } from 'react-router-dom'; + +// Services +import { getAllBookings } from '@service/bookingServices'; + +// Constants +import { DEFAULT_PAGE_SIZE } from '@constant/config'; + +/** + * Fetch booking from database + * @returns The status of loading bookings from database and data of bookings + */ +const useBookings = () => { + const queryClient = useQueryClient(); + const [searchParams] = useSearchParams(); + + const searchValue = searchParams.get('search') || ''; + const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1; + + const { + isLoading, + data: { data: bookings, count } = {}, + error, + } = useQuery({ + queryKey: ['bookings', searchValue, page], + queryFn: () => + getAllBookings({ + userNameSearch: searchValue, + page, + }), + }); + + if (error) { + toast.error(error.message); + } + + // Pre-fetching + const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE); + + if (page < totalPage) { + const nextPage = page + 1; + + queryClient.prefetchQuery({ + queryKey: ['bookings', searchValue, nextPage], + queryFn: () => + getAllBookings({ + userNameSearch: searchValue, + page: nextPage, + }), + }); + } + + if (page > 1) { + const previousPage = page - 1; + + queryClient.prefetchQuery({ + queryKey: ['bookings', searchValue, previousPage], + queryFn: () => + getAllBookings({ + userNameSearch: searchValue, + page: previousPage, + }), + }); + } + + return { isLoading, bookings, count }; +}; + +export { useBookings }; diff --git a/hotel-management/src/hooks/bookings/useCheckout.ts b/hotel-management/src/hooks/bookings/useCheckout.ts new file mode 100644 index 0000000..b99bd39 --- /dev/null +++ b/hotel-management/src/hooks/bookings/useCheckout.ts @@ -0,0 +1,43 @@ +import toast from 'react-hot-toast'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +// Constants +import { CHECKOUT_SUCCESS } from '@constant/messages'; + +// Hooks +import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; + +// Services +import { checkOutBooking as checkOutBookingFn } from '@service/bookingServices'; + +const useCheckOut = () => { + const queryClient = useQueryClient(); + const { dispatch } = useUserRoomAvailable(); + + const { + mutate: checkOutBooking, + isPending: isChecking, + isSuccess, + } = useMutation({ + mutationFn: checkOutBookingFn, + onSuccess: (data) => { + toast.success(CHECKOUT_SUCCESS); + queryClient.invalidateQueries({ queryKey: ['bookings'] }); + + // Update status room, users + dispatch!({ + type: 'updateStatusRoom', + payload: [{ id: data.roomId, status: false }], + }); + dispatch!({ + type: 'updateStatusUser', + payload: [{ id: data.userId, isBooked: false }], + }); + }, + onError: (err) => toast.error(err.message), + }); + + return { checkOutBooking, isChecking, isSuccess }; +}; + +export default useCheckOut; diff --git a/hotel-management/src/hooks/bookings/useCreateBooking.ts b/hotel-management/src/hooks/bookings/useCreateBooking.ts new file mode 100644 index 0000000..de26709 --- /dev/null +++ b/hotel-management/src/hooks/bookings/useCreateBooking.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; + +// Services +import { createBooking as createBookingFn } from '@service/bookingServices'; + +// Constants +import { ADD_SUCCESS } from '@constant/messages'; + +/** + * Create booking on database + * @returns The boolean of isCreating and create booking function + */ +const useCreateBooking = () => { + const queryClient = useQueryClient(); + + const { + mutate: createBooking, + isPending: isCreating, + isSuccess, + } = useMutation({ + mutationFn: createBookingFn, + onSuccess: () => { + toast.success(ADD_SUCCESS); + queryClient.invalidateQueries({ queryKey: ['bookings'] }); + }, + onError: (err) => toast.error(err.message), + }); + + return { isCreating, createBooking, isSuccess }; +}; + +export { useCreateBooking }; diff --git a/hotel-management/src/hooks/bookings/useDeleteBooking.ts b/hotel-management/src/hooks/bookings/useDeleteBooking.ts new file mode 100644 index 0000000..4834bc8 --- /dev/null +++ b/hotel-management/src/hooks/bookings/useDeleteBooking.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; + +// Services +import { deleteBooking as deleteBookingFn } from '@service/bookingServices'; + +// Constants +import { DELETE_SUCCESS } from '@constant/messages'; + +/** + * Delete the booking from database + * @returns The boolean of status deleting and deleteBooking function + */ +const useDeleteBooking = () => { + const queryClient = useQueryClient(); + + const { + isPending: isDeleting, + mutate: deleteBooking, + isSuccess, + } = useMutation({ + mutationFn: deleteBookingFn, + onSuccess: () => { + toast.success(DELETE_SUCCESS); + queryClient.invalidateQueries({ + queryKey: ['bookings'], + }); + }, + onError: (err) => toast.error(err.message), + }); + + return { isDeleting, deleteBooking, isSuccess }; +}; + +export { useDeleteBooking }; diff --git a/hotel-management/src/hooks/bookings/useUpdateBooking.ts b/hotel-management/src/hooks/bookings/useUpdateBooking.ts new file mode 100644 index 0000000..d1577ef --- /dev/null +++ b/hotel-management/src/hooks/bookings/useUpdateBooking.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; + +// Services +import { updateBooking as updateBookingFn } from '@service/bookingServices'; + +// Constants +import { UPDATE_SUCCESS } from '@constant/messages'; + +/** + * Update booking from database + * @returns The status of updating and updateBooking function + */ +const useUpdateBooking = () => { + const queryClient = useQueryClient(); + + const { + mutate: updateBooking, + isPending: isUpdating, + isSuccess + } = useMutation({ + mutationFn: updateBookingFn, + onSuccess: () => { + toast.success(UPDATE_SUCCESS); + queryClient.invalidateQueries({ queryKey: ['bookings'] }); + }, + onError: (err) => toast.error(err.message), + }); + + return { isUpdating, updateBooking, isSuccess }; +}; + +export { useUpdateBooking }; diff --git a/hotel-management/src/hooks/rooms/useRooms.ts b/hotel-management/src/hooks/rooms/useRooms.ts index 8e0a56a..ff06f56 100644 --- a/hotel-management/src/hooks/rooms/useRooms.ts +++ b/hotel-management/src/hooks/rooms/useRooms.ts @@ -31,7 +31,7 @@ const useRooms = () => { getAllRooms({ sortBy: sortByValue, orderBy: orderByValue, - roomName: searchValue, + roomSearch: searchValue, page, }), }); @@ -52,7 +52,7 @@ const useRooms = () => { getAllRooms({ sortBy: sortByValue, orderBy: orderByValue, - roomName: searchValue, + roomSearch: searchValue, page: nextPage, }), }); @@ -67,7 +67,7 @@ const useRooms = () => { getAllRooms({ sortBy: sortByValue, orderBy: orderByValue, - roomName: searchValue, + roomSearch: searchValue, page: previousPage, }), }); diff --git a/hotel-management/src/hooks/users/useDeleteUser.ts b/hotel-management/src/hooks/users/useDeleteUser.ts new file mode 100644 index 0000000..7435085 --- /dev/null +++ b/hotel-management/src/hooks/users/useDeleteUser.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import toast from 'react-hot-toast'; + +// Services +import { deleteUser as deleteUserFn } from '@service/userServices'; + +// Constants +import * as messages from '@constant/messages'; + +/** + * Delete the user from database + * @returns The boolean of status deleting and deleteUser function + */ +const useDeleteUser = () => { + const queryClient = useQueryClient(); + + const { + isPending: isDeleting, + mutate: deleteUser, + isSuccess, + } = useMutation({ + mutationFn: deleteUserFn, + onSuccess: () => { + toast.success(messages.DELETE_SUCCESS); + queryClient.invalidateQueries({ + queryKey: ['users'], + }); + }, + onError: (err) => toast.error(err.message), + }); + + return { isDeleting, deleteUser, isSuccess }; +}; + +export { useDeleteUser }; From c1da56d9b5a58f844c158214ab14bb53a6ece06d Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 09:19:03 +0700 Subject: [PATCH 15/26] Format code style --- hotel-management/src/hooks/bookings/useBookings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hotel-management/src/hooks/bookings/useBookings.ts b/hotel-management/src/hooks/bookings/useBookings.ts index 99473dc..4134a3c 100644 --- a/hotel-management/src/hooks/bookings/useBookings.ts +++ b/hotel-management/src/hooks/bookings/useBookings.ts @@ -17,7 +17,9 @@ const useBookings = () => { const [searchParams] = useSearchParams(); const searchValue = searchParams.get('search') || ''; - const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1; + const page = searchParams.get('page') + ? Number(searchParams.get('page')) + : 1; const { isLoading, From 08b07ffd6b94b4fde32aaa051b66819a9b95977c Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 09:38:12 +0700 Subject: [PATCH 16/26] Add booking pages --- hotel-management/src/App.tsx | 49 +-- .../src/components/Search/styled.ts | 3 +- hotel-management/src/constants/commons.ts | 9 +- .../src/pages/Booking/BookingForm.tsx | 300 ++++++++++++++++++ .../src/pages/Booking/BookingRow.tsx | 112 +++++++ .../src/pages/Booking/BookingTable.tsx | 63 ++++ hotel-management/src/pages/Booking/index.tsx | 38 +++ hotel-management/src/pages/Booking/styled.ts | 36 +++ hotel-management/src/pages/Dashboard.tsx | 23 -- hotel-management/src/pages/Room/RoomRow.tsx | 3 +- hotel-management/src/pages/Room/index.tsx | 36 +-- hotel-management/src/pages/User/UserRow.tsx | 52 ++- 12 files changed, 640 insertions(+), 84 deletions(-) create mode 100644 hotel-management/src/pages/Booking/BookingForm.tsx create mode 100644 hotel-management/src/pages/Booking/BookingRow.tsx create mode 100644 hotel-management/src/pages/Booking/BookingTable.tsx create mode 100644 hotel-management/src/pages/Booking/index.tsx create mode 100644 hotel-management/src/pages/Booking/styled.ts delete mode 100644 hotel-management/src/pages/Dashboard.tsx 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)} + /> +
From 28f95e308bc13ec1b1cca13ae58cc7162f87ec18 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 10:16:07 +0700 Subject: [PATCH 17/26] Format code and update comment section --- hotel-management/src/App.tsx | 2 -- .../src/pages/Booking/BookingForm.tsx | 21 +++++++++---------- .../src/pages/Booking/BookingRow.tsx | 10 ++++++++- .../src/pages/Booking/BookingTable.tsx | 14 +++++++++++-- hotel-management/src/pages/Booking/styled.ts | 14 +------------ 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index 234c2f8..c0785e7 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -3,7 +3,6 @@ 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'; @@ -72,7 +71,6 @@ function App() { return ( - {/* */} diff --git a/hotel-management/src/pages/Booking/BookingForm.tsx b/hotel-management/src/pages/Booking/BookingForm.tsx index 6040472..dd548b6 100644 --- a/hotel-management/src/pages/Booking/BookingForm.tsx +++ b/hotel-management/src/pages/Booking/BookingForm.tsx @@ -3,7 +3,6 @@ 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'; @@ -104,24 +103,24 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => { reset(); onCloseModal?.(); - // Update room in global state - // Old room + // Update room status in global state + // Reset status old room dispatch!({ type: 'updateStatusRoom', payload: [{ id: booking!.rooms!.id, status: false }], }); - // New room + // Change status new room dispatch!({ type: 'updateStatusRoom', payload: [{ id: newBooking.roomId, status: true }], }); - // Update room available in server - // Old room + // Update room status in server + // Reset status old room await updateRoomStatus(booking!.rooms!.id, false); - // New room + // Change status new room await updateRoomStatus(newBooking.roomId, true); }, } @@ -277,7 +276,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => { - { ? 'Add' : 'Save' } - - + + Close - + diff --git a/hotel-management/src/pages/Booking/BookingRow.tsx b/hotel-management/src/pages/Booking/BookingRow.tsx index f1d900f..cdf8e07 100644 --- a/hotel-management/src/pages/Booking/BookingRow.tsx +++ b/hotel-management/src/pages/Booking/BookingRow.tsx @@ -29,7 +29,15 @@ interface IBookingRow { const BookingRow = ({ booking }: IBookingRow) => { const { checkOutBooking } = useCheckOut(); - const { id, users, startDate, endDate, rooms, amount, status } = booking; + const { + id, + users, + startDate, + endDate, + rooms, + amount, + status + } = booking; const statusText = status ? 'Check in' : 'Check out'; diff --git a/hotel-management/src/pages/Booking/BookingTable.tsx b/hotel-management/src/pages/Booking/BookingTable.tsx index 14a93ad..dd5faea 100644 --- a/hotel-management/src/pages/Booking/BookingTable.tsx +++ b/hotel-management/src/pages/Booking/BookingTable.tsx @@ -20,8 +20,18 @@ import { useBookings } from '@hook/bookings/useBookings'; import { TBookingResponse } from '@type/booking'; const BookingTable = () => { - const columnName = ['User', 'Date', 'Room', 'Amount', 'Status']; - const { isLoading, bookings, count } = useBookings(); + const columnName = [ + 'User', + 'Date', + 'Room', + 'Amount', + 'Status' + ]; + const { + isLoading, + bookings, + count + } = useBookings(); const renderBookingRow = useCallback( (booking: TBookingResponse) => ( diff --git a/hotel-management/src/pages/Booking/styled.ts b/hotel-management/src/pages/Booking/styled.ts index 6899c24..3bf1fdc 100644 --- a/hotel-management/src/pages/Booking/styled.ts +++ b/hotel-management/src/pages/Booking/styled.ts @@ -1,8 +1,5 @@ import styled from 'styled-components'; -// Components -import Button from '@commonStyle/Button'; - const StyledBooking = styled.main` padding: 20px; padding-bottom: 100px; @@ -18,19 +15,10 @@ const Title = styled.h2` 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 }; +export { StyledBooking, Title, StyledOperationTable }; From 5aec68dfce7f3dbfa307354497bdb3b01b50e77d Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 10:37:50 +0700 Subject: [PATCH 18/26] Update common file --- hotel-management/src/assets/images/logo.png | Bin 0 -> 15533 bytes hotel-management/src/commons/styles/Button.ts | 1 + .../src/commons/styles/Direction.ts | 3 +- .../src/components/ButtonIcon/index.tsx | 5 ++++ .../src/components/Form/index.tsx | 8 +++-- .../src/components/Form/styled.ts | 28 +++++++++++++++--- .../src/components/HeaderMenu/index.tsx | 9 +++++- .../src/components/RootLayout/index.tsx | 24 +++++++++++++++ .../src/components/RootLayout/styled.ts | 11 +++++++ .../src/components/RouteProtected/index.tsx | 15 ++++++++++ hotel-management/src/constants/path.ts | 1 + .../src/styles/abstracts/variables.css | 2 ++ hotel-management/src/types/common.ts | 9 +++++- 13 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 hotel-management/src/assets/images/logo.png create mode 100644 hotel-management/src/components/RootLayout/index.tsx create mode 100644 hotel-management/src/components/RootLayout/styled.ts create mode 100644 hotel-management/src/components/RouteProtected/index.tsx diff --git a/hotel-management/src/assets/images/logo.png b/hotel-management/src/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..198ab2bfc4c905f6f082e6fc1d69fb2bf19ac1b4 GIT binary patch literal 15533 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 &:disabled, &[disabled] { cursor: not-allowed; + background-color: var(--disabled-btn-color); } `; diff --git a/hotel-management/src/commons/styles/Direction.ts b/hotel-management/src/commons/styles/Direction.ts index 6fe5a18..c1a0661 100644 --- a/hotel-management/src/commons/styles/Direction.ts +++ b/hotel-management/src/commons/styles/Direction.ts @@ -1,7 +1,6 @@ +import { TDirection } from '@type/common'; import styled, { css } from 'styled-components'; -type TDirection = 'vertical' | 'horizontal'; - interface IDirection { type?: TDirection; } diff --git a/hotel-management/src/components/ButtonIcon/index.tsx b/hotel-management/src/components/ButtonIcon/index.tsx index dff27b0..6671291 100644 --- a/hotel-management/src/components/ButtonIcon/index.tsx +++ b/hotel-management/src/components/ButtonIcon/index.tsx @@ -1,4 +1,6 @@ import { ReactNode } from 'react'; + +// Styled import { StyledButtonIcon } from './styled'; interface IButtonIcon { @@ -14,6 +16,7 @@ interface IButtonIcon { size?: string; }; onClick?: () => void; + disabled?: boolean; } const ButtonIcon = ({ @@ -22,6 +25,7 @@ const ButtonIcon = ({ style, iconStyle, onClick, + disabled, }: IButtonIcon) => { const isHaveChildren = Boolean(children); @@ -35,6 +39,7 @@ const ButtonIcon = ({ }} iconStyle={iconStyle} onClick={onClick} + disabled={disabled} > {icon} {children} diff --git a/hotel-management/src/components/Form/index.tsx b/hotel-management/src/components/Form/index.tsx index 6dd2cc3..891934b 100644 --- a/hotel-management/src/components/Form/index.tsx +++ b/hotel-management/src/components/Form/index.tsx @@ -10,6 +10,9 @@ import { FormBtn, } from './styled'; +// Types +import { TDirection } from '@type/common'; + interface IFormProps { children: ReactNode; onSubmit: (event: FormEvent) => void; @@ -20,6 +23,7 @@ interface IFormRow { label: string; error?: string; children: ReactNode; + direction?: TDirection; } const Form = ({ children, onSubmit, id }: IFormProps) => { @@ -30,9 +34,9 @@ const Form = ({ children, onSubmit, id }: IFormProps) => { ); }; -const FormRow = ({ label, error, children }: IFormRow) => { +const FormRow = ({ label, error, children, direction }: IFormRow) => { return ( - +
{children} diff --git a/hotel-management/src/components/Form/styled.ts b/hotel-management/src/components/Form/styled.ts index 01ed87f..f9a2192 100644 --- a/hotel-management/src/components/Form/styled.ts +++ b/hotel-management/src/components/Form/styled.ts @@ -1,4 +1,11 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; + +// Types +import { TDirection } from '@type/common'; + +interface IStyledFormRow { + direction?: TDirection; +} // Styled import Button from '@commonStyle/Button'; @@ -16,11 +23,20 @@ const StyledActionBtn = styled.div` gap: 100px; `; -const StyledFormRow = styled.div` +const StyledFormRow = styled.div` display: flex; justify-content: space-between; - align-items: center; - gap: 100px; + + ${(props) => + props.direction === 'horizontal' + ? css` + align-items: center; + gap: 100px; + ` + : css` + flex-direction: column; + gap: 15px; + `} `; const Label = styled.label` @@ -45,4 +61,8 @@ const FormBtn = styled(Button)` } `; +StyledFormRow.defaultProps = { + direction: 'horizontal', +}; + export { StyledForm, StyledActionBtn, StyledFormRow, Label, Error, FormBtn }; diff --git a/hotel-management/src/components/HeaderMenu/index.tsx b/hotel-management/src/components/HeaderMenu/index.tsx index 9a5cd93..adfd681 100644 --- a/hotel-management/src/components/HeaderMenu/index.tsx +++ b/hotel-management/src/components/HeaderMenu/index.tsx @@ -6,7 +6,12 @@ import ButtonIcon from '@component/ButtonIcon'; // Styled import { StyledHeaderMenu } from './styled'; +// Hooks +import { useLogout } from '@hook/authentication/useLogout'; + const HeaderMenu = () => { + const { logout, isPending } = useLogout(); + return (
  • @@ -18,9 +23,11 @@ const HeaderMenu = () => {
  • } - iconStyle={{size: '23px' }} + iconStyle={{ size: '23px' }} />
  • diff --git a/hotel-management/src/components/RootLayout/index.tsx b/hotel-management/src/components/RootLayout/index.tsx new file mode 100644 index 0000000..8ca6e58 --- /dev/null +++ b/hotel-management/src/components/RootLayout/index.tsx @@ -0,0 +1,24 @@ +import { Outlet } from 'react-router-dom'; + +// Styled +import Spinner from '@commonStyle/Spinner'; +import { FullPageLayout } from './styled'; + +// Hooks +import { useAccount } from '@hook/authentication/useAccount'; + +const RootLayout = () => { + const { isPending } = useAccount(); + + return isPending ? ( + + + + ) : ( + <> + + + ); +}; + +export default RootLayout; diff --git a/hotel-management/src/components/RootLayout/styled.ts b/hotel-management/src/components/RootLayout/styled.ts new file mode 100644 index 0000000..159ab1c --- /dev/null +++ b/hotel-management/src/components/RootLayout/styled.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const FullPageLayout = styled.div` + height: 100vh; + background-color: var(--color-grey-50); + display: flex; + align-items: center; + justify-content: center; +`; + +export { FullPageLayout }; diff --git a/hotel-management/src/components/RouteProtected/index.tsx b/hotel-management/src/components/RouteProtected/index.tsx new file mode 100644 index 0000000..9f970f2 --- /dev/null +++ b/hotel-management/src/components/RouteProtected/index.tsx @@ -0,0 +1,15 @@ +import { useAccount } from '@hook/authentication/useAccount'; +import { ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; + +interface IRouteProtected { + children: ReactNode; +} +const RouteProtected = ({ children }: IRouteProtected) => { + // Load user login + const { account } = useAccount(); + + return !account ? : children; +}; + +export default RouteProtected; diff --git a/hotel-management/src/constants/path.ts b/hotel-management/src/constants/path.ts index a83803e..89dc268 100644 --- a/hotel-management/src/constants/path.ts +++ b/hotel-management/src/constants/path.ts @@ -4,3 +4,4 @@ export const ROOM = '/room'; export const OTHER_PATH = '*'; export const USER_PATH = 'users'; export const ROOM_PATH = 'rooms'; +export const LOGIN = 'login'; diff --git a/hotel-management/src/styles/abstracts/variables.css b/hotel-management/src/styles/abstracts/variables.css index d9ae533..8ced783 100644 --- a/hotel-management/src/styles/abstracts/variables.css +++ b/hotel-management/src/styles/abstracts/variables.css @@ -23,6 +23,8 @@ --form-color: #fff; + --background-login: #f8f8f8; + --shadow-sm: 0px 10px 20px rgba(0, 0, 0, 0.2); --fs-md: 30px; diff --git a/hotel-management/src/types/common.ts b/hotel-management/src/types/common.ts index 7445679..08e954e 100644 --- a/hotel-management/src/types/common.ts +++ b/hotel-management/src/types/common.ts @@ -7,4 +7,11 @@ interface IDataState { isBooked?: boolean; } -export type { Nullable, IDataState }; +interface ILogin { + email: string; + password: string; +} + +type TDirection = 'vertical' | 'horizontal'; + +export type { Nullable, IDataState, TDirection, ILogin }; From 3e51049feb7d61733a6c865c859691dac46ec5e1 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 10:41:24 +0700 Subject: [PATCH 19/26] Update comment section and styled. --- hotel-management/src/commons/styles/Direction.ts | 4 +++- hotel-management/src/components/RootLayout/styled.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hotel-management/src/commons/styles/Direction.ts b/hotel-management/src/commons/styles/Direction.ts index c1a0661..60a3944 100644 --- a/hotel-management/src/commons/styles/Direction.ts +++ b/hotel-management/src/commons/styles/Direction.ts @@ -1,6 +1,8 @@ -import { TDirection } from '@type/common'; import styled, { css } from 'styled-components'; +// Types +import { TDirection } from '@type/common'; + interface IDirection { type?: TDirection; } diff --git a/hotel-management/src/components/RootLayout/styled.ts b/hotel-management/src/components/RootLayout/styled.ts index 159ab1c..7880d5a 100644 --- a/hotel-management/src/components/RootLayout/styled.ts +++ b/hotel-management/src/components/RootLayout/styled.ts @@ -2,7 +2,6 @@ import styled from 'styled-components'; const FullPageLayout = styled.div` height: 100vh; - background-color: var(--color-grey-50); display: flex; align-items: center; justify-content: center; From f09a4bc60d070af78ad74a4619672ccb0bbc54a4 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 11:19:18 +0700 Subject: [PATCH 20/26] Create hooks and implement login pages --- hotel-management/src/App.tsx | 27 ++++++-- .../src/hooks/authentication/useAccount.ts | 20 ++++++ .../src/hooks/authentication/useLogin.ts | 32 +++++++++ .../src/hooks/authentication/useLogout.ts | 22 ++++++ .../src/pages/Login/LoginForm.tsx | 69 +++++++++++++++++++ hotel-management/src/pages/Login/index.tsx | 32 +++++++++ hotel-management/src/pages/Login/styled.ts | 43 ++++++++++++ .../src/services/authenticationService.ts | 43 ++++++++++++ hotel-management/src/services/roomServices.ts | 26 ++----- hotel-management/tsconfig.json | 3 +- 10 files changed, 290 insertions(+), 27 deletions(-) create mode 100644 hotel-management/src/hooks/authentication/useAccount.ts create mode 100644 hotel-management/src/hooks/authentication/useLogin.ts create mode 100644 hotel-management/src/hooks/authentication/useLogout.ts create mode 100644 hotel-management/src/pages/Login/LoginForm.tsx create mode 100644 hotel-management/src/pages/Login/index.tsx create mode 100644 hotel-management/src/pages/Login/styled.ts create mode 100644 hotel-management/src/services/authenticationService.ts diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index c0785e7..e6edaa6 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -7,12 +7,15 @@ import { useEffect, useMemo, useReducer } from 'react'; // Components import AppLayout from './components/AppLayout'; import Toast from './components/Toast'; +import RouteProtected from '@component/RouteProtected'; +import RootLayout from '@component/RootLayout'; // Pages import User from './pages/User'; import Booking from './pages/Booking'; import Room from './pages/Room'; import NotFound from './pages/NotFound'; +import Login from '@page/Login'; // Constants import * as PATH from './constants/path'; @@ -75,13 +78,25 @@ function App() { - }> - } /> - } /> - } /> - } /> + }> + + + + } + > + } + /> + } /> + } /> + } /> + + } /> + } /> - } /> diff --git a/hotel-management/src/hooks/authentication/useAccount.ts b/hotel-management/src/hooks/authentication/useAccount.ts new file mode 100644 index 0000000..34c5c86 --- /dev/null +++ b/hotel-management/src/hooks/authentication/useAccount.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; + +// Services +import { getCurrentAccount } from '@service/authenticationService'; + +const useAccount = () => { + let isAuthenticated: boolean = false; + const { data: account, isPending } = useQuery({ + queryKey: ['account'], + queryFn: getCurrentAccount, + }); + + if (account) { + isAuthenticated = account.role === 'authenticated'; + } + + return { isPending, account, isAuthenticated }; +}; + +export { useAccount }; diff --git a/hotel-management/src/hooks/authentication/useLogin.ts b/hotel-management/src/hooks/authentication/useLogin.ts new file mode 100644 index 0000000..b2c854a --- /dev/null +++ b/hotel-management/src/hooks/authentication/useLogin.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import toast from 'react-hot-toast'; + +// Services +import { login as loginFn } from '@service/authenticationService'; + +// Types +import { ILogin } from '@type/common'; + +const useLogin = () => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get('redirectTo') || null; + + const { mutate: login, isPending } = useMutation({ + mutationFn: ({ email, password }: ILogin) => loginFn({ email, password }), + onSuccess: (account) => { + queryClient.setQueryData(['account'], account.user); + redirectTo ? navigate(redirectTo) : navigate('/'); + }, + onError: (err) => { + console.error(err.message); + toast.error('Email or password not correct!'); + }, + }); + + return { login, isPending }; +}; + +export { useLogin }; diff --git a/hotel-management/src/hooks/authentication/useLogout.ts b/hotel-management/src/hooks/authentication/useLogout.ts new file mode 100644 index 0000000..66cc623 --- /dev/null +++ b/hotel-management/src/hooks/authentication/useLogout.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; + +// Services +import { logout as logoutFn } from '@service/authenticationService'; + +const useLogout = () => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const { mutate: logout, isPending } = useMutation({ + mutationFn: logoutFn, + onSuccess: () => { + queryClient.removeQueries(); + navigate('/login', { replace: true }); + }, + }); + + return { logout, isPending }; +}; + +export { useLogout }; diff --git a/hotel-management/src/pages/Login/LoginForm.tsx b/hotel-management/src/pages/Login/LoginForm.tsx new file mode 100644 index 0000000..9d0f6a2 --- /dev/null +++ b/hotel-management/src/pages/Login/LoginForm.tsx @@ -0,0 +1,69 @@ +import { useForm } from 'react-hook-form'; + +// Components +import Form from '@component/Form'; + +// Styled +import { FieldInput, StyledLoginForm } from './styled'; +import Button from '@commonStyle/Button'; + +// Constants +import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage'; + +// Types +import { ILogin } from '@type/common'; + +// Hooks +import { useLogin } from '@hook/authentication/useLogin'; + +const LoginForm = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + const { login, isPending } = useLogin(); + + const onSubmit = (data: ILogin) => { + login(data); + }; + + return ( + +
    + + + + + + + + +
    +
    + ); +}; + +export default LoginForm; diff --git a/hotel-management/src/pages/Login/index.tsx b/hotel-management/src/pages/Login/index.tsx new file mode 100644 index 0000000..c443ab2 --- /dev/null +++ b/hotel-management/src/pages/Login/index.tsx @@ -0,0 +1,32 @@ +import { Navigate } from 'react-router-dom'; + +// Styled +import { LoginLayout, StyledLogo, TitleStyled } from './styled'; + +// Assets +import logo from '@assets/images/logo.png'; + +// Components +import LoginForm from './LoginForm'; + +// Hooks +import { useAccount } from '@hook/authentication/useAccount'; + +const Login = () => { + const { account } = useAccount(); + + if (account) { + return ; + } + + return ( + + + Log In + + + + ); +}; + +export default Login; diff --git a/hotel-management/src/pages/Login/styled.ts b/hotel-management/src/pages/Login/styled.ts new file mode 100644 index 0000000..9298f3b --- /dev/null +++ b/hotel-management/src/pages/Login/styled.ts @@ -0,0 +1,43 @@ +import styled from 'styled-components'; + +// Styled +import CommonInput from '@commonStyle/CommonInput'; + +const LoginLayout = styled.main` + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: var(--background-login); +`; + +const StyledLogo = styled.img` + width: 250px; + height: 250px; +`; + +const TitleStyled = styled.h1` + font-size: var(--fs-md); +`; + +const StyledLoginForm = styled.div` + background-color: var(--form-color); + + border-radius: var(--radius-sm); + + display: flex; + flex-direction: column; + gap: 10px; + + margin-top: 20px; + padding: 30px 50px; +`; + +const FieldInput = styled.input` + ${CommonInput} + + width: 300px; +` + +export { LoginLayout, StyledLogo, TitleStyled, StyledLoginForm, FieldInput }; diff --git a/hotel-management/src/services/authenticationService.ts b/hotel-management/src/services/authenticationService.ts new file mode 100644 index 0000000..5a0e5f1 --- /dev/null +++ b/hotel-management/src/services/authenticationService.ts @@ -0,0 +1,43 @@ +import { ILogin } from '@type/common'; + +// Services +import supabase from './supabaseService'; + +const login = async ({ email, password }: ILogin) => { + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error) { + throw new Error(error.message); + } + + return data; +}; + +const getCurrentAccount = async () => { + const { data } = await supabase.auth.getSession(); + + if (!data.session) { + return null; + } + + const { data: accountData, error } = await supabase.auth.getUser(); + + if (error) { + throw new Error(error.message); + } + + return accountData.user; +}; + +const logout = async () => { + const { error } = await supabase.auth.signOut(); + + if (error) { + throw new Error(error.message); + } +}; + +export { login, logout, getCurrentAccount }; diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index e660376..91c76c4 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -1,6 +1,5 @@ // Types import { IRoom } from '@type/rooms'; -import { IDataState } from '@type/common'; // Services import supabase from './supabaseService'; @@ -127,28 +126,17 @@ const getRoomById = async (idRoom: string): Promise => { return data; }; + /** - * - * @returns + * Update status of room + * @param id The id of room need to be change status + * @param status The status of room */ -const getRoomsAvailable = async (): Promise => { - const { data, error } = await supabase - .from(ROOMS_TABLE) - .select('id, name, status'); - - if (error) { - console.error(error.message); - throw new Error(ERROR_FETCHING_ROOM); - } - - return data; -}; - const updateRoomStatus = async ( id: number, status: boolean -): Promise => { - const { error, status: statusConnection } = await supabase +): Promise => { + const { error} = await supabase .from(ROOMS_TABLE) .update({ status }) .eq('id', id); @@ -158,7 +146,6 @@ const updateRoomStatus = async ( throw new Error(ERROR_UPDATE_ROOM); } - return statusConnection; }; export { @@ -166,7 +153,6 @@ export { updateRoom, createRoom, deleteRoom, - getRoomsAvailable, getRoomById, updateRoomStatus, }; diff --git a/hotel-management/tsconfig.json b/hotel-management/tsconfig.json index ba79ba5..284f24e 100644 --- a/hotel-management/tsconfig.json +++ b/hotel-management/tsconfig.json @@ -32,7 +32,8 @@ "@component/*": ["./src/components/*"], "@commonStyle/*": ["./src/commons/styles/*"], "@type/*": ["./src/types/*"], - "@page/*": ["./src/pages/*"] + "@page/*": ["./src/pages/*"], + "@assets/*": ["./src/assets/*"], } }, "include": ["src", "test"], From b5589bba0fbbfaa5c3bcfdf51b4e0961f01c872e Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 11:21:37 +0700 Subject: [PATCH 21/26] Format code style --- hotel-management/src/hooks/authentication/useLogin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hotel-management/src/hooks/authentication/useLogin.ts b/hotel-management/src/hooks/authentication/useLogin.ts index b2c854a..5529169 100644 --- a/hotel-management/src/hooks/authentication/useLogin.ts +++ b/hotel-management/src/hooks/authentication/useLogin.ts @@ -18,7 +18,9 @@ const useLogin = () => { mutationFn: ({ email, password }: ILogin) => loginFn({ email, password }), onSuccess: (account) => { queryClient.setQueryData(['account'], account.user); - redirectTo ? navigate(redirectTo) : navigate('/'); + redirectTo + ? navigate(redirectTo) + : navigate('/'); }, onError: (err) => { console.error(err.message); From fc2428f738964001dfd77f4512e80868ae0aacea Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 12:02:43 +0700 Subject: [PATCH 22/26] Create and update commons, components and types --- hotel-management/src/commons/styles/Input.ts | 2 +- .../src/components/AppLayout/index.tsx | 7 ++++++- .../src/components/HeaderMenu/index.tsx | 4 ++++ hotel-management/src/constants/commons.ts | 1 + .../src/constants/formValidateMessage.ts | 16 +++++++++++++++- hotel-management/src/constants/messages.ts | 3 +++ hotel-management/src/constants/path.ts | 1 + hotel-management/src/types/account.ts | 6 ++++++ 8 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 hotel-management/src/types/account.ts diff --git a/hotel-management/src/commons/styles/Input.ts b/hotel-management/src/commons/styles/Input.ts index bd746fa..09113f5 100644 --- a/hotel-management/src/commons/styles/Input.ts +++ b/hotel-management/src/commons/styles/Input.ts @@ -3,7 +3,7 @@ import styled, { css } from 'styled-components'; // Styled import CommonInput from './CommonInput'; -type TInput = 'text' | 'checkbox' | 'hidden' | 'date'; +type TInput = 'text' | 'checkbox' | 'hidden' | 'date' | 'password'; interface IInputTyped { type?: TInput; diff --git a/hotel-management/src/components/AppLayout/index.tsx b/hotel-management/src/components/AppLayout/index.tsx index 231cff6..e4070a6 100644 --- a/hotel-management/src/components/AppLayout/index.tsx +++ b/hotel-management/src/components/AppLayout/index.tsx @@ -7,10 +7,15 @@ import Sidebar from '@component/Sidebar'; // Styled import { Main, StyledAppLayout } from './styled'; +// Hooks +import { useAccount } from '@hook/authentication/useAccount'; + const AppLayout = () => { + const { account } = useAccount(); + return ( -
    +
    diff --git a/hotel-management/src/components/HeaderMenu/index.tsx b/hotel-management/src/components/HeaderMenu/index.tsx index adfd681..252001a 100644 --- a/hotel-management/src/components/HeaderMenu/index.tsx +++ b/hotel-management/src/components/HeaderMenu/index.tsx @@ -1,3 +1,5 @@ +import { useNavigate } from 'react-router-dom'; + // Components import { IoPersonCircleOutline } from 'react-icons/io5'; import { HiOutlineLogout } from 'react-icons/hi'; @@ -11,6 +13,7 @@ import { useLogout } from '@hook/authentication/useLogout'; const HeaderMenu = () => { const { logout, isPending } = useLogout(); + const navigate = useNavigate(); return ( @@ -19,6 +22,7 @@ const HeaderMenu = () => { aria-label="Profile" icon={} iconStyle={{ size: '23px' }} + onClick={() => navigate('/account')} />
  • diff --git a/hotel-management/src/constants/commons.ts b/hotel-management/src/constants/commons.ts index e488048..e790987 100644 --- a/hotel-management/src/constants/commons.ts +++ b/hotel-management/src/constants/commons.ts @@ -56,6 +56,7 @@ const REGEX = { PHONE: /(84|0[3|5|7|8|9])+([0-9]{8})\b/g, NAME: /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm, NUMBER: /^[0-9]*$/, + PASSWORD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$@!%&*?])[A-Za-z\d#$@!%&*?]{8,16}$/ }; export { USER_PAGE, ROOM_PAGE, FORM, REGEX, ORDERBY_OPTIONS }; diff --git a/hotel-management/src/constants/formValidateMessage.ts b/hotel-management/src/constants/formValidateMessage.ts index 763d090..1d442c7 100644 --- a/hotel-management/src/constants/formValidateMessage.ts +++ b/hotel-management/src/constants/formValidateMessage.ts @@ -2,5 +2,19 @@ const INVALID_FIELD = 'This field is invalid format!'; const INVALID_PHONE = 'Phone number is invalid!'; const REQUIRED_FIELD_ERROR = 'This field is required!'; const INVALID_DISCOUNT = 'Discount should be greater than 0 and less than 100!'; +const PASSWORD_CONDITION = + 'Your password must have 1 uppercase, 1 lowercase, 1 special character, min 1 number and the length between from 8 to 16!'; +const INVALID_PASSWORD = 'Invalid password'; +const PASSWORD_NOT_MATCH = 'Password not match!'; +const PASSWORD_NOT_MATCH_REQUIREMENT = 'Password not match the requirement!' -export { REQUIRED_FIELD_ERROR, INVALID_DISCOUNT, INVALID_FIELD, INVALID_PHONE }; +export { + REQUIRED_FIELD_ERROR, + INVALID_DISCOUNT, + INVALID_FIELD, + INVALID_PHONE, + PASSWORD_CONDITION, + INVALID_PASSWORD, + PASSWORD_NOT_MATCH, + PASSWORD_NOT_MATCH_REQUIREMENT +}; diff --git a/hotel-management/src/constants/messages.ts b/hotel-management/src/constants/messages.ts index b8fefdd..7553385 100644 --- a/hotel-management/src/constants/messages.ts +++ b/hotel-management/src/constants/messages.ts @@ -27,6 +27,8 @@ const ERROR_CREATE_USER = "Can't create user!"; const ERROR_UPDATE_USER = "Can't update user!"; const ERROR_DELETE_USER = "Can't delete user!"; +const UPDATE_ACCOUNT_SUCCESSFUL = 'Update account successfully!'; + export { ADD_SUCCESS, UPDATE_SUCCESS, @@ -50,4 +52,5 @@ export { ERROR_CREATE_USER, ERROR_UPDATE_USER, ERROR_DELETE_USER, + UPDATE_ACCOUNT_SUCCESSFUL, }; diff --git a/hotel-management/src/constants/path.ts b/hotel-management/src/constants/path.ts index 89dc268..8a5ee7e 100644 --- a/hotel-management/src/constants/path.ts +++ b/hotel-management/src/constants/path.ts @@ -1,6 +1,7 @@ export const BOOKING = '/booking'; export const USER = '/user'; export const ROOM = '/room'; +export const ACCOUNT = '/account'; export const OTHER_PATH = '*'; export const USER_PATH = 'users'; export const ROOM_PATH = 'rooms'; diff --git a/hotel-management/src/types/account.ts b/hotel-management/src/types/account.ts new file mode 100644 index 0000000..1413d0c --- /dev/null +++ b/hotel-management/src/types/account.ts @@ -0,0 +1,6 @@ +interface IAccount { + fullName?: string; + password?: string; +} + +export type { IAccount }; From 45eba50509d3b76378e3887dd0fc6f0549641fbf Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Thu, 30 Nov 2023 14:13:03 +0700 Subject: [PATCH 23/26] Add edit information account page --- hotel-management/src/App.tsx | 3 + .../src/components/Sidebar/index.tsx | 5 +- .../src/components/Sidebar/styled.ts | 2 + .../hooks/authentication/useUpdateAccount.ts | 25 +++++ .../src/pages/Account/AccountInforForm.tsx | 59 ++++++++++++ .../src/pages/Account/AccountPasswordForm.tsx | 93 +++++++++++++++++++ hotel-management/src/pages/Account/index.tsx | 26 ++++++ hotel-management/src/pages/Account/styled.ts | 36 +++++++ .../src/services/authenticationService.ts | 27 +++++- 9 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 hotel-management/src/hooks/authentication/useUpdateAccount.ts create mode 100644 hotel-management/src/pages/Account/AccountInforForm.tsx create mode 100644 hotel-management/src/pages/Account/AccountPasswordForm.tsx create mode 100644 hotel-management/src/pages/Account/index.tsx create mode 100644 hotel-management/src/pages/Account/styled.ts diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index e6edaa6..3f2c121 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -16,6 +16,7 @@ import Booking from './pages/Booking'; import Room from './pages/Room'; import NotFound from './pages/NotFound'; import Login from '@page/Login'; +import Account from '@page/Account'; // Constants import * as PATH from './constants/path'; @@ -74,6 +75,7 @@ function App() { return ( + {/* */} @@ -93,6 +95,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/hotel-management/src/components/Sidebar/index.tsx b/hotel-management/src/components/Sidebar/index.tsx index b393109..7d97768 100644 --- a/hotel-management/src/components/Sidebar/index.tsx +++ b/hotel-management/src/components/Sidebar/index.tsx @@ -3,15 +3,18 @@ import Nav from '@component/Nav'; // Styled import { Heading, StyledSidebar } from './styled'; +import { useNavigate } from 'react-router-dom'; interface ISidebar { heading: string; } const Sidebar = ({ heading }: ISidebar) => { + const navigate = useNavigate(); + return ( - {heading} + navigate('/')}>{heading}