diff --git a/hotel-management/package.json b/hotel-management/package.json index d13cb38..2e05cd6 100644 --- a/hotel-management/package.json +++ b/hotel-management/package.json @@ -18,6 +18,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: diff --git a/hotel-management/src/App.test.tsx b/hotel-management/src/App.test.tsx index 3de1685..ec995a5 100644 --- a/hotel-management/src/App.test.tsx +++ b/hotel-management/src/App.test.tsx @@ -20,30 +20,48 @@ describe('App', () => { }); test('Should init room, user data', async () => { - const mockGetUserNotBooked = jest.spyOn(userServices, 'getUserNotBooked'); - const mockGetRoomAvailable = jest.spyOn(roomServices, 'getRoomsAvailable'); + const mockGetUserNotBooked = jest.spyOn(userServices, 'getAllUsers'); + const mockGetRoomAvailable = jest.spyOn(roomServices, 'getAllRooms'); - mockGetUserNotBooked.mockResolvedValue([ - { - id: 1, - name: 'Nezumi', - }, - { - id: 2, - name: 'Loi Phan', - }, - ]); + mockGetUserNotBooked.mockResolvedValue({ + data: [ + { + id: 1, + name: 'Nezumi', + phone: '0324432321', + isBooked: true, + isDelete: true, + }, + { + id: 1, + name: 'Loi Phan', + phone: '0324432123', + isBooked: true, + isDelete: true, + }, + ], + count: 2, + }); - mockGetRoomAvailable.mockResolvedValue([ - { - id: 1, - name: 'Room 1', - }, - { - id: 2, - name: 'Room 2', - }, - ]); + mockGetRoomAvailable.mockResolvedValue({ + data: [ + { + id: 1, + name: 'Room 1', + price: 230, + status: true, + isDelete: true, + }, + { + id: 2, + name: 'Room 2', + price: 242, + status: true, + isDelete: true + }, + ], + count: 2 + }); expect(mockGetUserNotBooked).toHaveBeenCalled(); expect(mockGetRoomAvailable).toHaveBeenCalled(); diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index 96ae698..534dbb3 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -2,35 +2,37 @@ 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'; // 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 Dashboard from './pages/Dashboard'; +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'; -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 +43,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 }); } }; @@ -67,16 +79,26 @@ function App() { - }> + }> } - /> - } /> - } /> - } /> + element={ + + + + } + > + } + /> + } /> + } /> + } /> + } /> + + } /> + } /> - } /> diff --git a/hotel-management/src/__snapshots__/App.test.tsx.snap b/hotel-management/src/__snapshots__/App.test.tsx.snap index 5a3bca4..5cbca46 100644 --- a/hotel-management/src/__snapshots__/App.test.tsx.snap +++ b/hotel-management/src/__snapshots__/App.test.tsx.snap @@ -1,1322 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`App Render with data 1`] = ` -{ - "asFragment": [Function], - "baseElement": -
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
- , - "container":
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - -exports[`App Should init room, user data 1`] = ` -{ - "asFragment": [Function], - "baseElement": -
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
- , - "container":
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - -exports[`App Should render correctly 1`] = ` -{ - "asFragment": [Function], - "baseElement": -
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
- , - "container":
-
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
-
-
-
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - exports[`App should render correctly 1`] = ` { "asFragment": [Function], "baseElement":
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
+
, "container":
-
-

- Hi, - Admin - ! -

-
    -
  • - -
  • -
  • - -
  • -
-
- -
-
-

- This page currently still develop. Please try again later! -

-
-
+
` @@ -36,7 +36,8 @@ const Button = styled.button` &:disabled, &[disabled] { - cursor: no-drop; + 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..60a3944 100644 --- a/hotel-management/src/commons/styles/Direction.ts +++ b/hotel-management/src/commons/styles/Direction.ts @@ -1,6 +1,7 @@ import styled, { css } from 'styled-components'; -type TDirection = 'vertical' | 'horizontal'; +// Types +import { TDirection } from '@type/common'; interface IDirection { type?: TDirection; diff --git a/hotel-management/src/commons/styles/Input.ts b/hotel-management/src/commons/styles/Input.ts index 32e5eb2..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'; +type TInput = 'text' | 'checkbox' | 'hidden' | 'date' | 'password'; 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/components/AppLayout/AppLayout.test.tsx b/hotel-management/src/components/AppLayout/AppLayout.test.tsx index 471cd48..6717ed4 100644 --- a/hotel-management/src/components/AppLayout/AppLayout.test.tsx +++ b/hotel-management/src/components/AppLayout/AppLayout.test.tsx @@ -3,12 +3,17 @@ import { BrowserRouter } from 'react-router-dom'; // Components import AppLayout from '.'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; describe('AppLayout', () => { + const queryClient = new QueryClient(); + const wrapper = renderer.create( - - - + + + + + ); test('Should render correctly', () => { diff --git a/hotel-management/src/components/AppLayout/__snapshots__/AppLayout.test.tsx.snap b/hotel-management/src/components/AppLayout/__snapshots__/AppLayout.test.tsx.snap index 6d48dfc..dc7ccf8 100644 --- a/hotel-management/src/components/AppLayout/__snapshots__/AppLayout.test.tsx.snap +++ b/hotel-management/src/components/AppLayout/__snapshots__/AppLayout.test.tsx.snap @@ -9,7 +9,6 @@ exports[`AppLayout Should render correctly 1`] = ` >

Hi, - Admin !

+
+
+ , + "container":
+
- Delete - - -
-
+

+ Hello World! +

+
+ + +
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} `; 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/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 - + ); diff --git a/hotel-management/src/components/Form/__snapshots__/Form.test.tsx.snap b/hotel-management/src/components/Form/__snapshots__/Form.test.tsx.snap index a4ab15f..b3c34ce 100644 --- a/hotel-management/src/components/Form/__snapshots__/Form.test.tsx.snap +++ b/hotel-management/src/components/Form/__snapshots__/Form.test.tsx.snap @@ -2,56 +2,58 @@ exports[`Form Should render correctly 1`] = `

This is error message

This is error message

+ )} + /> + + + + + + + + + ); +}; + +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..3bf1fdc --- /dev/null +++ b/hotel-management/src/pages/Booking/styled.ts @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +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 StyledOperationTable = styled.div` + display: flex; + gap: 30px; + justify-content: flex-end; +`; + +export { StyledBooking, Title, StyledOperationTable }; 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/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/pages/Room/RoomForm.tsx b/hotel-management/src/pages/Room/RoomForm.tsx index d33bc64..5d25da9 100644 --- a/hotel-management/src/pages/Room/RoomForm.tsx +++ b/hotel-management/src/pages/Room/RoomForm.tsx @@ -1,11 +1,8 @@ -import styled from 'styled-components'; - // Styled -import Button from '@commonStyle/Button.ts'; import Input from '@commonStyle/Input.ts'; // Types -import { IRoom } from '@type/rooms.ts'; +import { IRoom } from '@type/room.ts'; // Constants import { @@ -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/RoomRow.tsx b/hotel-management/src/pages/Room/RoomRow.tsx index ae7aaf4..a8e6607 100644 --- a/hotel-management/src/pages/Room/RoomRow.tsx +++ b/hotel-management/src/pages/Room/RoomRow.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react'; import { formatCurrency } from '@helper/helper'; // Types -import { IRoom } from '@type/rooms'; +import { IRoom } from '@type/room'; // Components import RoomForm from './RoomForm'; @@ -16,7 +16,7 @@ import { HiTrash } from 'react-icons/hi'; import ConfirmMessage from '@component/ConfirmMessage'; // Hooks -import { useDeleteRoom } from '@hook/rooms/useDeleteRoom'; +import { useSetIsDeleteRoom } from '@hook/rooms/useSetIsDeleteRoom'; // Constants import { FORM } from '@constant/commons'; @@ -27,7 +27,7 @@ interface IRoomRow { const RoomRow = ({ room }: IRoomRow) => { const { id, name, price, status } = room; - const { isDeleting, deleteRoom } = useDeleteRoom(); + const { setIsDeleteRoom } = useSetIsDeleteRoom(); const statusText = status ? 'Unavailable' : 'Available'; @@ -78,9 +78,8 @@ const RoomRow = ({ room }: IRoomRow) => { deleteRoom(id)} + onConfirm={() => setIsDeleteRoom(id)} /> diff --git a/hotel-management/src/pages/Room/RoomTable.tsx b/hotel-management/src/pages/Room/RoomTable.tsx index daaa726..5585563 100644 --- a/hotel-management/src/pages/Room/RoomTable.tsx +++ b/hotel-management/src/pages/Room/RoomTable.tsx @@ -10,7 +10,7 @@ import OrderBy from '@component/OrderBy'; import RoomRow from './RoomRow'; // Types -import { IRoom } from '@type/rooms'; +import { IRoom } from '@type/room'; // Constants import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons'; @@ -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/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/UserForm.tsx b/hotel-management/src/pages/User/UserForm.tsx index 04b8b68..a558451 100644 --- a/hotel-management/src/pages/User/UserForm.tsx +++ b/hotel-management/src/pages/User/UserForm.tsx @@ -19,11 +19,8 @@ import { } from '@constant/formValidateMessage.ts'; import { REGEX } from '@constant/commons.ts'; -// Styled -import { FormBtn } from './styled.ts'; - // Types -import { IUser } from '@type/users.ts'; +import { IUser } from '@type/user'; interface IUserFormProp { onCloseModal?: () => void; @@ -106,7 +103,7 @@ const UserForm = ({ onCloseModal, user }: IUserFormProp) => { - { ? 'Add' : 'Save' } - - + + Close - + diff --git a/hotel-management/src/pages/User/UserRow.tsx b/hotel-management/src/pages/User/UserRow.tsx index 0ca4aca..c8ec6b5 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 { IUser } from '@type/user'; +import { FORM } from '@constant/commons'; +import { HiTrash } from 'react-icons/hi'; +import { useCallback } from 'react'; +import ConfirmMessage from '@component/ConfirmMessage'; +import { useIsDeleteUser } from '@hook/users/useSetIsDeleteUser'; interface IUserRow { user: IUser; @@ -15,17 +19,35 @@ interface IUserRow { const UserRow = ({ user }: IUserRow) => { const { id, name, phone, isBooked } = user; + const { setIsDeleteUser } = useIsDeleteUser(); + 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} + /> + + - + + + + setIsDeleteUser(id)} + /> +
diff --git a/hotel-management/src/pages/User/UserTable.tsx b/hotel-management/src/pages/User/UserTable.tsx index f3179a8..31f0254 100644 --- a/hotel-management/src/pages/User/UserTable.tsx +++ b/hotel-management/src/pages/User/UserTable.tsx @@ -10,7 +10,7 @@ import OrderBy from '@component/OrderBy'; import UserRow from './UserRow'; // Types -import { IUser } from '@type/users'; +import { IUser } from '@type/user'; // Constants import { ORDERBY_OPTIONS, USER_PAGE } from '@constant/commons'; @@ -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/__tests__/UserRow.test.tsx b/hotel-management/src/pages/User/__tests__/UserRow.test.tsx index 21dce06..d140cde 100644 --- a/hotel-management/src/pages/User/__tests__/UserRow.test.tsx +++ b/hotel-management/src/pages/User/__tests__/UserRow.test.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import renderer from 'react-test-renderer'; // Types -import { IUser } from '@type/users'; +import { IUser } from '@type/user'; // Components import UserRow from '../UserRow'; @@ -13,6 +13,7 @@ describe('UserRow', () => { name: 'Temp Room', phone: '0324421232', isBooked: false, + isDelete: true, }; const queryClient = new QueryClient(); const wrapper = renderer.create( diff --git a/hotel-management/src/pages/User/__tests__/__snapshots__/UserForm.test.tsx.snap b/hotel-management/src/pages/User/__tests__/__snapshots__/UserForm.test.tsx.snap index edcd103..1b92e15 100644 --- a/hotel-management/src/pages/User/__tests__/__snapshots__/UserForm.test.tsx.snap +++ b/hotel-management/src/pages/User/__tests__/__snapshots__/UserForm.test.tsx.snap @@ -1,67 +1,184 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UserForm Should render correctly 1`] = ` -
-
- +{ + "asFragment": [Function], + "baseElement":
- + +
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
-
-
-
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} `; diff --git a/hotel-management/src/pages/User/__tests__/__snapshots__/UserRow.test.tsx.snap b/hotel-management/src/pages/User/__tests__/__snapshots__/UserRow.test.tsx.snap index c6b759a..357c1d0 100644 --- a/hotel-management/src/pages/User/__tests__/__snapshots__/UserRow.test.tsx.snap +++ b/hotel-management/src/pages/User/__tests__/__snapshots__/UserRow.test.tsx.snap @@ -18,11 +18,11 @@ exports[`UserRow Should render correctly 1`] = `
`; diff --git a/hotel-management/src/pages/User/__tests__/__snapshots__/index.test.tsx.snap b/hotel-management/src/pages/User/__tests__/__snapshots__/index.test.tsx.snap index 10bb701..99a82fa 100644 --- a/hotel-management/src/pages/User/__tests__/__snapshots__/index.test.tsx.snap +++ b/hotel-management/src/pages/User/__tests__/__snapshots__/index.test.tsx.snap @@ -2,19 +2,19 @@ exports[`User Should render correctly 1`] = `

