diff --git a/hotel-management/.gitignore b/hotel-management/.gitignore index a547bf3..8cdfe8e 100644 --- a/hotel-management/.gitignore +++ b/hotel-management/.gitignore @@ -12,6 +12,8 @@ dist dist-ssr *.local +src/db.json + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/hotel-management/db.json b/hotel-management/db.json deleted file mode 100644 index 07a3a79..0000000 --- a/hotel-management/db.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "users": [ - { - "id": "123", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "231", - "name": "Phan Huu Loi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Ho Chi Minh" - }, - { - "id": "312", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "523", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "534", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "756", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "23", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "756", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - }, - { - "id": "756", - "name": "Nezumi", - "identifiedCode": "123", - "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" - } - ] -} diff --git a/hotel-management/src/constants/messages.ts b/hotel-management/src/constants/messages.ts index 8d8440c..7ddb73a 100644 --- a/hotel-management/src/constants/messages.ts +++ b/hotel-management/src/constants/messages.ts @@ -2,10 +2,20 @@ const invalidFormatMsg = (field: string) => { return `Invalid ${field} format`; }; -const ADD_SUCCESS = 'Add success'; - const errorMsg = (errorCode: number, msg: string) => { return `Error code: ${errorCode}. Message: ${msg}`; }; -export { invalidFormatMsg, ADD_SUCCESS, errorMsg }; +const ADD_SUCCESS = 'Add success'; +const EDIT_SUCCESS = 'Edit success'; +const CONFIRM_DELETE = 'Are you sure to delete it?'; +const DELETE_SUCCESS = 'Delete success'; + +export { + invalidFormatMsg, + ADD_SUCCESS, + errorMsg, + EDIT_SUCCESS, + CONFIRM_DELETE, + DELETE_SUCCESS, +}; diff --git a/hotel-management/src/constants/path.ts b/hotel-management/src/constants/path.ts index e4900ab..d4bd6b1 100644 --- a/hotel-management/src/constants/path.ts +++ b/hotel-management/src/constants/path.ts @@ -3,3 +3,4 @@ export const USER: string = '/user'; export const ROOM: string = '/room'; export const OTHER_PATH: string = '*'; export const BASE_URL: string = 'https://hotel-management-api.loiphan.com/'; +export const USER_PATH = 'users'; diff --git a/hotel-management/src/constants/statusCode.ts b/hotel-management/src/constants/statusCode.ts index fd1e9d3..587e7ea 100644 --- a/hotel-management/src/constants/statusCode.ts +++ b/hotel-management/src/constants/statusCode.ts @@ -1,4 +1,5 @@ const STATUS_CODE = { + OK: 200, CREATE: 201, NOT_FOUND: 404, }; diff --git a/hotel-management/src/globals/interfaces.ts b/hotel-management/src/globals/interfaces.ts index 21baafc..ef16998 100644 --- a/hotel-management/src/globals/interfaces.ts +++ b/hotel-management/src/globals/interfaces.ts @@ -1,3 +1,6 @@ +// Types +import { TUser } from './types'; + interface IMenusContext { openId?: string; close?: () => void; @@ -27,6 +30,8 @@ interface IDialogProps { reload?: boolean; setReload?: React.Dispatch>; ref?: React.MutableRefObject; + user?: TUser | null; + isAdd?: boolean; } export type { IMenusContext, IButton, ITable, ITableBody, IDialogProps }; diff --git a/hotel-management/src/helpers/utils.ts b/hotel-management/src/helpers/utils.ts index 234b66a..eb5ff4f 100644 --- a/hotel-management/src/helpers/utils.ts +++ b/hotel-management/src/helpers/utils.ts @@ -1,5 +1,8 @@ +// Constants import { invalidFormatMsg } from '../constants/messages'; -import { TKeyValue, TPropValues, TStateSchema } from '../globals/types'; + +// Types +import { TKeyValue, TPropValues, TStateSchema, TUser } from '../globals/types'; const VALUE = 'value'; const ERROR = 'error'; @@ -44,10 +47,19 @@ const addValidator = ({ validatorFunc, prop, required = true }: TValidator) => { }; }; +const getValueUser = (user: TUser | null = null, prop: string): string => { + if (user) { + return user[prop as keyof TUser]; + } + + return ''; +}; + export { isObject, isRequired, getPropValues, + getValueUser, addValidator, VALUE, ERROR, diff --git a/hotel-management/src/hooks/useForm.ts b/hotel-management/src/hooks/useForm.ts index fa5e71c..bc28fc7 100644 --- a/hotel-management/src/hooks/useForm.ts +++ b/hotel-management/src/hooks/useForm.ts @@ -24,6 +24,7 @@ const useForm = ( stateSchema = {}, stateValidatorSchema = {} as TValidator, submitFormCallback: (values: TKeyValue) => void, + initialValue: string = '', ) => { const [values, setValues] = useState(getPropValues(stateSchema, VALUE)); const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR)); @@ -33,8 +34,17 @@ const useForm = ( // Get a local copy of stateSchema useEffect(() => { - setInitialErrorState(); - }, []); // eslint-disable-line + setInitialErrorState(initialValue); + setDisable(true); + + // If initial value true, setValues again from stateSchema + // and enabled button + if (initialValue) { + setValues({}); + setValues(getPropValues(stateSchema, VALUE)); + setDisable(false); + } + }, [initialValue]); // eslint-disable-line // Validate fields in forms const validateFormFields = useCallback( @@ -47,15 +57,19 @@ const useForm = ( const field = validator[name]; let error = ''; - error = isRequired(value, field!.required); - if (isObject(field['validator']) && error === '') { - const fieldValidator = field['validator']; + // Skip check id field + if (name !== 'id') { + error = isRequired(value, field!.required); - // Test the function callback if the value is meet the criteria - const testFunc = fieldValidator!['func']; - if (!testFunc!(value)) { - error = fieldValidator!['error']!; + if (isObject(field['validator']) && error === '') { + const fieldValidator = field['validator']; + + // Test the function callback if the value is meet the criteria + const testFunc = fieldValidator!['func']; + if (!testFunc!(value)) { + error = fieldValidator!['error']!; + } } } @@ -66,14 +80,19 @@ const useForm = ( // Set Initial Error State // When hooks was first rendered... - const setInitialErrorState = useCallback(() => { - Object.keys(errors).map((name) => - setErrors((prevState) => ({ - ...prevState, - [name]: validateFormFields(name, values[name] as string), - })), - ); - }, [errors, values, validateFormFields]); + const setInitialErrorState = useCallback( + (initialValue: string) => { + Object.keys(errors).map((name) => + setErrors((prevState) => ({ + ...prevState, + [name]: !initialValue // Skip error when initialValue have values + ? validateFormFields(name, values[name] as string) + : '', + })), + ); + }, + [errors, values, validateFormFields], + ); // Used to disable submit button if there's a value in errors // or the required field in state has no value. @@ -116,6 +135,7 @@ const useForm = ( // Making sure that there's no error in the state // before calling the submit callback function // and disabled button + if (!validateErrorState()) { submitFormCallback(values); setDisable(true); diff --git a/hotel-management/src/pages/User/Dialog.tsx b/hotel-management/src/pages/User/Dialog.tsx index b626fe7..b869266 100644 --- a/hotel-management/src/pages/User/Dialog.tsx +++ b/hotel-management/src/pages/User/Dialog.tsx @@ -11,7 +11,14 @@ const UserDialog = forwardRef((props, ref) => { const dialogRef = ref as React.MutableRefObject< HTMLDialogElement | undefined >; - const { onClose, setReload, reload } = props; + // prettier-ignore + const { + onClose, + setReload, + reload, + user, + isAdd + } = props; useEffect(() => { if (dialogRef.current) { @@ -31,7 +38,13 @@ const UserDialog = forwardRef((props, ref) => { return ( - + ); }) as React.FC; diff --git a/hotel-management/src/pages/User/Form.tsx b/hotel-management/src/pages/User/Form.tsx index 2d6086e..86fa0e9 100644 --- a/hotel-management/src/pages/User/Form.tsx +++ b/hotel-management/src/pages/User/Form.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import styled from 'styled-components'; import toast from 'react-hot-toast'; @@ -14,8 +14,8 @@ import Button from '../../commons/styles/Button.ts'; // Types import { TKeyValue, - TResponse, TStateSchema, + TUser, TValidator, } from '../../globals/types'; @@ -29,12 +29,17 @@ import { isValidPhoneNumber, isValidString, } from '../../helpers/validators'; -import { addValidator } from '../../helpers/utils.ts'; +import { addValidator, getValueUser } from '../../helpers/utils.ts'; import { sendRequest } from '../../helpers/sendRequest.ts'; // Constants import { STATUS_CODE } from '../../constants/statusCode.ts'; -import { ADD_SUCCESS, errorMsg } from '../../constants/messages.ts'; +import { + ADD_SUCCESS, + EDIT_SUCCESS, + errorMsg, +} from '../../constants/messages.ts'; +import { USER_PATH } from '../../constants/path.ts'; const FormBtn = styled(Button)` width: 100%; @@ -49,18 +54,32 @@ interface IUserFormProp { onClose: () => void; reload: boolean; setReload: React.Dispatch>; + user?: TUser | null; + isAdd: boolean; } -const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { +const UserForm = ({ + onClose, + reload, + setReload, + user, + isAdd, +}: IUserFormProp) => { const [reset, setReset] = useState(true); + // prettier-ignore + const initialValue: string = isAdd + ? '' + : user! + && user.id; // Define your state schema const stateSchema: TStateSchema = { - name: { value: '', error: '' }, - identifiedCode: { value: '', error: '' }, - phone: { value: '', error: '' }, - roomId: { value: '', error: '' }, - address: { value: '', error: '' }, + id: { value: getValueUser(user, 'id') }, + name: { value: getValueUser(user, 'name'), error: '' }, + identifiedCode: { value: getValueUser(user, 'identifiedCode'), error: '' }, + phone: { value: getValueUser(user, 'phone'), error: '' }, + roomId: { value: getValueUser(user, 'roomId'), error: '' }, + address: { value: getValueUser(user, 'address'), error: '' }, }; // prettier-ignore @@ -90,31 +109,51 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { // Submit form const onSubmitForm = async (state: TKeyValue) => { try { - const response = await sendRequest( - 'users', - JSON.stringify(state), - 'POST', - ); + if (isAdd) { + // Add request + const response = await sendRequest( + USER_PATH, + JSON.stringify(state), + 'POST', + ); - if (response.statusCode === STATUS_CODE.CREATE) { - toast.success(ADD_SUCCESS); - - // Reload table data - setReload(!reload); + if (response.statusCode === STATUS_CODE.CREATE) { + toast.success(ADD_SUCCESS); + } else { + throw new Error(errorMsg(response.statusCode, response.msg)); + } } else { - toast.error(errorMsg(response.statusCode, response.msg)); - } + // Edit request + const response = await sendRequest( + USER_PATH + `/${user!.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); onResetForm(); - } catch (error) { - toast.error( - errorMsg((error as TResponse).statusCode, (error as TResponse).msg), - ); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } } onClose(); }; + // Close and reset form + const closeAndReset = () => { + onClose(); + onResetForm(); + }; + // prettier-ignore const { values, @@ -127,10 +166,12 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { stateSchema, stateValidatorSchema, onSubmitForm, + initialValue ); // prettier-ignore - const { + const { + id, name, identifiedCode, phone, @@ -138,6 +179,12 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { address } = values; + useEffect(() => { + if (isAdd) { + Object.keys(values).forEach((key) => (values[key] = '')); + } + }, [isAdd]); // eslint-disable-line + // Reset form const onResetForm = () => { setReset(!reset); @@ -146,7 +193,7 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { return (
- + { Add - + Close diff --git a/hotel-management/src/pages/User/Table.tsx b/hotel-management/src/pages/User/Table.tsx index fdcedd9..533ba3c 100644 --- a/hotel-management/src/pages/User/Table.tsx +++ b/hotel-management/src/pages/User/Table.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; // Components import { HiSquare2Stack } from 'react-icons/hi2'; @@ -14,15 +15,51 @@ import { TUser } from '../../globals/types'; // Constants import { useFetch } from '../../hooks/useFetch'; +import { USER_PATH } from '../../constants/path'; +import { STATUS_CODE } from '../../constants/statusCode'; +import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages'; // Styled import Spinner from '../../commons/styles/Spinner'; -type TUserModal = { user: TUser }; +// Utils +import { sendRequest } from '../../helpers/sendRequest'; -const UserRow = ({ user }: TUserModal) => { - const handleOnClick = (id: string) => { - alert(`Id: ${id}`); +interface IUserRow { + user: TUser; + openFormDialog: () => void; + setUser: React.Dispatch>; + reload: boolean; + setReload: React.Dispatch>; +} + +const UserRow = ({ + user, + openFormDialog, + setUser, + reload, + setReload, +}: IUserRow) => { + const handleOnEdit = (user: TUser) => { + setUser(user); + openFormDialog(); + }; + + const handleOnDelete = async (user: TUser) => { + if (confirm(CONFIRM_DELETE)) { + const response = await sendRequest( + USER_PATH + `/${user.id}`, + null, + 'DELETE', + ); + + if (response.statusCode === STATUS_CODE.OK) { + toast.success(DELETE_SUCCESS); + + // Reload table + setReload(!reload); + } + } }; const { id, name, identifiedCode, phone, roomId } = user; @@ -41,11 +78,11 @@ const UserRow = ({ user }: TUserModal) => { } - onClick={() => handleOnClick(id)} + onClick={() => handleOnEdit(user)} > Edit - } onClick={() => handleOnClick(id)}> + } onClick={() => handleOnDelete(user)}> Delete @@ -56,9 +93,17 @@ const UserRow = ({ user }: TUserModal) => { interface IUserTable { reload: boolean; + setReload: React.Dispatch>; + openFormDialog: () => void; + setUser?: React.Dispatch>; } -const UserTable = ({ reload }: IUserTable) => { +const UserTable = ({ + reload, + setReload, + openFormDialog, + setUser, +}: IUserTable) => { const { data, isPending, errorMsg } = useFetch('users', reload); const [users, setUsers] = useState([]); @@ -94,7 +139,16 @@ const UserTable = ({ reload }: IUserTable) => { data={users} - render={(user: TUser) => } + render={(user: TUser) => ( + + )} /> diff --git a/hotel-management/src/pages/User/index.tsx b/hotel-management/src/pages/User/index.tsx index ce784df..1163f29 100644 --- a/hotel-management/src/pages/User/index.tsx +++ b/hotel-management/src/pages/User/index.tsx @@ -9,15 +9,21 @@ import UserTable from './Table'; import { StyledUser, Title } from './styled'; import UserDialog from './Dialog'; +// Types +import { TUser } from '../../globals/types'; + const User = () => { const dialogRef = useRef(); const [reload, setReload] = useState(true); + const [user, setUser] = useState(null); + const [isAdd, setIsAdd] = useState(false); - const openDialog = () => { + const openFormDialog = (isAddForm: boolean = false) => { + setIsAdd(isAddForm); dialogRef.current?.showModal(); }; - const closeDialog = () => { + const closeFormDialog = () => { dialogRef.current?.close(); }; @@ -26,17 +32,24 @@ const User = () => { List User - + - + openFormDialog()} + setUser={setUser} + /> );