diff --git a/hotel-management/src/helpers/utils.ts b/hotel-management/src/helpers/utils.ts index 1dcbbd7..b774f80 100644 --- a/hotel-management/src/helpers/utils.ts +++ b/hotel-management/src/helpers/utils.ts @@ -87,7 +87,9 @@ const getValueFromObj = (obj: T | null = null): TKeyString => { for (const key of Object.keys(obj)) { const tempValue = obj[key as keyof typeof obj]; const value: string | boolean | number = - typeof tempValue === 'boolean' || typeof tempValue === 'string' + typeof tempValue === 'boolean' || + typeof tempValue === 'string' || + typeof tempValue === 'number' ? tempValue : ''; diff --git a/hotel-management/src/hooks/useFetch.ts b/hotel-management/src/hooks/useFetch.ts index eda4eea..59647c5 100644 --- a/hotel-management/src/hooks/useFetch.ts +++ b/hotel-management/src/hooks/useFetch.ts @@ -2,12 +2,25 @@ import { useEffect, useState } from 'react'; // Constants import { BASE_URL } from '../constants/path'; -import { searchQuery } from '../helpers/utils'; import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config'; -export const useFetch = ( +// Utils +import { searchQuery } from '../helpers/utils'; + +/** + * + * @param path The path of url + * @param columnSearch Column need to search + * @param keyWord The key word to search + * @param tempSortBy Sort by + * @param tempOrderBy Order by + * @param reload Fetch again + * @returns data: A data after fetch, isPending: A boolean indicating whether or not the progress of fetch data is done, errorMsg: A error message from the server. + */ +const useFetch = ( path: string, - phoneNum: string, + columnSearch: string, + keyWord: string, tempSortBy: string, tempOrderBy: string, reload?: boolean, @@ -35,7 +48,7 @@ export const useFetch = ( : DEFAULT_ORDER_BY; // Query search - const query = searchQuery(phoneNum, sortBy, orderBy); + const query = searchQuery(columnSearch, keyWord, sortBy, orderBy); try { const response = await fetch(BASE_URL + path + '?' + query); @@ -56,6 +69,8 @@ export const useFetch = ( }; fetchData(); - }, [path, reload, phoneNum, tempOrderBy, tempSortBy]); + }, [path, reload, columnSearch, keyWord, tempOrderBy, tempSortBy]); return { data, isPending, errorMsg }; }; + +export { useFetch }; diff --git a/hotel-management/src/hooks/useForm.ts b/hotel-management/src/hooks/useForm.ts index bc28fc7..362aaee 100644 --- a/hotel-management/src/hooks/useForm.ts +++ b/hotel-management/src/hooks/useForm.ts @@ -1,17 +1,14 @@ import { useState, useEffect, useCallback, ChangeEvent } from 'react'; // Utils -import { - ERROR, - VALUE, - getPropValues, - isObject, - isRequired, -} from '../helpers/utils'; +import { getPropValues, isObject, isRequired } from '../helpers/utils'; // Types import { TKeyValue, TValidator } from '../globals/types'; +// Constants +import { ERROR, VALUE } from '../constants/variables'; + /** * Custom hooks to validate your Form... * @@ -36,11 +33,11 @@ const useForm = ( useEffect(() => { setInitialErrorState(initialValue); setDisable(true); + setDirty(getPropValues(stateSchema)); // If initial value true, setValues again from stateSchema // and enabled button if (initialValue) { - setValues({}); setValues(getPropValues(stateSchema, VALUE)); setDisable(false); } @@ -116,10 +113,19 @@ const useForm = ( (event: ChangeEvent) => { setIsDirty(true); + let error = ''; const name = (event.target! as HTMLInputElement).name; - const value = (event.target! as HTMLInputElement).value; + let value: string | boolean = ''; - const error = validateFormFields(name, value); + if ((event.target! as HTMLInputElement).type === 'checkbox') { + value = (event.target! as HTMLInputElement).checked; + } else { + value = (event.target! as HTMLInputElement).value; + } + + if (typeof value === 'string') { + error = validateFormFields(name, value)!; + } setValues((prevState) => ({ ...prevState, [name]: value })); setErrors((prevState) => ({ ...prevState, [name]: error })); diff --git a/hotel-management/src/hooks/useOutsideClick.ts b/hotel-management/src/hooks/useOutsideClick.ts index c7278d5..bc6e040 100644 --- a/hotel-management/src/hooks/useOutsideClick.ts +++ b/hotel-management/src/hooks/useOutsideClick.ts @@ -1,5 +1,11 @@ import { useEffect, useRef } from 'react'; +/** + * Custom hook to call handler when click outside element + * @param handler Handler function + * @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 = ( handler: () => void, listeningCapturing = true, diff --git a/hotel-management/src/pages/Room.tsx b/hotel-management/src/pages/Room.tsx deleted file mode 100644 index a7185f5..0000000 --- a/hotel-management/src/pages/Room.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; - -const StyledRoom = styled.main` - padding: 20px; -`; - -const Room = () => { - return ( - -

Room page

-
- ); -}; - -export default Room; diff --git a/hotel-management/src/pages/Room/Dialog.tsx b/hotel-management/src/pages/Room/Dialog.tsx new file mode 100644 index 0000000..5fd8f99 --- /dev/null +++ b/hotel-management/src/pages/Room/Dialog.tsx @@ -0,0 +1,55 @@ +import { forwardRef, useEffect } from 'react'; + +// Components +import Dialog from '../../components/Dialog'; +import RoomForm from './Form'; + +// Interfaces +import { IDialogProps } from '../../globals/interfaces'; + +// Types +import { TRoom } from '../../globals/types'; + +const RoomDialog = forwardRef((props, ref) => { + const dialogRef = ref as React.MutableRefObject< + HTMLDialogElement | undefined + >; + // prettier-ignore + const { + onClose, + setReload, + reload, + data, + 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 ( + + + + ); +}) as React.FC>; + +export default RoomDialog; diff --git a/hotel-management/src/pages/Room/Form.tsx b/hotel-management/src/pages/Room/Form.tsx new file mode 100644 index 0000000..40ca36b --- /dev/null +++ b/hotel-management/src/pages/Room/Form.tsx @@ -0,0 +1,355 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import toast from 'react-hot-toast'; + +// Styled +import Input from '../../commons/styles/Input.ts'; +import TextArea from '../../commons/styles/TextArea.ts'; + +// Components +import Form from '../../components/Form/index.tsx'; +import FormRow from '../../components/FormRow/index.tsx'; +import Button from '../../commons/styles/Button.ts'; + +// Types +import { + TKeyValue, + TRoom, + TStateSchema, + TValidator, +} from '../../globals/types.ts'; + +// Hooks +import useForm from '../../hooks/useForm.ts'; + +// Utils +import { + isValidNumber, + isValidString, + skipCheck, +} from '../../helpers/validators.ts'; +import { addValidator, getValueFromObj } from '../../helpers/utils.ts'; +import { sendRequest } from '../../helpers/sendRequest.ts'; + +// Constants +import { STATUS_CODE } from '../../constants/statusCode.ts'; +import { + ADD_SUCCESS, + EDIT_SUCCESS, + errorMsg, +} from '../../constants/messages.ts'; +import { ROOM_PATH } from '../../constants/path.ts'; + +const FormBtn = styled(Button)` + width: 100%; + + &:disabled, + &[disabled] { + background-color: var(--disabled-btn-color); + } +`; + +interface IRoomFormProp { + onClose: () => void; + reload: boolean; + setReload: React.Dispatch>; + room?: TRoom | null; + isAdd: boolean; +} + +const RoomForm = ({ + onClose, + reload, + setReload, + room, + isAdd, +}: IRoomFormProp) => { + const [reset, setReset] = useState(true); + + if (isAdd) { + // If this is add form, reset value + room = null; + } + + // prettier-ignore + const initialValue: string = isAdd + ? '' + : room! + && room.id; + + const { + idValue, + nameValue, + amountValue, + priceValue, + discountValue, + statusValue, + descriptionValue, + } = getValueFromObj(room); + + // Define your state schema + // prettier-ignore + const stateSchema: TStateSchema = { + id: { + value: idValue || '' + }, + name: { + value: nameValue || '', + error: '' + }, + amount: { + value: amountValue || '', + error: '' + }, + price: { + value: priceValue || '', + error: '' + }, + discount: { + value: discountValue || '', + error: '' + }, + status: { + value: statusValue || '', + error: '' + }, + description: { + value: descriptionValue || '', + error: '' + }, + }; + + // prettier-ignore + const stateValidatorSchema: TValidator = { + name: addValidator({ + validatorFunc: isValidString, + prop: 'name' + }), + amount: addValidator({ + validatorFunc: isValidNumber, + prop: 'amount', + }), + price: addValidator({ + validatorFunc: isValidNumber, + prop: 'price', + }), + discount: addValidator({ + validatorFunc: isValidNumber, + prop: 'discount' + }), + status: addValidator({ + validatorFunc: skipCheck, + prop: 'status', + required: false, + }), + description: addValidator({ + validatorFunc: isValidString, + prop: 'description' + }), + }; + + // Submit form + const onSubmitForm = async (state: TKeyValue) => { + try { + if (isAdd) { + // Add request + const response = await sendRequest( + ROOM_PATH, + JSON.stringify(state), + 'POST', + ); + + if (response.statusCode === STATUS_CODE.CREATE) { + toast.success(ADD_SUCCESS); + + onResetForm(); + } else { + throw new Error(errorMsg(response.statusCode, response.msg)); + } + } else { + // Edit request + const response = await sendRequest( + ROOM_PATH + `/${room!.id}`, + JSON.stringify(state), + 'PUT', + ); + + if (response.statusCode == STATUS_CODE.OK) { + toast.success(EDIT_SUCCESS); + } else { + throw new Error(errorMsg(response.statusCode, response.msg)); + } + } + // Reload table data + setReload(!reload); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + + onClose(); + }; + + // Close and reset form + const closeAndReset = () => { + onClose(); + onResetForm(); + }; + + console.log(initialValue); + + // prettier-ignore + const { + values, + errors, + dirty, + handleOnChange, + handleOnSubmit, + disable } = + useForm( + stateSchema, + stateValidatorSchema, + onSubmitForm, + initialValue + ); + + // prettier-ignore + const { + id, + name, + amount, + price, + discount, + status, + description + } = values; + + // Clear form value when this is a add form + useEffect(() => { + if (isAdd) { + Object.keys(values).forEach((key) => (values[key] = '')); + } + }, [isAdd]); // eslint-disable-line + + // Reset form + const onResetForm = () => { + setReset(!reset); + Object.keys(values).forEach((key) => (values[key] = '')); + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +