diff --git a/hotel-management/package.json b/hotel-management/package.json index 09732e8..2c613bd 100644 --- a/hotel-management/package.json +++ b/hotel-management/package.json @@ -49,6 +49,7 @@ "husky": "^8.0.0", "lint-staged": "^15.0.1", "typescript": "^5.0.2", - "vite": "^4.4.5" + "vite": "^4.4.5", + "vite-tsconfig-paths": "^4.2.1" } } diff --git a/hotel-management/pnpm-lock.yaml b/hotel-management/pnpm-lock.yaml index ab6d085..428134c 100644 --- a/hotel-management/pnpm-lock.yaml +++ b/hotel-management/pnpm-lock.yaml @@ -70,6 +70,9 @@ devDependencies: vite: specifier: ^4.4.5 version: 4.4.11 + vite-tsconfig-paths: + specifier: ^4.2.1 + version: 4.2.1(typescript@5.2.2)(vite@4.4.11) packages: @@ -1406,6 +1409,10 @@ packages: slash: 3.0.0 dev: true + /globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + /goober@2.1.13(csstype@3.1.2): resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==} peerDependencies: @@ -2088,6 +2095,19 @@ packages: typescript: 5.2.2 dev: true + /tsconfck@2.1.2(typescript@5.2.2): + resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} + engines: {node: ^14.13.1 || ^16 || >=18} + hasBin: true + peerDependencies: + typescript: ^4.3.5 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.2.2 + dev: true + /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: false @@ -2132,6 +2152,23 @@ packages: punycode: 2.3.0 dev: true + /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.4.11): + resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + debug: 4.3.4 + globrex: 0.1.2 + tsconfck: 2.1.2(typescript@5.2.2) + vite: 4.4.11 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /vite@4.4.11: resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/hotel-management/src/commons/styles/ButtonIcon.ts b/hotel-management/src/commons/styles/ButtonIcon.ts deleted file mode 100644 index 7dc043e..0000000 --- a/hotel-management/src/commons/styles/ButtonIcon.ts +++ /dev/null @@ -1,22 +0,0 @@ -import styled from 'styled-components'; - -const ButtonIcon = styled.button` - background: none; - - padding: 8px; - transition: all 0.2s; - - border: none; - border-radius: var(--radius-md); - - &:hover { - background-color: var(--hover-background-color); - } - - & svg { - width: 25px; - height: 25px; - } -`; - -export default ButtonIcon; diff --git a/hotel-management/src/components/AppLayout/index.tsx b/hotel-management/src/components/AppLayout/index.tsx index d2a0239..231cff6 100644 --- a/hotel-management/src/components/AppLayout/index.tsx +++ b/hotel-management/src/components/AppLayout/index.tsx @@ -1,8 +1,8 @@ import { Outlet } from 'react-router-dom'; // Components -import Header from '../Header'; -import Sidebar from '../Sidebar'; +import Header from '@component/Header'; +import Sidebar from '@component/Sidebar'; // Styled import { Main, StyledAppLayout } from './styled'; diff --git a/hotel-management/src/components/ButtonIcon/ButtonIcon.tsx b/hotel-management/src/components/ButtonIcon/ButtonIcon.tsx new file mode 100644 index 0000000..8375518 --- /dev/null +++ b/hotel-management/src/components/ButtonIcon/ButtonIcon.tsx @@ -0,0 +1,131 @@ +import { COLOR, SIZE } from '@constant/styles'; +import { ReactNode } from 'react'; +import styled, { css } from 'styled-components'; + +interface IStyledButtonIcon { + isHaveChildren: boolean; + style?: { + fontSize?: string; + backgroundColor?: string; + color?: string; + }; + iconStyle?: { + color?: string; + size?: string; + }; +} + +const StyledButtonIcon = styled.button` + background: none; + + padding: 10px; + transition: all 0.2s; + + border: none; + + ${(props) => + props.style?.fontSize && + css` + font-size: ${props.style.fontSize}; + `} + + ${(props) => + props.style?.fontSize && + css` + font-size: ${props.style.fontSize}; + `} + + ${(props) => + props.color && + css` + font-size: ${props.color}; + `} + + ${(props) => + props.isHaveChildren + ? css` + display: flex; + align-items: center; + gap: 10px; + + text-align: left; + width: 100%; + ` + : css` + border-radius: var(--radius-md); + `} + + &:hover { + background-color: var(--hover-background-color); + } + + & svg { + ${(props) => + props.iconStyle?.size && + css` + width: ${props.iconStyle.size}; + height: ${props.iconStyle.size}; + `} + + ${(props) => + props.iconStyle?.color && + css` + color: ${props.iconStyle.color}; + `} + transition: all 0.3s; + } +`; + +StyledButtonIcon.defaultProps = { + style: { + fontSize: SIZE.DEFAULT, + backgroundColor: COLOR.DEFAULT, + color: COLOR.BLACK, + }, + iconStyle: { + color: COLOR.BLACK, + size: SIZE.DEFAULT, + }, +}; + +interface IButtonIcon { + icon: ReactNode; + children?: ReactNode; + style?: { + fontSize?: string; + backgroundColor?: string; + color?: string; + }; + iconStyle?: { + color?: string; + size?: string; + }; + onClick?: () => void; +} + +const ButtonIcon = ({ + icon, + children, + style, + iconStyle, + onClick, +}: IButtonIcon) => { + const isHaveChildren = Boolean(children); + + return ( + + {icon} {children} + + ); +}; + +export default ButtonIcon; diff --git a/hotel-management/src/components/ButtonIcon/styled.ts b/hotel-management/src/components/ButtonIcon/styled.ts new file mode 100644 index 0000000..e69de29 diff --git a/hotel-management/src/components/Dialog/index.tsx b/hotel-management/src/components/Dialog/index.tsx deleted file mode 100644 index a58f982..0000000 --- a/hotel-management/src/components/Dialog/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { MutableRefObject, ReactNode, forwardRef } from 'react'; - -// Styled -import { StyledBody, StyledDialog, StyledTitle } from './styled'; - -// Type -import { Nullable } from '../../types/common'; - -export interface IDialogProps { - title?: string; - children?: ReactNode; - onClose?: () => void; - ref?: MutableRefObject>; -} - -const Dialog = forwardRef( - (props: IDialogProps, ref) => { - const { title, children, onClose } = props; - - return ( - - {title} - {children} - - ); - } -); - -export default Dialog; diff --git a/hotel-management/src/components/Dialog/styled.ts b/hotel-management/src/components/Dialog/styled.ts deleted file mode 100644 index 700551b..0000000 --- a/hotel-management/src/components/Dialog/styled.ts +++ /dev/null @@ -1,23 +0,0 @@ -import styled from 'styled-components'; - -const StyledDialog = styled.dialog` - border-radius: var(--radius-sm); - border-color: var(--border-color); -`; - -const StyledTitle = styled.p` - font-size: var(--fs-md); - text-transform: capitalize; - font-weight: 600; - - padding: 20px 40px; - border-bottom: 1px solid var(--border-color); - - text-align: center; -`; - -const StyledBody = styled.div` - padding: 10px 20px; -`; - -export { StyledDialog, StyledTitle, StyledBody }; diff --git a/hotel-management/src/components/HeaderMenu/index.tsx b/hotel-management/src/components/HeaderMenu/index.tsx index 3e24e4b..8365978 100644 --- a/hotel-management/src/components/HeaderMenu/index.tsx +++ b/hotel-management/src/components/HeaderMenu/index.tsx @@ -1,7 +1,7 @@ // Components import { IoPersonCircleOutline } from 'react-icons/io5'; import { HiOutlineLogout } from 'react-icons/hi'; -import ButtonIcon from '../../commons/styles/ButtonIcon'; +import ButtonIcon from '@component/ButtonIcon/ButtonIcon'; // Styled import { StyledHeaderMenu } from './styled'; @@ -10,14 +10,18 @@ const HeaderMenu = () => { return (
  • - - - + } + iconStyle={{ size: '23px' }} + />
  • - - - + } + iconStyle={{size: '23px' }} + />
  • ); diff --git a/hotel-management/src/components/Menus/index.tsx b/hotel-management/src/components/Menus/index.tsx index 629c97d..e985f51 100644 --- a/hotel-management/src/components/Menus/index.tsx +++ b/hotel-management/src/components/Menus/index.tsx @@ -4,16 +4,18 @@ import { MouseEvent, ReactNode, useContext, useState } from 'react'; import { HiEllipsisVertical } from 'react-icons/hi2'; // Hooks -import { useOutsideClick } from '../../hooks/useOutsideClick'; +import { useOutsideClick } from '@hook/useOutsideClick'; // Styled -import { StyledMenu, StyledButton, StyledList, StyledToggle } from './styled'; +import { StyledMenu, StyledList, StyledToggle } from './styled'; // Contexts -import MenusContext from '../../contexts/MenuContext'; +import MenusContext from '@context/MenuContext'; // Types -import { Nullable } from '../../types/common'; +import { Nullable } from '@type/common'; +import ButtonIcon from '@component/ButtonIcon/ButtonIcon'; +import { COLOR } from '@constant/styles'; interface IButton { children?: string; @@ -58,7 +60,7 @@ const List = ({ children: ReactNode; }): Nullable => { const { openId, close } = useContext(MenusContext); - const ref = useOutsideClick(close!, false); + const ref = useOutsideClick(close!, false); if (openId !== id) return null; @@ -74,9 +76,14 @@ const Button = ({ children, icon, onClick }: IButton): ReactNode => { return (
  • - - {icon} {children} - + + {children} +
  • ); }; diff --git a/hotel-management/src/components/Modal/index.tsx b/hotel-management/src/components/Modal/index.tsx new file mode 100644 index 0000000..add80a6 --- /dev/null +++ b/hotel-management/src/components/Modal/index.tsx @@ -0,0 +1,75 @@ +import { + ReactElement, + ReactNode, + cloneElement, + useContext, + useState, +} from 'react'; +import { createPortal } from 'react-dom'; + +// Hooks +import { useOutsideClick } from '@hook/useOutsideClick'; + +// Contexts +import { ModalContext } from '@context/ModalContext'; + +// Styled +import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled'; + +interface IModal { + children: ReactNode; +} + +const Modal = ({ children }: IModal) => { + const [openName, setOpenName] = useState(''); + + const close = () => setOpenName(''); + const open = setOpenName; + + return ( + + {children} + + ); +}; + +interface IOpen { + renderChildren: (onCloseModal: () => void) => ReactNode; + modalName: string; +} + +const Open = ({ renderChildren, modalName }: IOpen) => { + const { open } = useContext(ModalContext); + + return renderChildren(() => open!(modalName)); +}; + +interface IWindow { + children: ReactElement; + name: string; + title: string; +} + +const Window = ({ children, name, title }: IWindow) => { + const { openName, close } = useContext(ModalContext); + const ref = useOutsideClick(close!); + + if (name !== openName) return null; + + return createPortal( + + + + {title} + {cloneElement(children, { onCloseModal: close })} + + + , + document.body + ); +}; + +Modal.Open = Open; +Modal.Window = Window; + +export default Modal; diff --git a/hotel-management/src/components/Modal/styled.ts b/hotel-management/src/components/Modal/styled.ts new file mode 100644 index 0000000..1b1cc0d --- /dev/null +++ b/hotel-management/src/components/Modal/styled.ts @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +const StyledModal = styled.div` + position: fixed; + top: 50%; + 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; +`; + +const StyledModalContent = styled.div` + background-color: var(--form-color); + + border-radius: var(--radius-md); + + padding: 30px 30px; +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + + width: 100%; + height: 100vh; + + background-color: var(--overlay-color); + + z-index: 1000; + transition: all 0.5s; +`; + +const TitleModal = styled.p` + font-size: var(--fs-md); + font-weight: 600; + text-align: center; + + margin-bottom: 30px; +` + +export { StyledModal, Overlay, StyledModalContent, TitleModal }; diff --git a/hotel-management/src/components/Nav/index.tsx b/hotel-management/src/components/Nav/index.tsx index 6ce3f2a..7e0e452 100644 --- a/hotel-management/src/components/Nav/index.tsx +++ b/hotel-management/src/components/Nav/index.tsx @@ -7,7 +7,7 @@ import { BsHouseDoorFill } from 'react-icons/bs'; import { StyledNav, StyledNavLink } from './styled'; // Constants -import { DASHBOARD, ROOM, USER } from '../../constants/path'; +import { DASHBOARD, ROOM, USER } from '@constant/path'; const Nav = () => { return ( diff --git a/hotel-management/src/components/OrderBy/index.tsx b/hotel-management/src/components/OrderBy/index.tsx index c24df2e..f4ceaa8 100644 --- a/hotel-management/src/components/OrderBy/index.tsx +++ b/hotel-management/src/components/OrderBy/index.tsx @@ -1,4 +1,3 @@ -import { memo } from 'react'; import { useSearchParams } from 'react-router-dom'; // Styled @@ -11,7 +10,7 @@ interface IOrderProps { }[]; } -const OrderBy = memo(({ options }: IOrderProps) => { +const OrderBy = ({ options }: IOrderProps) => { const field = 'orderBy'; const [searchParams, setSearchParams] = useSearchParams(); const currentOrder = searchParams.get(field) || options[0].value; @@ -35,6 +34,6 @@ const OrderBy = memo(({ options }: IOrderProps) => { ))} ); -}); +}; export default OrderBy; diff --git a/hotel-management/src/components/Search/index.tsx b/hotel-management/src/components/Search/index.tsx index 06d3bbb..b60db2c 100644 --- a/hotel-management/src/components/Search/index.tsx +++ b/hotel-management/src/components/Search/index.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { StyledSearch } from './styled'; // Hooks -import { useDebounce } from '../../hooks/useDebounce'; +import { useDebounce } from '@hook/useDebounce'; interface ISearch { setPlaceHolder: string; diff --git a/hotel-management/src/components/Select/index.tsx b/hotel-management/src/components/Select/index.tsx index 8a59409..720c0f0 100644 --- a/hotel-management/src/components/Select/index.tsx +++ b/hotel-management/src/components/Select/index.tsx @@ -1,10 +1,9 @@ import { FieldValues, RegisterOptions, - UseFormRegister, useFormContext, } from 'react-hook-form'; -import { ChangeEventHandler, ReactNode } from 'react'; +import { ChangeEventHandler } from 'react'; // Styled import { StyledSelect } from './styled'; @@ -22,46 +21,6 @@ interface ISelect { ariaLabel: string; } -interface IRender extends ISelect { - register: UseFormRegister; - children: ReactNode[]; -} - -const RenderSelect = ({ - options, - id, - value, - optionsConfigForm, - onChange, - register, - children, - ariaLabel, -}: IRender) => { - if (options && !register) { - return ( - - {children} - - ); - } - - return ( - - {children} - - ); -}; - const Select = ({ options, id, @@ -75,21 +34,20 @@ const Select = ({ if (!options) return; return ( - {options.map((option) => ( ))} - + ); }; diff --git a/hotel-management/src/components/Sidebar/index.tsx b/hotel-management/src/components/Sidebar/index.tsx index 3c14f06..b393109 100644 --- a/hotel-management/src/components/Sidebar/index.tsx +++ b/hotel-management/src/components/Sidebar/index.tsx @@ -1,5 +1,5 @@ // Components -import Nav from '../Nav'; +import Nav from '@component/Nav'; // Styled import { Heading, StyledSidebar } from './styled'; diff --git a/hotel-management/src/components/SortBy/index.tsx b/hotel-management/src/components/SortBy/index.tsx index ca85296..1d29c41 100644 --- a/hotel-management/src/components/SortBy/index.tsx +++ b/hotel-management/src/components/SortBy/index.tsx @@ -1,8 +1,8 @@ import { useSearchParams } from 'react-router-dom'; -import { ChangeEvent, memo } from 'react'; +import { ChangeEvent } from 'react'; // Components -import Select from '../Select'; +import Select from '@component/Select'; interface ISortByProps { options: { @@ -11,7 +11,7 @@ interface ISortByProps { }[]; } -const SortBy = memo(({ options }: ISortByProps) => { +const SortBy = ({ options }: ISortByProps) => { const [searchParams, setSearchParams] = useSearchParams(); const sortBy = searchParams.get('sortBy') || ''; const handleChange = (event: ChangeEvent) => { @@ -27,6 +27,6 @@ const SortBy = memo(({ options }: ISortByProps) => { ariaLabel="Sort" /> ); -}); +}; export default SortBy; diff --git a/hotel-management/src/components/Table/index.tsx b/hotel-management/src/components/Table/index.tsx index 0f16658..1cea173 100644 --- a/hotel-management/src/components/Table/index.tsx +++ b/hotel-management/src/components/Table/index.tsx @@ -4,13 +4,17 @@ import { ReactNode, useContext } from 'react'; import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled'; // Contexts -import TableContext from '../../contexts/TableContext'; +import TableContext from '@context/TableContext'; export interface ITable { columns?: string; children: ReactNode; } +export interface IHeader { + headerColumn: string[]; +} + interface ITableBody { data?: T[]; render?: CallbackMapFunc; @@ -26,10 +30,12 @@ const Table = ({ columns, children }: ITable) => { ); }; -const Header = ({ children }: ITable) => { +const Header = ({ headerColumn }: IHeader) => { const { columns } = useContext(TableContext); - return {children}; + return + {headerColumn.map((item) =>
    {item}
    )} +
    ; }; const Body = ({ data, render }: ITableBody) => { diff --git a/hotel-management/src/constants/variables.ts b/hotel-management/src/constants/commons.ts similarity index 82% rename from hotel-management/src/constants/variables.ts rename to hotel-management/src/constants/commons.ts index 426425d..eaaca99 100644 --- a/hotel-management/src/constants/variables.ts +++ b/hotel-management/src/constants/commons.ts @@ -51,9 +51,6 @@ const ROOM_PAGE = { ], }; -const VALUE = 'value'; -const ERROR = 'error'; - const INIT_VALUE_USER_FORM = { name: '', id: 0, @@ -68,11 +65,23 @@ const INIT_VALUE_ROOM_FORM = { discount: 0, }; +const FORM = { + EDIT: 'edit', + USER: 'user-form', + ROOM: 'room-form', +}; + +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]*$/, +}; + export { USER_PAGE, ROOM_PAGE, - VALUE, - ERROR, + FORM, + REGEX, ORDERBY_OPTIONS, INIT_VALUE_USER_FORM, INIT_VALUE_ROOM_FORM, diff --git a/hotel-management/src/constants/styles.ts b/hotel-management/src/constants/styles.ts new file mode 100644 index 0000000..2144a4c --- /dev/null +++ b/hotel-management/src/constants/styles.ts @@ -0,0 +1,11 @@ +const COLOR = { + PRIMARY: '#0010ba', + BLACK: '#000', + DEFAULT: 'transparent', +}; + +const SIZE = { + DEFAULT: '14px', +}; + +export { COLOR, SIZE }; diff --git a/hotel-management/src/contexts/ModalContext.ts b/hotel-management/src/contexts/ModalContext.ts new file mode 100644 index 0000000..d7a529a --- /dev/null +++ b/hotel-management/src/contexts/ModalContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +interface IModalContext { + openName?: string; + close?: () => void; + open?: React.Dispatch>; +} + +const ModalContext = createContext({}); + +export { ModalContext }; diff --git a/hotel-management/src/helpers/sendRequest.ts b/hotel-management/src/helpers/sendRequest.ts index 329645b..717f7f0 100644 --- a/hotel-management/src/helpers/sendRequest.ts +++ b/hotel-management/src/helpers/sendRequest.ts @@ -1,8 +1,8 @@ // Constants -import { BASE_URL } from '../constants/path'; +import { BASE_URL } from '@constant/path'; // Types -import { IResponse } from '../types/responses'; +import { IResponse } from '@type/responses'; type TMethodRequest = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; diff --git a/hotel-management/src/helpers/validators.ts b/hotel-management/src/helpers/validators.ts index ae3c974..25ecace 100644 --- a/hotel-management/src/helpers/validators.ts +++ b/hotel-management/src/helpers/validators.ts @@ -1,20 +1,12 @@ /** - * Check value is valid name or not - * @param value Values need to be checked - * @returns A boolean indicating whether or not the argument has valid - */ -const isValidName = (value: string): boolean => { - return /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm.test(value); -}; - -/** - * Check value is valid number or not + * Compare the regex with value + * @param regex The regex to compare * @param value Value need to be checked - * @returns A boolean indicating whether or not the argument has valid + * @returns A boolean indicating whether of not the argument has valid */ -const isValidNumber = (value: string): boolean => { - return /^[0-9]*$/.test(value); -}; +const isValidRegex = (regex: RegExp, value: string): boolean => { + return regex.test(value); +} /** * Check value is valid discount or not @@ -25,14 +17,6 @@ const isValidDiscount = (value: number): boolean => { return value >= 0 && value <= 100; }; -/** - * Check value is valid phone number or not - * @param value Value need to be checked - * @returns A boolean indicating whether or not the argument has valid - */ -const isValidPhoneNumber = (value: string): boolean => { - return /(84|0[3|5|7|8|9])+([0-9]{8})\b/g.test(value); -}; /** * Check value is valid string or not @@ -43,12 +27,6 @@ const isValidString = (value: string): boolean => { return value.trim().length >= 5; }; -/** - * A method skip check value - * @returns Return true - */ -const skipCheck = () => true; - /** * Check object is empty or not * @param obj Object need to be check @@ -59,11 +37,8 @@ const isEmptyObj = (obj: object) => { }; export { - isValidName, - isValidNumber, - isValidPhoneNumber, + isValidRegex, isValidString, - skipCheck, isValidDiscount, isEmptyObj, }; diff --git a/hotel-management/src/hooks/useFetch.ts b/hotel-management/src/hooks/useFetch.ts index 70cd78e..39b6b02 100644 --- a/hotel-management/src/hooks/useFetch.ts +++ b/hotel-management/src/hooks/useFetch.ts @@ -1,14 +1,14 @@ import { useEffect, useState } from 'react'; // Constants -import { BASE_URL } from '../constants/path'; -import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config'; +import { BASE_URL } from '@constant/path'; +import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '@constant/config'; // Helpers -import { searchQuery } from '../helpers/helper'; +import { searchQuery } from '@helper/helper'; // Types -import { Nullable } from '../types/common'; +import { Nullable } from '@type/common'; /** * The function use to fetch data on server API. diff --git a/hotel-management/src/hooks/useForwardRef.ts b/hotel-management/src/hooks/useForwardRef.ts index e1ed2eb..e333401 100644 --- a/hotel-management/src/hooks/useForwardRef.ts +++ b/hotel-management/src/hooks/useForwardRef.ts @@ -1,7 +1,7 @@ import { ForwardedRef, useEffect, useRef } from 'react'; // Types -import { Nullable } from '../types/common'; +import { Nullable } from '@type/common'; const useForwardRef = ( ref: ForwardedRef, diff --git a/hotel-management/src/hooks/useOutsideClick.ts b/hotel-management/src/hooks/useOutsideClick.ts index 76858ee..84a2248 100644 --- a/hotel-management/src/hooks/useOutsideClick.ts +++ b/hotel-management/src/hooks/useOutsideClick.ts @@ -1,7 +1,7 @@ import { MutableRefObject, useEffect, useRef } from 'react'; // Types -import { Nullable } from '../types/common'; +import { Nullable } from '@type/common'; /** * Custom hook to call handler when click outside element @@ -9,11 +9,11 @@ import { Nullable } from '../types/common'; * @param listeningCapturing A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. If not specified, defaults to false. * @returns Return a mutable ref object */ -export const useOutsideClick = ( +export const useOutsideClick = ( handler: () => void, listeningCapturing = true -): MutableRefObject> => { - const ref = useRef(null); +): MutableRefObject> => { + const ref = useRef(null); useEffect(() => { const handleClick = (e: Event) => { diff --git a/hotel-management/src/pages/Room/Dialog.tsx b/hotel-management/src/pages/Room/Dialog.tsx deleted file mode 100644 index 4f9adf9..0000000 --- a/hotel-management/src/pages/Room/Dialog.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - Dispatch, - MutableRefObject, - SetStateAction, - forwardRef, - useEffect, -} from 'react'; - -// Components -import Dialog from '../../components/Dialog'; -import RoomForm from './Form'; - -// Types -import { Nullable } from '../../types/common'; -import { IRoom } from '../../types/rooms'; - -// Hooks -import { useForwardRef } from '../../hooks/useForwardRef'; - -interface IRoomDialog { - onClose?: () => void; - reload?: boolean; - setReload?: Dispatch>; - ref?: MutableRefObject>; - room?: Nullable; - isAdd?: boolean; -} - -const RoomDialog = forwardRef( - (props: IRoomDialog, ref) => { - const dialogRef = useForwardRef(ref); - const { - onClose, - setReload, - reload, - room, - isAdd - } = props; - - useEffect(() => { - if (dialogRef.current) { - dialogRef.current.addEventListener('click', (e: MouseEvent) => { - const dialogDimensions = dialogRef.current!.getBoundingClientRect(); - if ( - e.clientX < dialogDimensions.left || - e.clientX > dialogDimensions.right || - e.clientY < dialogDimensions.top || - e.clientY > dialogDimensions.bottom - ) { - dialogRef.current!.close(); - } - }); - } - }, [dialogRef]); - - return ( - - - - ); - } -); - -export default RoomDialog; diff --git a/hotel-management/src/pages/Room/RomTable.tsx b/hotel-management/src/pages/Room/RomTable.tsx new file mode 100644 index 0000000..f6dd170 --- /dev/null +++ b/hotel-management/src/pages/Room/RomTable.tsx @@ -0,0 +1,104 @@ +import { useSearchParams } from 'react-router-dom'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +// Components +import Menus from '@component/Menus'; +import Table from '@component/Table'; +import Message from '@component/Message'; +import Search from '@component/Search'; +import SortBy from '@component/SortBy'; +import OrderBy from '@component/OrderBy'; +import RoomRow from './RoomRow'; + +// Types +import { IRoom } from '@type/rooms'; + +// Constants +import { useFetch } from '@hook/useFetch'; +import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons'; + +// Styled +import { StyledOperationTable } from './styled'; +import Direction from '@commonStyle/Direction'; +import Spinner from '@commonStyle/Spinner'; + + +interface IRoomTable { + reload: boolean; + setReload: Dispatch>; +} + +const RoomTable = ({ reload, setReload }: IRoomTable) => { + const columnName = ['Id', 'Name', 'Price', 'Status']; + const [nameSearch, setNameSearch] = useState(''); + const [searchParams] = useSearchParams(); + const [rooms, setRooms] = useState([]); + const sortByValue = searchParams.get('sortBy') + ? searchParams.get('sortBy')! + : ''; + const orderByValue = searchParams.get('orderBy') + ? searchParams.get('orderBy')! + : ''; + + const { data, isPending, errorFetchMsg } = useFetch( + 'rooms', + 'name', + nameSearch, + sortByValue, + orderByValue, + reload + ); + + useEffect(() => { + if (data) { + setRooms(data); + } else { + setRooms([]); + } + + if (errorFetchMsg) { + console.error(errorFetchMsg); + } + }, [data, errorFetchMsg]); + + return ( + <> + + + + + + + + + {isPending && } + + {rooms.length ? ( + + + + + data={rooms} + render={(room: IRoom) => ( + + )} + /> +
    +
    + ) : ( + !isPending && No data to show here! + )} +
    + + ); +}; + +export default RoomTable; diff --git a/hotel-management/src/pages/Room/Form.tsx b/hotel-management/src/pages/Room/RoomForm.tsx similarity index 57% rename from hotel-management/src/pages/Room/Form.tsx rename to hotel-management/src/pages/Room/RoomForm.tsx index 76042fb..3f90f55 100644 --- a/hotel-management/src/pages/Room/Form.tsx +++ b/hotel-management/src/pages/Room/RoomForm.tsx @@ -6,36 +6,35 @@ import styled from 'styled-components'; import { useForm } from 'react-hook-form'; // Styled -import Button from '../../commons/styles/Button.ts'; -import Input from '../../commons/styles/Input.ts'; +import Button from '@commonStyle/Button.ts'; +import Input from '@commonStyle/Input.ts'; // Types -import { Nullable } from '../../types/common'; -import { IRoom } from '../../types/rooms'; +import { Nullable } from '@type/common.ts'; +import { IRoom } from '@type/rooms.ts'; // Constants -import { - ADD_SUCCESS, - EDIT_SUCCESS, -} from '../../constants/messages.ts'; +import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts'; import { INVALID_DISCOUNT, INVALID_FIELD, REQUIRED_FIELD_ERROR, -} from '../../constants/formValidateMessage.ts'; +} from '@constant/formValidateMessage.ts'; +import { INIT_VALUE_ROOM_FORM, REGEX } from '../../constants/commons.ts'; // Helpers import { isValidDiscount, - isValidNumber, + isValidRegex, isValidString, -} from '../../helpers/validators.ts'; +} from '@helper/validators.ts'; // Components -import FormRow from '../../components/LabelControl/index.tsx'; -import Form from '../../components/Form/index.tsx'; -import { addRoom, updateRoom } from '../../services/roomServices.ts'; -import { INIT_VALUE_ROOM_FORM } from '../../constants/variables.ts'; +import FormRow from '@component/LabelControl/index.tsx'; +import Form from '@component/Form/index.tsx'; + +// Services +import { addRoom, updateRoom } from '@service/roomServices.ts'; const FormBtn = styled(Button)` width: 100%; @@ -48,68 +47,61 @@ const FormBtn = styled(Button)` `; interface IRoomFormProp { - onClose: () => void; - reload: boolean; - setReload: Dispatch>; - room?: Nullable; - isAdd: boolean; + onCloseModal?: () => void; + reload?: boolean; + setReload?: Dispatch>; + roomEdit?: Nullable; } const RoomForm = ({ - onClose, + onCloseModal, reload, setReload, - room, - isAdd, + roomEdit, }: IRoomFormProp) => { const formMethods = useForm(); const { register, handleSubmit, reset, - formState: { errors, isDirty, isValid }, + formState: { errors, isDirty, isValid, isSubmitting }, trigger, } = formMethods; useEffect(() => { - if (room && !isAdd) { - reset(room); + if (roomEdit) { + reset(roomEdit); } else { reset(INIT_VALUE_ROOM_FORM); } - }, [room, reset, isAdd]); + }, [roomEdit, reset]); // Submit form const onSubmit = async (room: IRoom) => { // Calculate final price room.finalPrice = room.price - (room.price * room.discount) / 100; - try { - if (isAdd) { - // Add request - const response = await addRoom(room); - - if(response) { - toast.success(ADD_SUCCESS); - } - } else { - // Edit request - const response = await updateRoom(room); + if (!roomEdit) { + // Add request + const response = await addRoom(room); - if (response) { - toast.success(EDIT_SUCCESS); - } + if (response) { + toast.success(ADD_SUCCESS); } - // Reload table data - setReload(!reload); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); + } else { + // Edit request + const response = await updateRoom(room); + + if (response) { + toast.success(EDIT_SUCCESS); } } + // Reload table data + setReload!(!reload); + reset(); - onClose(); + onCloseModal!(); }; return ( @@ -122,7 +114,7 @@ const RoomForm = ({ {...register('name', { required: REQUIRED_FIELD_ERROR, validate: { - checkValidName: (value) => isValidString(value) || INVALID_FIELD, + checkValidRoomName: (value) => isValidString(value) || INVALID_FIELD, }, onChange: () => trigger('name'), })} @@ -137,8 +129,8 @@ const RoomForm = ({ valueAsNumber: true, required: REQUIRED_FIELD_ERROR, validate: { - checkIdentifiedCode: (v) => - isValidNumber(v.toString()) || INVALID_FIELD, + checkPrice: (v) => + isValidRegex(new RegExp(REGEX.NUMBER), v.toString()) || INVALID_FIELD, }, onChange: () => trigger('price'), })} @@ -153,7 +145,7 @@ const RoomForm = ({ valueAsNumber: true, required: REQUIRED_FIELD_ERROR, validate: { - checkPhoneNum: (v) => isValidDiscount(v) || INVALID_DISCOUNT, + checkDiscount: (v) => isValidDiscount(v) || INVALID_DISCOUNT, }, onChange: () => trigger('discount'), })} @@ -161,14 +153,18 @@ const RoomForm = ({ - + { - isAdd - ? 'Add' - : 'Save' + !roomEdit + ? 'Add' + : 'Save' } - + Close diff --git a/hotel-management/src/pages/Room/RoomRow.tsx b/hotel-management/src/pages/Room/RoomRow.tsx new file mode 100644 index 0000000..f6662a4 --- /dev/null +++ b/hotel-management/src/pages/Room/RoomRow.tsx @@ -0,0 +1,90 @@ +import { Dispatch, SetStateAction } from 'react'; +import toast from 'react-hot-toast'; + +// Constants +import { ROOM_PATH } from '@constant/path'; +import { STATUS_CODE } from '@constant/responseStatus'; +import { CONFIRM_DELETE, DELETE_SUCCESS } from '@constant/messages'; + +// Helpers +import { sendRequest } from '@helper/sendRequest'; +import { formatCurrency } from '@helper/helper'; + +// Types +import { IRoom } from '@type/rooms'; + +// Components +import RoomForm from './RoomForm'; +import Modal from '@component/Modal'; +import Table from '@component/Table'; +import Menus from '@component/Menus'; +import { RiEditBoxFill } from 'react-icons/ri'; +import { HiTrash } from 'react-icons/hi'; + +interface IRoomRow { + room: IRoom; + reload: boolean; + setReload: Dispatch>; +} + +const RoomRow = ({ room, reload, setReload }: IRoomRow) => { + const handleDelete = async (room: IRoom) => { + if (confirm(CONFIRM_DELETE)) { + const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE'); + + if (response.statusCode === STATUS_CODE.OK) { + toast.success(DELETE_SUCCESS); + + // Reload table + setReload(!reload); + } + } + }; + + const { id, name, finalPrice, status } = room; + + const statusText = status + ? 'Unavailable' + : 'Available'; + + return ( + +
    {id}
    +
    {name}
    +
    {formatCurrency(finalPrice)}
    +
    {statusText}
    + +
    + + + + + + ( + }> + Edit + + )} + /> + + } + onClick={() => handleDelete(room)} + > + Delete + + + + + + + + +
    +
    + ); +}; + +export default RoomRow; diff --git a/hotel-management/src/pages/Room/Table.tsx b/hotel-management/src/pages/Room/Table.tsx deleted file mode 100644 index 32bb775..0000000 --- a/hotel-management/src/pages/Room/Table.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { useSearchParams } from 'react-router-dom'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; - -// Components -import { RiEditBoxFill } from 'react-icons/ri'; -import { HiTrash } from 'react-icons/hi'; -import { StyledOperationTable } from './styled'; -import Menus from '../../components/Menus'; -import Table from '../../components/Table'; -import Direction from '../../commons/styles/Direction'; -import Message from '../../components/Message'; -import Search from '../../components/Search'; -import SortBy from '../../components/SortBy'; -import OrderBy from '../../components/OrderBy'; - -// Types -import { Nullable } from '../../types/common'; -import { IRoom } from '../../types/rooms'; - -// Constants -import { useFetch } from '../../hooks/useFetch'; -import { ROOM_PATH } from '../../constants/path'; -import { STATUS_CODE } from '../../constants/responseStatus'; -import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages'; -import { ORDERBY_OPTIONS, ROOM_PAGE } from '../../constants/variables'; - -// Styled -import Spinner from '../../commons/styles/Spinner'; - -// Helpers -import { sendRequest } from '../../helpers/sendRequest'; -import { formatCurrency } from '../../helpers/helper'; - -interface IRoomRow { - room: IRoom; - openFormDialog: () => void; - setRoom: Dispatch>>; - reload: boolean; - setReload: Dispatch>; -} - -const RoomRow = ({ - room, - openFormDialog, - setRoom, - reload, - setReload, -}: IRoomRow) => { - const handleEdit = (room: IRoom) => { - setRoom(room); - openFormDialog(); - }; - - const handleDelete = async (room: IRoom) => { - if (confirm(CONFIRM_DELETE)) { - const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE'); - - if (response.statusCode === STATUS_CODE.OK) { - toast.success(DELETE_SUCCESS); - - // Reload table - setReload(!reload); - } - } - }; - - const { id, name, finalPrice, status } = room; - - const statusText = status - ? 'Unavailable' - : 'Available'; - - return ( - -
    {id}
    -
    {name}
    -
    {formatCurrency(finalPrice)}
    -
    {statusText}
    - - - - - - } - onClick={() => handleEdit(room)} - > - Edit - - } onClick={() => handleDelete(room)}> - Delete - - - -
    - ); -}; - -interface IRoomTable { - reload: boolean; - setReload: Dispatch>; - openFormDialog: () => void; - setRoom?: Dispatch>>; -} - -const RoomTable = ({ - reload, - setReload, - openFormDialog, - setRoom, -}: IRoomTable) => { - const [nameSearch, setNameSearch] = useState(''); - const [searchParams] = useSearchParams(); - const [rooms, setRooms] = useState([]); - const sortByValue = searchParams.get('sortBy') - ? searchParams.get('sortBy')! - : ''; - const orderByValue = searchParams.get('orderBy') - ? searchParams.get('orderBy')! - : ''; - - const { data, isPending, errorFetchMsg } = useFetch( - 'rooms', - 'name', - nameSearch, - sortByValue, - orderByValue, - reload - ); - - useEffect(() => { - if (data) { - setRooms(data); - } else { - setRooms([]); - } - - if (errorFetchMsg) { - console.error(errorFetchMsg); - } - }, [data, errorFetchMsg]); - - return ( - <> - - - - - - - - - {isPending && } - - {rooms.length ? ( - - - -
    Id
    -
    Name
    -
    Price
    -
    Status
    -
    - - data={rooms} - render={(room: IRoom) => ( - - )} - /> -
    -
    - ) : ( - !isPending && No data to show here! - )} -
    - - ); -}; - -export default RoomTable; diff --git a/hotel-management/src/pages/Room/index.tsx b/hotel-management/src/pages/Room/index.tsx index c416ebe..aa7c3ab 100644 --- a/hotel-management/src/pages/Room/index.tsx +++ b/hotel-management/src/pages/Room/index.tsx @@ -1,57 +1,41 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; // Components -import RoomTable from './Table'; +import RoomTable from './RomTable'; +import RoomForm from './RoomForm'; // Styled import { StyledRoom, Title } from './styled'; -import Direction from '../../commons/styles/Direction'; -import Button from '../../commons/styles/Button'; -import RoomDialog from './Dialog'; +import Direction from '@commonStyle/Direction.ts'; +import Button from '@commonStyle/Button'; // Types -import { Nullable } from '../../types/common'; -import { IRoom } from '../../types/rooms'; +import Modal from '@component/Modal'; const Room = () => { - const dialogRef = useRef(null); const [reload, setReload] = useState(true); - const [room, setRoom] = useState>(null); - const [isAdd, setIsAdd] = useState(false); - - const openFormDialog = (isAddForm: boolean = false) => { - setIsAdd(isAddForm); - dialogRef.current?.showModal(); - }; - - const closeFormDialog = () => { - dialogRef.current?.close(); - }; return ( <> List Room - + + + ( + + )} + /> + + + + - openFormDialog()} - setRoom={setRoom} - /> + - - ); }; diff --git a/hotel-management/src/pages/User/Dialog.tsx b/hotel-management/src/pages/User/Dialog.tsx deleted file mode 100644 index 03dfac7..0000000 --- a/hotel-management/src/pages/User/Dialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - Dispatch, - MutableRefObject, - SetStateAction, - forwardRef, - useEffect, -} from 'react'; - -// Components -import Dialog from '../../components/Dialog'; -import UserForm from './Form'; - -// Types -import { Nullable } from '../../types/common'; -import { IUser } from '../../types/users'; - -// Hooks -import { useForwardRef } from '../../hooks/useForwardRef'; - -interface IUserDialog { - onClose: () => void; - reload: boolean; - setReload: Dispatch>; - ref: MutableRefObject>; - user: Nullable; - isAdd: boolean; -} - -const UserDialog = forwardRef((props, ref) => { - const dialogRef = useForwardRef(ref); - - const { - onClose, - setReload, - reload, - user, - isAdd, - } = props; - - useEffect(() => { - if (dialogRef.current) { - dialogRef.current.addEventListener('click', (e: MouseEvent) => { - const dialogDimensions = dialogRef.current!.getBoundingClientRect(); - if ( - e.clientX < dialogDimensions.left || - e.clientX > dialogDimensions.right || - e.clientY < dialogDimensions.top || - e.clientY > dialogDimensions.bottom - ) { - dialogRef.current!.close(); - } - }); - } - }, [dialogRef]); - - return ( - - - - ); -}); - -export default UserDialog; diff --git a/hotel-management/src/pages/User/Table.tsx b/hotel-management/src/pages/User/Table.tsx deleted file mode 100644 index 825faf2..0000000 --- a/hotel-management/src/pages/User/Table.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useSearchParams } from 'react-router-dom'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; - -// Components -import { RiEditBoxFill } from 'react-icons/ri'; -import { IoExit } from 'react-icons/io5'; -import Menus from '../../components/Menus'; -import Table from '../../components/Table'; -import Direction from '../../commons/styles/Direction'; -import Message from '../../components/Message'; -import Search from '../../components/Search'; -import SortBy from '../../components/SortBy'; -import OrderBy from '../../components/OrderBy'; - -// Types -import { Nullable } from '../../types/common'; -import { IUser } from '../../types/users'; - -// Hooks -import { useFetch } from '../../hooks/useFetch'; - -// Constants -import { STATUS_CODE } from '../../constants/responseStatus'; -import { ORDERBY_OPTIONS, USER_PAGE } from '../../constants/variables'; -import { CONFIRM_MESSAGE, DENIED_ACTION } from '../../constants/messages'; - -// Styled -import { StyledOperationTable } from './styled'; -import Spinner from '../../commons/styles/Spinner'; - -// Services -import { updateRoomStatus } from '../../services/roomServices'; -import { checkOutUser } from '../../services/userServices'; - -interface IUserRow { - user: IUser; - openFormDialog: () => void; - setUser: Dispatch>>; - reload: boolean; - setReload: Dispatch>; -} - -const UserRow = ({ - user, - openFormDialog, - setUser, - reload, - setReload, -}: IUserRow) => { - const handleEdit = (user: IUser) => { - setUser(user); - openFormDialog(); - }; - - const handleCheckOut = async (user: IUser) => { - if(user.roomId !== 0) { - if (confirm(CONFIRM_MESSAGE)) { - const resUpdateStatus = await updateRoomStatus(user.roomId, false); - const resCheckoutUser = await checkOutUser(user); - - if ( - resCheckoutUser?.statusCode === STATUS_CODE.OK && - resUpdateStatus?.statusCode === STATUS_CODE.OK - ) { - toast.success('Check out complete!'); - - // Reload table - setReload(!reload); - } - } - } else { - toast.error(DENIED_ACTION); - } - - }; - - const { id, name, identifiedCode, phone, roomId } = user; - - return ( - -
    {id}
    -
    {name}
    -
    {identifiedCode}
    -
    {phone}
    -
    { - roomId - ? roomId - : 'None' - }
    - - - - - - } - onClick={() => handleEdit(user)} - > - Edit - - } onClick={() => handleCheckOut(user)}> - Check out - - - -
    - ); -}; - -interface IUserTable { - reload: boolean; - setReload: Dispatch>; - openFormDialog: () => void; - setUser?: Dispatch>>; -} - -const UserTable = ({ - reload, - setReload, - openFormDialog, - setUser, -}: IUserTable) => { - const [users, setUsers] = useState([]); - const [phoneSearch, setPhoneSearch] = useState(''); - const [searchParams] = useSearchParams(); - const sortByValue = searchParams.get('sortBy') - ? searchParams.get('sortBy')! - : ''; - const orderByValue = searchParams.get('orderBy') - ? searchParams.get('orderBy')! - : ''; - - const { data, isPending, errorFetchMsg } = useFetch( - 'users', - 'phone', - phoneSearch, - sortByValue, - orderByValue, - reload - ); - - useEffect(() => { - if (data) { - setUsers(data); - } else { - setUsers([]); - } - - if (errorFetchMsg) { - console.error(errorFetchMsg); - } - }, [data, errorFetchMsg, setUsers]); - - return ( - <> - - - - - - - - - {isPending && } - - {users.length ? ( - - - -
    Id
    -
    Name
    -
    Identified Code
    -
    Phone
    -
    Room Id
    -
    - - data={users} - render={(user: IUser) => ( - - )} - /> -
    -
    - ) : ( - !isPending && No data to show here! - )} -
    - - ); -}; - -export default UserTable; diff --git a/hotel-management/src/pages/User/Form.tsx b/hotel-management/src/pages/User/UserForm.tsx similarity index 63% rename from hotel-management/src/pages/User/Form.tsx rename to hotel-management/src/pages/User/UserForm.tsx index d0ee6c0..80608be 100644 --- a/hotel-management/src/pages/User/Form.tsx +++ b/hotel-management/src/pages/User/UserForm.tsx @@ -11,63 +11,51 @@ import toast from 'react-hot-toast'; import { FormProvider, useForm } from 'react-hook-form'; // Styled -import Input from '../../commons/styles/Input'; +import Input from '@commonStyle/Input.ts'; // Components -import Form from '../../components/Form'; -import FormRow from '../../components/LabelControl/index.tsx'; -import Select, { ISelectOptions } from '../../components/Select'; +import Form from '@component/Form/index.tsx'; +import FormRow from '@component/LabelControl/index.tsx'; +import Select, { ISelectOptions } from '@component/Select/index.tsx'; // Helpers -import { - isEmptyObj, - isValidName, - isValidNumber, - isValidPhoneNumber, -} from '../../helpers/validators.ts'; +import { isEmptyObj, isValidRegex } from '@helper/validators.ts'; // Constants -import { ADD_SUCCESS, EDIT_SUCCESS } from '../../constants/messages.ts'; +import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts'; import { INVALID_FIELD, INVALID_PHONE, REQUIRED_FIELD_ERROR, -} from '../../constants/formValidateMessage.ts'; -import { INIT_VALUE_USER_FORM } from '../../constants/variables.ts'; +} from '@constant/formValidateMessage.ts'; +import { INIT_VALUE_USER_FORM, REGEX } from '@constant/commons.ts'; // Styled import { FormBtn } from './styled.ts'; // Services -import { getAllRoom, updateRoomStatus } from '../../services/roomServices.ts'; +import { getAllRoom, updateRoomStatus } from '@service/roomServices.ts'; +import { createUser, updateUser } from '@service/userServices.ts'; // Types -import { Nullable } from '../../types/common.ts'; -import { IUser } from '../../types/users.ts'; -import { IRoom } from '../../types/rooms.ts'; -import { createUser, updateUser } from '../../services/userServices.ts'; +import { Nullable } from '@type/common.ts'; +import { IUser } from '@type/users.ts'; +import { IRoom } from '@type/rooms.ts'; interface IUserFormProp { - onClose: () => void; + onCloseModal?: () => void; reload: boolean; setReload: Dispatch>; - user: Nullable; - isAdd: boolean; + user?: Nullable; } -const UserForm = ({ - onClose, - reload, - setReload, - user, - isAdd, -}: IUserFormProp) => { +const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => { const formMethods = useForm(); const { register, handleSubmit, reset, - formState: { errors, isDirty, isValid }, + formState: { errors, isDirty, isValid, isSubmitting }, trigger, } = formMethods; const [rooms, setRooms] = useState([]); @@ -77,9 +65,7 @@ const UserForm = ({ useEffect(() => { const load = async () => { const options: ISelectOptions[] = []; - const tempUser = isAdd - ? {} - : { ...user }; + const tempUser = !user ? { roomId: 0 } : { ...user }; // Load and set default options room if (rooms.length > 0) { @@ -111,45 +97,39 @@ const UserForm = ({ }; load(); - }, [reset, user, isAdd, rooms]); + }, [reset, user, rooms]); // Submit form const onSubmit = useCallback( async (newUser: IUser) => { - try { - if (isAdd) { - // Add request - const response = await createUser(newUser); + if (!user) { + // Add request + const response = await createUser(newUser); - if (response) { - toast.success(ADD_SUCCESS); - } - - // Update room status - updateRoomStatus(newUser.roomId, true); - } else { - // Edit request - const response = await updateUser(newUser); - - if (response) { - toast.success(EDIT_SUCCESS); - } - - // Update room status - updateRoomStatus(user!.roomId, true, newUser.roomId); + if (response) { + toast.success(ADD_SUCCESS); } - // Reload table data - setReload(!reload); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); + + // Update room status + updateRoomStatus(newUser.roomId, true); + } else { + // Edit request + const response = await updateUser(newUser); + + if (response) { + toast.success(EDIT_SUCCESS); } + + // Update room status + updateRoomStatus(user!.roomId, true, newUser.roomId); } + // Reload table data + setReload(!reload); reset(); - onClose(); + onCloseModal!(); }, - [isAdd, onClose, reload, reset, setReload, user] + [onCloseModal, reload, reset, setReload, user] ); // Load all rooms @@ -176,7 +156,8 @@ const UserForm = ({ {...register('name', { required: REQUIRED_FIELD_ERROR, validate: { - checkValidName: (value) => isValidName(value) || INVALID_FIELD, + checkValidName: (value) => + isValidRegex(new RegExp(REGEX.NAME), value) || INVALID_FIELD, }, onChange: () => trigger('name'), })} @@ -194,7 +175,8 @@ const UserForm = ({ required: REQUIRED_FIELD_ERROR, validate: { checkIdentifiedCode: (v) => - isValidNumber(v.toString()) || INVALID_FIELD, + isValidRegex(new RegExp(REGEX.NUMBER), v.toString()) || + INVALID_FIELD, }, onChange: () => trigger('identifiedCode'), })} @@ -208,7 +190,8 @@ const UserForm = ({ {...register('phone', { required: REQUIRED_FIELD_ERROR, validate: { - checkPhoneNum: (v) => isValidPhoneNumber(v) || INVALID_PHONE, + checkPhoneNum: (v) => + isValidRegex(new RegExp(REGEX.PHONE), v) || INVALID_PHONE, }, onChange: () => trigger('phone'), })} @@ -233,14 +216,14 @@ const UserForm = ({ - - { - isAdd - ? 'Add' - : 'Save' - } + + {!user ? 'Add' : 'Save'} - + Close diff --git a/hotel-management/src/pages/User/UserRow.tsx b/hotel-management/src/pages/User/UserRow.tsx new file mode 100644 index 0000000..c21c3c5 --- /dev/null +++ b/hotel-management/src/pages/User/UserRow.tsx @@ -0,0 +1,98 @@ +import toast from 'react-hot-toast'; +import { Dispatch, SetStateAction } from 'react'; + +// Components +import { RiEditBoxFill } from 'react-icons/ri'; +import { IoExit } from 'react-icons/io5'; +import Modal from '@component/Modal'; +import Table from '@component/Table'; +import Menus from '@component/Menus'; +import UserForm from './UserForm'; + +// Constants +import { STATUS_CODE } from '@constant/responseStatus'; +import { CONFIRM_MESSAGE, DENIED_ACTION } from '@constant/messages'; + +// Services +import { updateRoomStatus } from '@service/roomServices'; +import { checkOutUser } from '@service/userServices'; + +// Types +import { IUser } from '@type/users'; + +interface IUserRow { + user: IUser; + reload: boolean; + setReload: Dispatch>; +} + +const UserRow = ({ user, reload, setReload }: IUserRow) => { + const handleCheckOut = async (user: IUser) => { + if (user.roomId !== 0) { + if (confirm(CONFIRM_MESSAGE)) { + const resUpdateStatus = await updateRoomStatus(user.roomId, false); + const resCheckoutUser = await checkOutUser(user); + + if ( + resCheckoutUser?.statusCode === STATUS_CODE.OK && + resUpdateStatus?.statusCode === STATUS_CODE.OK + ) { + toast.success('Check out complete!'); + + // Reload table + setReload(!reload); + } + } + } else { + toast.error(DENIED_ACTION); + } + }; + + const { id, name, identifiedCode, phone, roomId } = user; + + return ( + +
    {id}
    +
    {name}
    +
    {identifiedCode}
    +
    {phone}
    +
    + { + roomId + ? roomId + : 'None' + } +
    +
    + + + + + + ( + }> + Edit + + )} + /> + } + onClick={() => handleCheckOut(user)} + > + Check out + + + + + + + + +
    +
    + ); +}; + +export default UserRow; diff --git a/hotel-management/src/pages/User/UserTable.tsx b/hotel-management/src/pages/User/UserTable.tsx new file mode 100644 index 0000000..645969e --- /dev/null +++ b/hotel-management/src/pages/User/UserTable.tsx @@ -0,0 +1,105 @@ +import { useSearchParams } from 'react-router-dom'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +// Components +import Menus from '@component/Menus'; +import Table from '@component/Table'; +import Message from '@component/Message'; +import Search from '@component/Search'; +import SortBy from '@component/SortBy'; +import OrderBy from '@component/OrderBy'; +import UserRow from './UserRow'; + +// Types +import { IUser } from '@type/users'; + +// Hooks +import { useFetch } from '@hook/useFetch'; + +// Constants +import { ORDERBY_OPTIONS, USER_PAGE } from '@constant/commons'; + +// Styled +import Direction from '@commonStyle/Direction'; +import { StyledOperationTable } from './styled'; +import Spinner from '@commonStyle/Spinner'; + +interface IUserTable { + reload: boolean; + setReload: Dispatch>; +} + +const UserTable = ({ reload, setReload }: IUserTable) => { + const columnName = ['Id', 'Name', 'Identified Code', 'Phone', 'Room Id']; + const [users, setUsers] = useState([]); + const [phoneSearch, setPhoneSearch] = useState(''); + const [searchParams] = useSearchParams(); + const sortByValue = searchParams.get('sortBy') + ? searchParams.get('sortBy')! + : ''; + const orderByValue = searchParams.get('orderBy') + ? searchParams.get('orderBy')! + : ''; + + const { data, isPending, errorFetchMsg } = useFetch( + 'users', + 'phone', + phoneSearch, + sortByValue, + orderByValue, + reload + ); + + useEffect(() => { + if (data) { + setUsers(data); + } else { + setUsers([]); + } + + if (errorFetchMsg) { + console.error(errorFetchMsg); + } + }, [data, errorFetchMsg, setUsers]); + + return ( + <> + + + + + + + + + {isPending && } + + {users.length ? ( + + + + + data={users} + render={(user: IUser) => ( + + )} + /> +
    +
    + ) : ( + !isPending && No data to show here! + )} +
    + + ); +}; + +export default UserTable; diff --git a/hotel-management/src/pages/User/index.tsx b/hotel-management/src/pages/User/index.tsx index 50af0a4..d1c9b55 100644 --- a/hotel-management/src/pages/User/index.tsx +++ b/hotel-management/src/pages/User/index.tsx @@ -1,57 +1,45 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; // Components -import UserTable from './Table'; +import UserTable from './UserTable'; // Styled -import Button from '../../commons/styles/Button'; -import Direction from '../../commons/styles/Direction'; +import Button from '@commonStyle/Button'; +import Direction from '@commonStyle/Direction.ts'; import { StyledUser, Title } from './styled'; -import UserDialog from './Dialog'; // Types -import { Nullable } from '../../types/common'; -import { IUser } from '../../types/users'; +import Modal from '@component/Modal'; +import UserForm from './UserForm'; + +// Constants +import { FORM } from '@constant/commons'; const User = () => { - const dialogRef = useRef(null); + const TITLE = 'Add user'; const [reload, setReload] = useState(true); - const [user, setUser] = useState>(null); - const [isAdd, setIsAdd] = useState(false); - - const openFormDialog = (isAddForm: boolean = false) => { - setIsAdd(isAddForm); - dialogRef.current?.showModal(); - }; - - const closeFormDialog = () => { - dialogRef.current?.close(); - }; return ( <> List User - + + + ( + + )} + /> + + + + - openFormDialog()} - setUser={setUser} - /> + - - ); }; diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index d258e37..4976142 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -1,17 +1,17 @@ import toast from 'react-hot-toast'; // Types -import { Nullable } from '../types/common'; -import { IResponse } from '../types/responses'; -import { IRoom } from '../types/rooms'; +import { Nullable } from '@type/common'; +import { IResponse } from '@type/responses'; +import { IRoom } from '@type/rooms'; // Helpers -import { sendRequest } from '../helpers/sendRequest'; -import { errorMsg } from '../helpers/helper'; +import { sendRequest } from '@helper/sendRequest'; +import { errorMsg } from '@helper/helper'; // Constants -import { STATUS_CODE, RESPONSE_MESSAGE } from '../constants/responseStatus'; -import { ROOM_PATH } from '../constants/path'; +import { STATUS_CODE, RESPONSE_MESSAGE } from '@constant/responseStatus'; +import { ROOM_PATH } from '@constant/path'; /** * Get all rooms from server diff --git a/hotel-management/src/services/userServices.ts b/hotel-management/src/services/userServices.ts index ae43f86..94cc7b3 100644 --- a/hotel-management/src/services/userServices.ts +++ b/hotel-management/src/services/userServices.ts @@ -1,17 +1,17 @@ import toast from 'react-hot-toast'; // Constants -import { USER_PATH } from '../constants/path'; -import { STATUS_CODE } from '../constants/responseStatus'; +import { USER_PATH } from '@constant/path'; +import { STATUS_CODE } from '@constant/responseStatus'; // Types -import { Nullable } from '../types/common'; -import { IResponse } from '../types/responses'; -import { IUser } from '../types/users'; +import { Nullable } from '@type/common'; +import { IResponse } from '@type/responses'; +import { IUser } from '@type/users'; // Helpers -import { sendRequest } from '../helpers/sendRequest'; -import { errorMsg } from '../helpers/helper'; +import { sendRequest } from '@helper/sendRequest'; +import { errorMsg } from '@helper/helper'; /** * Create user to the server diff --git a/hotel-management/src/styles/abstracts/variables.css b/hotel-management/src/styles/abstracts/variables.css index c497130..648a503 100644 --- a/hotel-management/src/styles/abstracts/variables.css +++ b/hotel-management/src/styles/abstracts/variables.css @@ -17,6 +17,10 @@ --radius-sm: 5px; --radius-md: 10px; + --overlay-color: #0000004d; + + --form-color: #fff; + --shadow-sm: 0px 10px 20px rgba(0, 0, 0, 0.2); --fs-md: 30px; diff --git a/hotel-management/tsconfig.json b/hotel-management/tsconfig.json index a7fc6fb..867cbe7 100644 --- a/hotel-management/tsconfig.json +++ b/hotel-management/tsconfig.json @@ -18,7 +18,20 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + /* Alias */ + "paths": { + "@service/*": ["./src/services/*"], + "@constant/*": ["./src/constants/*"], + "@hook/*": ["./src/hooks/*"], + "@helper/*": ["./src/helpers/*"], + "@context/*": ["./src/contexts/*"], + "@component/*": ["./src/components/*"], + "@commonStyle/*": ["./src/commons/styles/*"], + "@type/*": ["./src/types/*"], + "@page/*": ["./src/pages/*"] + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/hotel-management/vite.config.ts b/hotel-management/vite.config.ts index 5a33944..3e8ce05 100644 --- a/hotel-management/vite.config.ts +++ b/hotel-management/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tsconfigPaths from 'vite-tsconfig-paths' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tsconfigPaths()], })