From b45130c1519a08bb66e94beff7598d8018e013b8 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Sun, 29 Oct 2023 21:33:49 +0700 Subject: [PATCH] Implement edit, delete user --- hotel-management/.gitignore | 2 + hotel-management/db.json | 40 ++++--- hotel-management/src/constants/messages.ts | 16 ++- hotel-management/src/constants/path.ts | 3 +- hotel-management/src/constants/statusCode.ts | 1 + hotel-management/src/globals/interfaces.ts | 4 + hotel-management/src/helpers/sendRequest.ts | 4 - hotel-management/src/helpers/utils.ts | 11 +- hotel-management/src/hooks/useForm.ts | 52 ++++++--- hotel-management/src/pages/User/Dialog.tsx | 10 +- hotel-management/src/pages/User/Form.tsx | 107 ++++++++++++++----- hotel-management/src/pages/User/Table.tsx | 68 ++++++++++-- hotel-management/src/pages/User/index.tsx | 17 ++- 13 files changed, 254 insertions(+), 81 deletions(-) diff --git a/hotel-management/.gitignore b/hotel-management/.gitignore index a547bf3..0d3d0da 100644 --- a/hotel-management/.gitignore +++ b/hotel-management/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +db.json diff --git a/hotel-management/db.json b/hotel-management/db.json index 07a3a79..2fb5fac 100644 --- a/hotel-management/db.json +++ b/hotel-management/db.json @@ -1,7 +1,7 @@ { "users": [ { - "id": "123", + "id": 1, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -9,7 +9,7 @@ "address": "Da Nang" }, { - "id": "231", + "id": 2, "name": "Phan Huu Loi", "identifiedCode": "123", "phone": "0937425123", @@ -17,7 +17,7 @@ "address": "Ho Chi Minh" }, { - "id": "312", + "id": 3, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -25,7 +25,7 @@ "address": "Da Nang" }, { - "id": "523", + "id": 4, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -33,7 +33,7 @@ "address": "Da Nang" }, { - "id": "534", + "id": 5, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -41,7 +41,7 @@ "address": "Da Nang" }, { - "id": "756", + "id": 6, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -49,7 +49,7 @@ "address": "Da Nang" }, { - "id": "23", + "id": 8, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -57,7 +57,7 @@ "address": "Da Nang" }, { - "id": "756", + "id": 9, "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", @@ -65,12 +65,28 @@ "address": "Da Nang" }, { - "id": "756", + "name": "Nezumi", + "identifiedCode": "3212", + "phone": "0327768351", + "roomId": "213", + "address": "32131212312", + "id": 10 + }, + { "name": "Nezumi", "identifiedCode": "123", "phone": "0937425123", - "roomId": 246, - "address": "Da Nang" + "roomId": "24632", + "address": "Da Nang232321", + "id": 13 + }, + { + "id": 16, + "name": "Phan Huu Loi", + "identifiedCode": "3213123212312", + "phone": "0327768321", + "roomId": "3123", + "address": "32131312332131" } ] -} +} \ No newline at end of file diff --git a/hotel-management/src/constants/messages.ts b/hotel-management/src/constants/messages.ts index cb5ad71..8fdb0e3 100644 --- a/hotel-management/src/constants/messages.ts +++ b/hotel-management/src/constants/messages.ts @@ -2,12 +2,20 @@ const INVALID_FORMAT_MSG = (field: string) => { return `Invalid ${field} format`; }; -const ADD_SUCCESS = 'Add success'; - const ERROR_MSG = (errorCode: number, msg: string) => { return `Error code: ${errorCode}. Message: ${msg}`; }; -const TIME_OUT_MSG = 'Connection time out!'; +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 { INVALID_FORMAT_MSG, ADD_SUCCESS, ERROR_MSG, TIME_OUT_MSG }; +export { + INVALID_FORMAT_MSG, + ADD_SUCCESS, + ERROR_MSG, + EDIT_SUCCESS, + CONFIRM_DELETE, + DELETE_SUCCESS, +}; diff --git a/hotel-management/src/constants/path.ts b/hotel-management/src/constants/path.ts index e4900ab..62c5bd6 100644 --- a/hotel-management/src/constants/path.ts +++ b/hotel-management/src/constants/path.ts @@ -2,4 +2,5 @@ export const DASHBOARD: string = '/dashboard'; 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 BASE_URL: string = 'http://localhost:3000/'; +export const USER_PATH = 'users'; diff --git a/hotel-management/src/constants/statusCode.ts b/hotel-management/src/constants/statusCode.ts index 3504d05..d0953b5 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, CONNECTION_TIME_OUT: 522, diff --git a/hotel-management/src/globals/interfaces.ts b/hotel-management/src/globals/interfaces.ts index 21baafc..0877e95 100644 --- a/hotel-management/src/globals/interfaces.ts +++ b/hotel-management/src/globals/interfaces.ts @@ -1,3 +1,5 @@ +import { TUser } from './types'; + interface IMenusContext { openId?: string; close?: () => void; @@ -27,6 +29,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/sendRequest.ts b/hotel-management/src/helpers/sendRequest.ts index f626704..4d2d264 100644 --- a/hotel-management/src/helpers/sendRequest.ts +++ b/hotel-management/src/helpers/sendRequest.ts @@ -8,9 +8,6 @@ export const sendRequest = async ( body: BodyInit | null | undefined, method: TMethodRequest = 'GET', ): Promise => { - const controller = new AbortController(); - const signal = controller.signal; - const response = await fetch(BASE_URL + path, { method, body, @@ -19,7 +16,6 @@ export const sendRequest = async ( 'Accept': 'application/json', 'Content-Type': 'application/json', }, - signal, }); return { diff --git a/hotel-management/src/helpers/utils.ts b/hotel-management/src/helpers/utils.ts index c804404..6b13527 100644 --- a/hotel-management/src/helpers/utils.ts +++ b/hotel-management/src/helpers/utils.ts @@ -1,5 +1,5 @@ import { INVALID_FORMAT_MSG } from '../constants/messages'; -import { TKeyValue, TPropValues, TStateSchema } from '../globals/types'; +import { TKeyValue, TPropValues, TStateSchema, TUser } from '../globals/types'; const VALUE = 'value'; const ERROR = 'error'; @@ -44,10 +44,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..bddd4b6 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,15 @@ const useForm = ( // Get a local copy of stateSchema useEffect(() => { - setInitialErrorState(); - }, []); // eslint-disable-line + setInitialErrorState(initialValue); + setDisable(true); + + if (initialValue) { + setValues({}); + setValues(getPropValues(stateSchema, VALUE)); + setDisable(false); + } + }, [initialValue]); // eslint-disable-line // Validate fields in forms const validateFormFields = useCallback( @@ -47,15 +55,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 +78,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 + ? 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 +133,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..e8c17a2 100644 --- a/hotel-management/src/pages/User/Dialog.tsx +++ b/hotel-management/src/pages/User/Dialog.tsx @@ -11,7 +11,7 @@ const UserDialog = forwardRef((props, ref) => { const dialogRef = ref as React.MutableRefObject< HTMLDialogElement | undefined >; - const { onClose, setReload, reload } = props; + const { onClose, setReload, reload, user, isAdd } = props; useEffect(() => { if (dialogRef.current) { @@ -31,7 +31,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 c3ddbd6..124f8ff 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, ERROR_MSG } from '../../constants/messages.ts'; +import { + ADD_SUCCESS, + EDIT_SUCCESS, + ERROR_MSG, +} from '../../constants/messages.ts'; +import { USER_PATH } from '../../constants/path.ts'; const FormBtn = styled(Button)` width: 100%; @@ -49,18 +54,34 @@ 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); + let initialValue: string = ''; + + if (isAdd) { + initialValue = ''; + } else if (user) { + initialValue = 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 +111,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(ERROR_MSG(response.statusCode, response.msg)); + } } else { - toast.error(ERROR_MSG(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(ERROR_MSG(response.statusCode, response.msg)); + } + } + // Reload table data + setReload(!reload); onResetForm(); - } catch (error) { - toast.error( - ERROR_MSG((error as TResponse).statusCode, (error as TResponse).msg), - ); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } } onClose(); }; + // Close form + const closeAndReset = () => { + onClose(); + onResetForm(); + }; + // prettier-ignore const { values, @@ -127,10 +168,12 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { stateSchema, stateValidatorSchema, onSubmitForm, + initialValue ); // prettier-ignore - const { + const { + id, name, identifiedCode, phone, @@ -138,6 +181,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 +195,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..22522c1 100644 --- a/hotel-management/src/pages/User/Table.tsx +++ b/hotel-management/src/pages/User/Table.tsx @@ -17,12 +17,47 @@ import { useFetch } from '../../hooks/useFetch'; // Styled import Spinner from '../../commons/styles/Spinner'; +import { sendRequest } from '../../helpers/sendRequest'; +import { USER_PATH } from '../../constants/path'; +import toast from 'react-hot-toast'; +import { STATUS_CODE } from '../../constants/statusCode'; +import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages'; -type TUserModal = { user: TUser }; +interface IUserRow { + user: TUser; + openFormDialog: () => void; + setUser: React.Dispatch>; + reload: boolean; + setReload: React.Dispatch>; +} -const UserRow = ({ user }: TUserModal) => { - const handleOnClick = (id: string) => { - alert(`Id: ${id}`); +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 +76,11 @@ const UserRow = ({ user }: TUserModal) => { } - onClick={() => handleOnClick(id)} + onClick={() => handleOnEdit(user)} > Edit - } onClick={() => handleOnClick(id)}> + } onClick={() => handleOnDelete(user)}> Delete @@ -56,9 +91,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 +137,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..5ed2196 100644 --- a/hotel-management/src/pages/User/index.tsx +++ b/hotel-management/src/pages/User/index.tsx @@ -8,12 +8,16 @@ import UserTable from './Table'; // Styled import { StyledUser, Title } from './styled'; import UserDialog from './Dialog'; +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 openDialog = (isAdd: boolean) => { + setIsAdd(isAdd); dialogRef.current?.showModal(); }; @@ -26,10 +30,15 @@ const User = () => { List User - + - + openDialog(false)} + setUser={setUser} + /> { ref={dialogRef} setReload={setReload} reload={reload} + user={user} + isAdd={isAdd} /> );