List User

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 }; diff --git a/hotel-management/src/services/__tests__/roomService.test.ts b/hotel-management/src/services/__tests__/roomService.test.ts index fcd4cc8..56202ca 100644 --- a/hotel-management/src/services/__tests__/roomService.test.ts +++ b/hotel-management/src/services/__tests__/roomService.test.ts @@ -1,61 +1,59 @@ import { waitFor } from '@testing-library/react'; -import supabase from '@service/supabaseService'; // Services import { createRoom, - deleteRoom, + setIsDeleteRoom, getAllRooms, updateRoom, } from '@service/roomServices'; // Types -import { IRoom } from '@type/rooms'; +import { IRoom } from '@type/room'; jest.mock('@service/supabaseService'); const mockGetAllRooms = jest.fn(getAllRooms); const mockUpdateRoom = jest.fn(updateRoom); const mockCreateRoom = jest.fn(createRoom); -const mockDeleteRoom = jest.fn(deleteRoom); +const mockIsDeleteRoom = jest.fn(setIsDeleteRoom); const sampleData: IRoom = { id: 999, name: 'Room name test', price: 9999, status: true, + isDelete: true, }; describe('Rooms service', () => { test('Should fetch room correctly', async () => { - mockGetAllRooms.mockResolvedValue([ - { - id: 1, - name: 'Nezumi', - price: 2500, - status: true, - }, - { - id: 2, - name: 'Loi Phan', - price: 8500, - status: false, - }, - ]); - const result = await mockGetAllRooms('name', 'asc', ''); + mockGetAllRooms.mockResolvedValue({ + data: [ + { + id: 1, + name: 'Nezumi', + price: 2500, + status: true, + isDelete: true, + }, + { + id: 2, + name: 'Loi Phan', + price: 8500, + status: false, + isDelete: true, + }, + ], + count: 2, + }); + const result = await mockGetAllRooms({ + sortBy: 'name', + orderBy: 'asc', + roomSearch: '', + page: 1, + }); - await waitFor(() => expect(result.length).toEqual(2)); - }); - - test('Can not fetch data', async () => { - jest.spyOn(supabase.from('rooms').select(), 'single').mockRejectedValue(() => { - return { - error: 'Error' - } - }) - - await mockGetAllRooms('name', 'asc', ''); - - expect(mockGetAllRooms).toThrow(); + await waitFor(() => expect(result.data.length).toEqual(2)); }); test('Should create room correctly', async () => { @@ -72,6 +70,7 @@ describe('Rooms service', () => { name: 'Nezumi', price: 2500, status: true, + isDelete: true, }); const result = await mockUpdateRoom(sampleData); @@ -79,7 +78,9 @@ describe('Rooms service', () => { }); test('Should delete room correctly', async () => { - mockDeleteRoom.mockImplementation(() => Promise.resolve()); - await mockDeleteRoom(1); + mockIsDeleteRoom.mockResolvedValue(sampleData); + const result = await mockIsDeleteRoom(1); + + await waitFor(() => expect(result).toBeTruthy()); }); }); diff --git a/hotel-management/src/services/__tests__/userService.test.ts b/hotel-management/src/services/__tests__/userService.test.ts index 9bf2619..af30ea7 100644 --- a/hotel-management/src/services/__tests__/userService.test.ts +++ b/hotel-management/src/services/__tests__/userService.test.ts @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react'; import { createUser, getAllUsers, updateUser } from '@service/userServices'; // Types -import { IUser } from '@type/users'; +import { IUser } from '@type/user'; const mockGetAllUsers = jest.fn(getAllUsers); const mockCreateUser = jest.fn(createUser); @@ -15,27 +15,37 @@ const sampleData: IUser = { name: 'User name test', phone: '123456789', isBooked: true, + isDelete: true, }; describe('User services', () => { test('Should fetch user data correctly', async () => { - mockGetAllUsers.mockResolvedValue([ - { - id: 1, - name: 'Nezumi', - phone: '0324422123', - isBooked: true, - }, - { - id: 2, - name: 'Loi Phan', - phone: '0324422145', - isBooked: false, - }, - ]); - const result = await mockGetAllUsers('name', 'asc', ''); + mockGetAllUsers.mockResolvedValue({ + data: [ + { + id: 1, + name: 'Nezumi', + phone: '0324422123', + isBooked: true, + isDelete: true, + }, + { + id: 2, + name: 'Loi Phan', + phone: '0324422145', + isBooked: false, + isDelete: true, + }, + ], count: 2 + }); + const result = await mockGetAllUsers(({ + sortBy: 'name', + orderBy: 'asc', + phoneSearch: '', + page: 1, + })); - await waitFor(() => expect(result.length).toEqual(2)); + await waitFor(() => expect(result.data.length).toEqual(2)); }); test('Should create user correctly', async () => { diff --git a/hotel-management/src/services/authenticationService.ts b/hotel-management/src/services/authenticationService.ts new file mode 100644 index 0000000..151730e --- /dev/null +++ b/hotel-management/src/services/authenticationService.ts @@ -0,0 +1,68 @@ +import { ILogin } from '@type/common'; +import { UserAttributes } from '@supabase/supabase-js'; + +// Services +import supabase from './supabaseService'; + +// Types +import { IAccount } from '@type/account'; + +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); + } +}; + +const updateAccount = async ({ fullName, password }: IAccount) => { + let accountUpdate: UserAttributes | null = null; + + if (password) { + accountUpdate = { password }; + } + + if (fullName) { + accountUpdate = { data: { fullName } }; + } + + const { data, error } = await supabase.auth.updateUser(accountUpdate!); + + if (error) { + console.error(error.message); + throw new Error(error.message); + } + + return data; +}; + +export { login, logout, getCurrentAccount, updateAccount }; diff --git a/hotel-management/src/services/bookingServices.ts b/hotel-management/src/services/bookingServices.ts new file mode 100644 index 0000000..b8cac0e --- /dev/null +++ b/hotel-management/src/services/bookingServices.ts @@ -0,0 +1,155 @@ +// 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 booking to database + * @param booking The booking 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 booking in database + * @param idRoom The id of booking 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); + } +}; + +/** + * Check out booking services + * @param param0 The ICheckOutBooking object + * @returns The data of booking after insert to database + */ +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 ed0a843..ed78b85 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -1,38 +1,58 @@ // Types -import { IRoom } from '@type/rooms'; -import { IDataState } from '@type/common'; +import { IRoom } from '@type/room'; // Services import supabase from './supabaseService'; // Constants -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 { DEFAULT_PAGE_SIZE } from '@constant/config'; +import { + ERROR_CREATE_ROOM, + ERROR_DELETE_ROOM, + ERROR_FETCHING_ROOM, + ERROR_UPDATE_ROOM, + ROOMS_TABLE, +} from '@constant/messages'; + +interface IGetAllRooms { + sortBy: string; + orderBy: string; + roomSearch: string; + page: number; +} /** * Get all rooms from database * @returns Return all rooms in database */ -const getAllRooms = async ( - sortBy: string, - orderBy: string, - roomName: string -): Promise => { - const { data, error } = await supabase +const getAllRooms = async ({ + sortBy, + orderBy, + roomSearch, + page, +}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => { + const from = (page - 1) * DEFAULT_PAGE_SIZE; + const to = from + DEFAULT_PAGE_SIZE - 1; + + let query = supabase .from(ROOMS_TABLE) - .select('*') + .select('*', { count: 'exact' }) .order(sortBy, { ascending: orderBy === 'asc' }) - .ilike('name', `%${roomName}%`); + .like('name', `%${roomSearch}%`) + .eq('isDelete', false); + + 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; + return { data, count }; }; /** @@ -75,30 +95,67 @@ const createRoom = async (room: IRoom): Promise => { }; /** - * Delete room in database - * @param idRoom The id of room need to delete + * Set room is delete status + * @param idRoom The id of room need to set */ -const deleteRoom = async (idRoom: number) => { - const { error } = await supabase.from(ROOMS_TABLE).delete().eq('id', idRoom); +const setIsDeleteRoom = async (idRoom: number) => { + const { data, error } = await supabase + .from(ROOMS_TABLE) + .update({ isDelete: true }) + .eq('id', idRoom) + .select() + .single(); if (error) { console.error(error.message); throw new Error(ERROR_DELETE_ROOM); } + + return data; }; -const getRoomsAvailable = async (): Promise => { +/** + * 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) - .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 }; +/** + * Update status of room + * @param id The id of room need to be change status + * @param status The status of room + */ +const updateRoomStatus = async (id: number, status: boolean): Promise => { + const { error } = await supabase + .from(ROOMS_TABLE) + .update({ status }) + .eq('id', id); + + if (error) { + console.error(error.message); + throw new Error(ERROR_UPDATE_ROOM); + } +}; + +export { + getAllRooms, + updateRoom, + createRoom, + setIsDeleteRoom, + getRoomById, + updateRoomStatus, +}; diff --git a/hotel-management/src/services/userServices.ts b/hotel-management/src/services/userServices.ts index 62d2fc9..1af1d12 100644 --- a/hotel-management/src/services/userServices.ts +++ b/hotel-management/src/services/userServices.ts @@ -1,14 +1,17 @@ // Types -import { IUser } from '@type/users'; -import { IDataState } from '@type/common'; +import { IUser } from '@type/user'; // Services import supabase from './supabaseService'; - -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 { IDataState } from '@type/common'; +import { DEFAULT_PAGE_SIZE } from '@constant/config'; +import { + ERROR_CREATE_USER, + ERROR_DELETE_USER, + ERROR_FETCHING_USER, + ERROR_UPDATE_USER, + USERS_TABLE, +} from '@constant/messages'; /** * Create user to the database @@ -21,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); @@ -49,6 +54,33 @@ const updateUser = async (user: IUser): Promise => { return data; }; +/** + * Set user in delete status + * @param idUser The id of user need to delete + */ +const setIsDeleteUser = async (idUser: number) => { + const { data, error } = await supabase + .from(USERS_TABLE) + .update({isDelete: true}) + .eq('id', idUser) + .select() + .single(); + + if (error) { + console.error(error.message); + throw new Error(ERROR_DELETE_USER); + } + + return data; +}; + +interface IGetAllUsers { + sortBy: string; + orderBy: string; + phoneSearch: string; + page: number; +} + /** * Return data of users from database * @param sortBy Sort by column @@ -56,37 +88,71 @@ 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 -): Promise => { - const { data, error } = await supabase +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; + + let query = supabase .from(USERS_TABLE) - .select('*') + .select('*', { count: 'exact' }) .order(sortBy, { ascending: orderBy === 'asc' }) - .ilike('phone', `%${phoneSearch}%`); + .like('phone', `%${phoneSearch}%`) + .eq('isDelete', false); + + 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; + return { data, count }; }; 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, + setIsDeleteUser, + getUserNotBooked, + updateUserBookedStatus, +}; diff --git a/hotel-management/src/styles/abstracts/variables.css b/hotel-management/src/styles/abstracts/variables.css index 8e38691..8ced783 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; @@ -22,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/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 }; 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 b98d8b5..08e954e 100644 --- a/hotel-management/src/types/common.ts +++ b/hotel-management/src/types/common.ts @@ -3,6 +3,15 @@ type Nullable = T | null; interface IDataState { id: number; name?: string; + status?: boolean; + isBooked?: boolean; } -export type { Nullable, IDataState }; +interface ILogin { + email: string; + password: string; +} + +type TDirection = 'vertical' | 'horizontal'; + +export type { Nullable, IDataState, TDirection, ILogin }; diff --git a/hotel-management/src/types/rooms.ts b/hotel-management/src/types/room.ts similarity index 83% rename from hotel-management/src/types/rooms.ts rename to hotel-management/src/types/room.ts index 63234af..f356993 100644 --- a/hotel-management/src/types/rooms.ts +++ b/hotel-management/src/types/room.ts @@ -3,6 +3,7 @@ interface IRoom { name: string; price: number; status: boolean; + isDelete: boolean; } export type { IRoom }; diff --git a/hotel-management/src/types/supabase.ts b/hotel-management/src/types/supabase.ts index f19cbb3..ed1784f 100644 --- a/hotel-management/src/types/supabase.ts +++ b/hotel-management/src/types/supabase.ts @@ -25,7 +25,7 @@ export interface Database { id?: number roomId: number startDate: string - status: boolean + status?: boolean userId: number } Update: { @@ -57,18 +57,21 @@ export interface Database { rooms: { Row: { id: number + isDelete: boolean name: string price: number status: boolean } Insert: { id?: number + isDelete?: boolean name: string price: number status?: boolean } Update: { id?: number + isDelete?: boolean name?: string price?: number status?: boolean @@ -79,18 +82,21 @@ export interface Database { Row: { id: number isBooked: boolean + isDelete: boolean name: string phone: string } Insert: { id?: number isBooked?: boolean + isDelete?: boolean name: string phone: string } Update: { id?: number isBooked?: boolean + isDelete?: boolean name?: string phone?: string } @@ -111,3 +117,83 @@ export interface Database { } } } + +export type Tables< + PublicTableNameOrOptions extends + | keyof (Database["public"]["Tables"] & Database["public"]["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (Database["public"]["Tables"] & + Database["public"]["Views"]) + ? (Database["public"]["Tables"] & + Database["public"]["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends + | keyof Database["public"]["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof Database["public"]["Tables"] + ? Database["public"]["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends + | keyof Database["public"]["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof Database["public"]["Tables"] + ? Database["public"]["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends + | keyof Database["public"]["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + : never = never +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof Database["public"]["Enums"] + ? Database["public"]["Enums"][PublicEnumNameOrOptions] + : never diff --git a/hotel-management/src/types/users.ts b/hotel-management/src/types/user.ts similarity index 84% rename from hotel-management/src/types/users.ts rename to hotel-management/src/types/user.ts index 0f9517b..a64f2c7 100644 --- a/hotel-management/src/types/users.ts +++ b/hotel-management/src/types/user.ts @@ -3,6 +3,7 @@ interface IUser { name: string; phone: string; isBooked: boolean; + isDelete: boolean; } export type { IUser }; 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"],