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",
"main": "index.js",
"license": "MIT",
"engines": {
"node": "18.x"
},
"scripts": {
"start": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
"prep": "yarn && yarn prepare-husky && yarn start",
@@ -7,7 +7,7 @@ import { Label } from 'Components/Label';
import { TextBox } from 'Components/TextBox';
import { CheckBox } from 'Components/CheckBox';
import { TextArea } from 'Components/TextArea';
import { ContractFormsSuccessToast } from '../ContractFormsSuccessToast';
import { ContractFormsToast } from '../ContractFormsToast';
import classes from './contractFormsModal.module.css';
@@ -35,6 +35,7 @@ interface Props {
submitText: string;
showToast: boolean;
toastMessage: string;
toastType?: string;
onFormSubmit: (e: any) => void;
onClickClose: (e: any) => void;
handleChange: (e: any) => void;
@@ -51,6 +52,7 @@ const ContractFormsModal = ({
formErrors,
showToast,
toastMessage,
toastType = 'success',
onFormSubmit,
onClickClose,
handleChange,
@@ -66,7 +68,7 @@ const ContractFormsModal = ({
leftHeaderIcon="projects"
modalHeader
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}>
<ValidateBackGround isValid={!formErrors?.name.length}>
@@ -139,6 +141,10 @@ const ContractFormsModal = ({
</Modal>
);
ContractFormsModal.defaultProps = {
toastType: 'success',
};
const ContractFormsModalMemo = memo(ContractFormsModal, areEqual);
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 {
background-color: #fff0f0;
border-bottom: 1px solid #e82828;
border-bottom: 1px solid #e82828 !important;
}
.toastWarning .toastText {
color: #d32f2f !important;
}
.toastCloseIcon {
@@ -0,0 +1 @@
export { ContractFormsToast } from './ContractFormsToast';
@@ -10,36 +10,45 @@ export interface Props {
isDisplayed: boolean;
message: string;
closeToast: (e: any) => void;
type?: 'success' | 'error';
}
const DeleteToast = ({ isDisplayed = false, message, closeToast }: Props) => (
<div
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>
);
const DeleteToast = ({ isDisplayed = false, message, closeToast, type = 'success' }: Props) => {
const getToastClass = () => (type === 'success' ? classes.toastSuccess : classes.toastWarning);
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);
+1 -1
View File
@@ -1,3 +1,3 @@
export { FormsList, ContractForms } from './FormTabs';
export { ContractFormsSuccessToast } from './ContractFormsSuccessToast';
export { ContractFormsToast } from './ContractFormsToast';
export { ContractFormsModal } from './ContractFormsModal';
@@ -33,6 +33,8 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
const [formData, setFormData] = useState({ ...initData });
const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false);
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [wasAdding, setWasAdding] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
@@ -89,6 +91,7 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
useEffect(() => {
if (isAdded) {
setToastType('success');
setToastMessage('Contract Form Added');
setShowToast(true);
setFormData({ ...initData });
@@ -102,7 +105,22 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
dispatch(setContractFormAdded(false));
}, 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(() => {
if (!isOpen) {
@@ -123,6 +141,7 @@ const AddContractForms = ({ isOpen, onClose }: Props) => {
}}
showToast={showToast}
toastMessage={toastMessage}
toastType={toastType}
onFormSubmit={onFormSubmit}
onClickClose={(e) => {
e.preventDefault();
@@ -33,6 +33,8 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
});
const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false);
const [toastType, setToastType] = useState<'success' | 'error'>('success');
const [wasEditing, setWasEditing] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
@@ -47,6 +49,7 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
useEffect(() => {
if (isEdited) {
setToastType('success');
setToastMessage('Contract Form Updated');
setShowToast(true);
if (companyId) {
@@ -59,8 +62,22 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
dispatch(setContractFormEdited(false));
}, 1500);
}
// eslint-disable-next-line
}, [isEdited]);
}, [isEdited, companyId, dispatch, onClose]);
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(() => {
if (!isOpen) {
@@ -139,6 +156,7 @@ const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
}}
showToast={showToast}
toastMessage={toastMessage}
toastType={toastType}
onFormSubmit={onFormSubmit}
onClickClose={(e) => {
e.preventDefault();
@@ -5,7 +5,7 @@ import {
contractFormsSelector,
fetchingContractFormsSelector,
companyIdSelector,
// deletingContractFormSelector,
deletingContractFormSelector,
contractFormDeletedSelector,
} from 'Containers/Forms/selectors';
import { ContractForms } from 'Components/Forms';
@@ -20,13 +20,16 @@ const ContractFormsContainer = () => {
const contractForms = useSelector(contractFormsSelector, areEqual);
const fetching = useSelector(fetchingContractFormsSelector, areEqual);
const companyId = useSelector(companyIdSelector, areEqual);
// const deleting = useSelector(deletingContractFormSelector, areEqual);
const deleting = useSelector(deletingContractFormSelector, areEqual);
const deletedId = useSelector(contractFormDeletedSelector, areEqual);
const [deleteModal, setDeleteModal] = useState({
isOpen: false,
id: null,
});
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 [editModal, setEditModal] = useState<{
isOpen: boolean;
@@ -89,15 +92,34 @@ const ContractFormsContainer = () => {
useEffect(() => {
if (deletedId) {
// Success case
setDeleteToastType('success');
setDeleteToastMessage('Form Deleted');
setShowDeletedToast(true);
dispatch(listCompanyContractForms(companyId));
setShowDeletedToast(true);
setTimeout(() => setShowDeletedToast(false), 1500);
dispatch(setDeletedFormId(null));
setTimeout(() => {
setShowDeletedToast(false);
dispatch(setDeletedFormId(null));
}, 1500);
}
}, [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(() => {
getContractForms();
@@ -128,7 +150,12 @@ const ContractFormsContainer = () => {
}
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
export const deleteContractForm = (contractFormId: number) => async (dispatch: any) => {
dispatch({ type: DELETING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest(dispatch, Api.delete(`/contract-forms/${contractFormId}`), '', DELETING_CONTRACT_FORM);
dispatch(setDeletedFormId(contractFormId));
// dispatch({ type: CONTRACT_FORM_DELETED, payload: contractFormId });
} catch (error) {
// handle error if needed
dispatch({ type: DELETING_CONTRACT_FORM, payload: false });
}
};
@@ -73,16 +73,19 @@ export const setContractFormAdded = (value: boolean) => (dispatch: any) => {
// Thunk to add a contract form
export const addContractForm = (formData: FormRequestBody) => async (dispatch: any) => {
dispatch({ type: ADDING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest(
dispatch,
Api.post('/contract-forms', formData),
'CONTRACT_FORM_ERRORS',
ADDING_CONTRACT_FORM
);
const response = await handleApiRequest(
dispatch,
Api.post('/contract-forms', formData),
'CONTRACT_FORM_ERRORS',
ADDING_CONTRACT_FORM
);
if (response) {
// API call succeeded
dispatch(setContractFormAdded(true));
} catch (error) {
dispatch({ type: ADDING_CONTRACT_FORM, payload: false });
} else {
// API call failed
dispatch(setContractFormAdded(false));
}
};
@@ -97,16 +100,19 @@ export const setContractFormEdited = (value: boolean) => (dispatch: any) => {
// Thunk to edit a contract form
export const editContractForm = (formData: FormRequestBody, contractId: string) => async (dispatch: any) => {
dispatch({ type: EDITING_CONTRACT_FORM, payload: true });
try {
await handleApiRequest(
dispatch,
Api.put('/contract-forms', formData),
'CONTRACT_FORM_ERRORS',
EDITING_CONTRACT_FORM
);
const response = await handleApiRequest(
dispatch,
Api.put('/contract-forms', formData),
'CONTRACT_FORM_ERRORS',
EDITING_CONTRACT_FORM
);
if (response) {
// API call succeeded
dispatch(setContractFormEdited(true));
} catch (error) {
dispatch({ type: EDITING_CONTRACT_FORM, payload: false });
} else {
// API call failed
dispatch(setContractFormEdited(false));
}
};