Refactor; add, edit and delete contract forms logic and api calls

This commit is contained in:
Ayobami
2025-07-06 23:10:29 +01:00
parent b7bbcc50c6
commit e9f51d6ea0
13 changed files with 197 additions and 102 deletions
-3
View File
@@ -3,9 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"engines": {
"node": "18.x"
},
"scripts": { "scripts": {
"start": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js", "start": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
"prep": "yarn && yarn prepare-husky && yarn start", "prep": "yarn && yarn prepare-husky && yarn start",
@@ -7,7 +7,7 @@ import { Label } from 'Components/Label';
import { TextBox } from 'Components/TextBox'; import { TextBox } from 'Components/TextBox';
import { CheckBox } from 'Components/CheckBox'; import { CheckBox } from 'Components/CheckBox';
import { TextArea } from 'Components/TextArea'; import { TextArea } from 'Components/TextArea';
import { ContractFormsSuccessToast } from '../ContractFormsSuccessToast'; import { ContractFormsToast } from '../ContractFormsToast';
import classes from './contractFormsModal.module.css'; import classes from './contractFormsModal.module.css';
@@ -35,6 +35,7 @@ interface Props {
submitText: string; submitText: string;
showToast: boolean; showToast: boolean;
toastMessage: string; toastMessage: string;
toastType?: string;
onFormSubmit: (e: any) => void; onFormSubmit: (e: any) => void;
onClickClose: (e: any) => void; onClickClose: (e: any) => void;
handleChange: (e: any) => void; handleChange: (e: any) => void;
@@ -51,6 +52,7 @@ const ContractFormsModal = ({
formErrors, formErrors,
showToast, showToast,
toastMessage, toastMessage,
toastType = 'success',
onFormSubmit, onFormSubmit,
onClickClose, onClickClose,
handleChange, handleChange,
@@ -66,7 +68,7 @@ const ContractFormsModal = ({
leftHeaderIcon="projects" leftHeaderIcon="projects"
modalHeader modalHeader
modalCloseClick={onClickClose} modalCloseClick={onClickClose}
toast={<ContractFormsSuccessToast showToast={showToast} message={toastMessage} />} toast={<ContractFormsToast type={toastType as 'success' | 'error'} showToast={showToast} message={toastMessage} />}
> >
<form className={classes.form} onSubmit={onFormSubmit}> <form className={classes.form} onSubmit={onFormSubmit}>
<ValidateBackGround isValid={!formErrors?.name.length}> <ValidateBackGround isValid={!formErrors?.name.length}>
@@ -139,6 +141,10 @@ const ContractFormsModal = ({
</Modal> </Modal>
); );
ContractFormsModal.defaultProps = {
toastType: 'success',
};
const ContractFormsModalMemo = memo(ContractFormsModal, areEqual); const ContractFormsModalMemo = memo(ContractFormsModal, areEqual);
export { ContractFormsModalMemo as ContractFormsModal }; export { ContractFormsModalMemo as ContractFormsModal };
@@ -1,35 +0,0 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { CheckedMarkSvg } from 'Components/Icons/CheckedMark';
import classes from './contractFormsSuccessToast.module.css';
export interface Props {
showToast: boolean;
message: string;
}
const ContractFormsSuccessToast = ({ showToast = false, message }: Props) => (
<div
className={`toast fade d-flex align-items-center position-absolute border-0 bottom-0 ${
showToast ? 'show' : 'hide'
} ${classes.toastBase} ${classes.toastSuccess}`}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className={`toast-body ${classes['toast-body-override']} ${classes.toastText}`}>
{message}
<span className={`${classes.toastIcon}`}>
<CheckedMarkSvg />
</span>
</div>
</div>
);
ContractFormsSuccessToast.defaultProps = {};
const ContractFormsSuccessToastMemo = memo(ContractFormsSuccessToast, areEqual);
export { ContractFormsSuccessToastMemo as ContractFormsSuccessToast };
@@ -1 +0,0 @@
export { ContractFormsSuccessToast } from './ContractFormsSuccessToast';
@@ -0,0 +1,44 @@
import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { CheckedMarkSvg } from 'Components/Icons/CheckedMark';
import classes from './contractFormsToast.module.css';
export interface Props {
showToast: boolean;
message: string;
type?: 'success' | 'error';
}
const ContractFormsToast = ({ showToast = false, message, type = 'success' }: Props) => {
const getToastClass = () => (type === 'success' ? classes.toastSuccess : classes.toastWarning);
return (
<div
className={`toast fade d-flex align-items-center position-absolute border-0 bottom-0 ${
showToast ? 'show' : 'hide'
} ${classes.toastBase} ${getToastClass()}`}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className={`toast-body ${classes['toast-body-override']} ${classes.toastText}`}>
{message}
{type === 'success' && (
<span className={`${classes.toastIcon}`}>
<CheckedMarkSvg />
</span>
)}
</div>
</div>
);
};
ContractFormsToast.defaultProps = {
type: 'success',
};
const ContractFormsToastMemo = memo(ContractFormsToast, areEqual);
export { ContractFormsToastMemo as ContractFormsToast };
@@ -57,7 +57,11 @@
.toastWarning { .toastWarning {
background-color: #fff0f0; background-color: #fff0f0;
border-bottom: 1px solid #e82828; border-bottom: 1px solid #e82828 !important;
}
.toastWarning .toastText {
color: #d32f2f !important;
} }
.toastCloseIcon { .toastCloseIcon {
@@ -0,0 +1 @@
export { ContractFormsToast } from './ContractFormsToast';
@@ -10,36 +10,45 @@ export interface Props {
isDisplayed: boolean; isDisplayed: boolean;
message: string; message: string;
closeToast: (e: any) => void; closeToast: (e: any) => void;
type?: 'success' | 'error';
} }
const DeleteToast = ({ isDisplayed = false, message, closeToast }: Props) => ( const DeleteToast = ({ isDisplayed = false, message, closeToast, type = 'success' }: Props) => {
<div const getToastClass = () => (type === 'success' ? classes.toastSuccess : classes.toastWarning);
className={`toast fade d-flex align-items-center position-fixed border-0 bottom-0 ${
isDisplayed ? 'show' : 'hide'
} ${classes.toastBase} ${classes.toastSuccess}`}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className={`toast-body ${classes['toast-body-override']} ${classes.toastText}`}>
{message}
<span className={`${classes.toastIcon}`}>
<CheckedMarkSvg />
</span>
</div>
<div className={classes.toastCloseButtonContainer}>
<Button
type="button"
className={`btn-close ${classes.toastCloseButton}`}
data-bs-dismiss="toast"
aria-label="Close"
onClick={closeToast}
/>
</div>
</div>
);
DeleteToast.defaultProps = {}; return (
<div
className={`toast fade d-flex align-items-center position-fixed border-0 bottom-0 ${
isDisplayed ? 'show' : 'hide'
} ${classes.toastBase} ${getToastClass()}`}
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<div className={`toast-body ${classes['toast-body-override']} ${classes.toastText}`}>
{message}
{type === 'success' && (
<span className={`${classes.toastIcon}`}>
<CheckedMarkSvg />
</span>
)}
</div>
<div className={classes.toastCloseButtonContainer}>
<Button
type="button"
className={`btn-close ${classes.toastCloseButton}`}
data-bs-dismiss="toast"
aria-label="Close"
onClick={closeToast}
/>
</div>
</div>
);
};
DeleteToast.defaultProps = {
type: 'success',
};
const DeleteToastMemo = memo(DeleteToast, areEqual); const DeleteToastMemo = memo(DeleteToast, areEqual);
+1 -1
View File
@@ -1,3 +1,3 @@
export { FormsList, ContractForms } from './FormTabs'; export { FormsList, ContractForms } from './FormTabs';
export { ContractFormsSuccessToast } from './ContractFormsSuccessToast'; export { ContractFormsToast } from './ContractFormsToast';
export { ContractFormsModal } from './ContractFormsModal'; export { ContractFormsModal } from './ContractFormsModal';
@@ -33,6 +33,8 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
const [formData, setFormData] = useState({ ...initData }); const [formData, setFormData] = useState({ ...initData });
const [toastMessage, setToastMessage] = useState(''); const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false); const [showToast, setShowToast] = useState(false);
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [wasAdding, setWasAdding] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
@@ -89,6 +91,7 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
useEffect(() => { useEffect(() => {
if (isAdded) { if (isAdded) {
setToastType('success');
setToastMessage('Contract Form Added'); setToastMessage('Contract Form Added');
setShowToast(true); setShowToast(true);
setFormData({ ...initData }); setFormData({ ...initData });
@@ -102,7 +105,22 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
dispatch(setContractFormAdded(false)); dispatch(setContractFormAdded(false));
}, 1500); }, 1500);
} }
}, [isAdded]); }, [isAdded, companyId, dispatch, onClose]);
useEffect(() => {
if (isAdding) {
setWasAdding(true);
} else if (wasAdding && !isAdded) {
// API call finished but form wasn't added (error occurred)
setToastType('error');
setToastMessage('Failed to add contract form. Please try again.');
setShowToast(true);
setWasAdding(false);
setTimeout(() => {
setShowToast(false);
}, 3000);
}
}, [isAdding, isAdded, wasAdding]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@@ -123,6 +141,7 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
}} }}
showToast={showToast} showToast={showToast}
toastMessage={toastMessage} toastMessage={toastMessage}
toastType={toastType}
onFormSubmit={onFormSubmit} onFormSubmit={onFormSubmit}
onClickClose={(e) => { onClickClose={(e) => {
e.preventDefault(); e.preventDefault();
@@ -33,6 +33,8 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
}); });
const [toastMessage, setToastMessage] = useState(''); const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false); const [showToast, setShowToast] = useState(false);
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [wasEditing, setWasEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null); const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => { useEffect(() => {
@@ -47,6 +49,7 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
useEffect(() => { useEffect(() => {
if (isEdited) { if (isEdited) {
setToastType('success');
setToastMessage('Contract Form Updated'); setToastMessage('Contract Form Updated');
setShowToast(true); setShowToast(true);
if (companyId) { if (companyId) {
@@ -59,8 +62,22 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
dispatch(setContractFormEdited(false)); dispatch(setContractFormEdited(false));
}, 1500); }, 1500);
} }
// eslint-disable-next-line }, [isEdited, companyId, dispatch, onClose]);
}, [isEdited]);
useEffect(() => {
if (isEditing) {
setWasEditing(true);
} else if (wasEditing && !isEdited) {
// API call finished but form wasn't edited (error occurred)
setToastType('error');
setToastMessage('Failed to update contract form. Please try again.');
setShowToast(true);
setWasEditing(false);
setTimeout(() => {
setShowToast(false);
}, 3000);
}
}, [isEditing, isEdited, wasEditing]);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@@ -139,6 +156,7 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
}} }}
showToast={showToast} showToast={showToast}
toastMessage={toastMessage} toastMessage={toastMessage}
toastType={toastType}
onFormSubmit={onFormSubmit} onFormSubmit={onFormSubmit}
onClickClose={(e) => { onClickClose={(e) => {
e.preventDefault(); e.preventDefault();
@@ -5,7 +5,7 @@ import {
contractFormsSelector, contractFormsSelector,
fetchingContractFormsSelector, fetchingContractFormsSelector,
companyIdSelector, companyIdSelector,
// deletingContractFormSelector, deletingContractFormSelector,
contractFormDeletedSelector, contractFormDeletedSelector,
} from 'Containers/Forms/selectors'; } from 'Containers/Forms/selectors';
import { ContractForms } from 'Components/Forms'; import { ContractForms } from 'Components/Forms';
@@ -20,13 +20,16 @@ const ContractFormsContainer = () => {
const contractForms = useSelector(contractFormsSelector, areEqual); const contractForms = useSelector(contractFormsSelector, areEqual);
const fetching = useSelector(fetchingContractFormsSelector, areEqual); const fetching = useSelector(fetchingContractFormsSelector, areEqual);
const companyId = useSelector(companyIdSelector, areEqual); const companyId = useSelector(companyIdSelector, areEqual);
// const deleting = useSelector(deletingContractFormSelector, areEqual); const deleting = useSelector(deletingContractFormSelector, areEqual);
const deletedId = useSelector(contractFormDeletedSelector, areEqual); const deletedId = useSelector(contractFormDeletedSelector, areEqual);
const [deleteModal, setDeleteModal] = useState({ const [deleteModal, setDeleteModal] = useState({
isOpen: false, isOpen: false,
id: null, id: null,
}); });
const [showDeletedToast, setShowDeletedToast] = useState(false); const [showDeletedToast, setShowDeletedToast] = useState(false);
const [deleteToastMessage, setDeleteToastMessage] = useState('Form Deleted');
const [deleteToastType, setDeleteToastType] = useState<'success' | 'error'>('success');
const [wasDeleting, setWasDeleting] = useState(false);
const [showAddFormsModal, setShowAddFormsModal] = useState(false); const [showAddFormsModal, setShowAddFormsModal] = useState(false);
const [editModal, setEditModal] = useState<{ const [editModal, setEditModal] = useState<{
isOpen: boolean; isOpen: boolean;
@@ -89,15 +92,34 @@ const ContractFormsContainer = () => {
useEffect(() => { useEffect(() => {
if (deletedId) { if (deletedId) {
// Success case
setDeleteToastType('success');
setDeleteToastMessage('Form Deleted');
setShowDeletedToast(true);
dispatch(listCompanyContractForms(companyId)); dispatch(listCompanyContractForms(companyId));
setShowDeletedToast(true); setTimeout(() => {
setShowDeletedToast(false);
setTimeout(() => setShowDeletedToast(false), 1500); dispatch(setDeletedFormId(null));
}, 1500);
dispatch(setDeletedFormId(null));
} }
}, [deletedId]); }, [deletedId, companyId, dispatch]);
useEffect(() => {
if (deleting) {
setWasDeleting(true);
} else if (wasDeleting && !deletedId) {
// API call finished but form wasn't deleted (error occurred)
setDeleteToastType('error');
setDeleteToastMessage('Failed to delete form. Please try again.');
setShowDeletedToast(true);
setWasDeleting(false);
setTimeout(() => {
setShowDeletedToast(false);
}, 3000);
}
}, [deleting, deletedId, wasDeleting]);
useEffect(() => { useEffect(() => {
getContractForms(); getContractForms();
@@ -128,7 +150,12 @@ const ContractFormsContainer = () => {
} }
onDelete={handleDelete} onDelete={handleDelete}
/> />
{deletedId && <DeleteToast isDisplayed={showDeletedToast} closeToast={closeToast} message="Form Deleted" />} <DeleteToast
isDisplayed={showDeletedToast}
closeToast={closeToast}
message={deleteToastMessage}
type={deleteToastType}
/>
</> </>
); );
}; };
+26 -20
View File
@@ -53,12 +53,12 @@ export const setDeletedFormId = (value: number | null) => async (dispatch: any)
// Thunk to delete a contract form by id // Thunk to delete a contract form by id
export const deleteContractForm = (contractFormId: number) => async (dispatch: any) => { export const deleteContractForm = (contractFormId: number) => async (dispatch: any) => {
dispatch({ type: DELETING_CONTRACT_FORM, payload: true }); dispatch({ type: DELETING_CONTRACT_FORM, payload: true });
try { try {
await handleApiRequest(dispatch, Api.delete(`/contract-forms/${contractFormId}`), '', DELETING_CONTRACT_FORM); await handleApiRequest(dispatch, Api.delete(`/contract-forms/${contractFormId}`), '', DELETING_CONTRACT_FORM);
dispatch(setDeletedFormId(contractFormId)); dispatch(setDeletedFormId(contractFormId));
// dispatch({ type: CONTRACT_FORM_DELETED, payload: contractFormId });
} catch (error) { } catch (error) {
// handle error if needed
dispatch({ type: DELETING_CONTRACT_FORM, payload: false }); dispatch({ type: DELETING_CONTRACT_FORM, payload: false });
} }
}; };
@@ -73,16 +73,19 @@ export const setContractFormAdded = (value: boolean) => (dispatch: any) => {
// Thunk to add a contract form // Thunk to add a contract form
export const addContractForm = (formData: FormRequestBody) => async (dispatch: any) => { export const addContractForm = (formData: FormRequestBody) => async (dispatch: any) => {
dispatch({ type: ADDING_CONTRACT_FORM, payload: true }); dispatch({ type: ADDING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest( const response = await handleApiRequest(
dispatch, dispatch,
Api.post('/contract-forms', formData), Api.post('/contract-forms', formData),
'CONTRACT_FORM_ERRORS', 'CONTRACT_FORM_ERRORS',
ADDING_CONTRACT_FORM ADDING_CONTRACT_FORM
); );
if (response) {
// API call succeeded
dispatch(setContractFormAdded(true)); dispatch(setContractFormAdded(true));
} catch (error) { } else {
dispatch({ type: ADDING_CONTRACT_FORM, payload: false }); // API call failed
dispatch(setContractFormAdded(false)); dispatch(setContractFormAdded(false));
} }
}; };
@@ -97,16 +100,19 @@ export const setContractFormEdited = (value: boolean) => (dispatch: any) => {
// Thunk to edit a contract form // Thunk to edit a contract form
export const editContractForm = (formData: FormRequestBody, contractId: string) => async (dispatch: any) => { export const editContractForm = (formData: FormRequestBody, contractId: string) => async (dispatch: any) => {
dispatch({ type: EDITING_CONTRACT_FORM, payload: true }); dispatch({ type: EDITING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest( const response = await handleApiRequest(
dispatch, dispatch,
Api.put('/contract-forms', formData), Api.put('/contract-forms', formData),
'CONTRACT_FORM_ERRORS', 'CONTRACT_FORM_ERRORS',
EDITING_CONTRACT_FORM EDITING_CONTRACT_FORM
); );
if (response) {
// API call succeeded
dispatch(setContractFormEdited(true)); dispatch(setContractFormEdited(true));
} catch (error) { } else {
dispatch({ type: EDITING_CONTRACT_FORM, payload: false }); // API call failed
dispatch(setContractFormEdited(false)); dispatch(setContractFormEdited(false));
} }
}; };