feat: add create and edit forms functionality

This commit is contained in:
Ayobami
2025-07-04 20:13:32 +01:00
parent 3b46073ee8
commit aa66f262b2
24 changed files with 892 additions and 71 deletions
+11 -11
View File
@@ -1,7 +1,7 @@
import React, { memo, ReactNode, useCallback, useRef, useState } from "react"; import React, { memo, ReactNode, useCallback, useRef, useState } from 'react';
import { areEqualShallow } from "Utils/equalityChecks"; import { areEqualShallow } from 'Utils/equalityChecks';
import { useDebounce } from "Hooks/useDebounce"; import { useDebounce } from 'Hooks/useDebounce';
interface Props { interface Props {
id?: string; id?: string;
@@ -38,16 +38,16 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
const onSubmitLocal = useDebounce((e: any) => { const onSubmitLocal = useDebounce((e: any) => {
e.preventDefault(); e.preventDefault();
formRef.current.classList.add("was-validated"); formRef.current.classList.add('was-validated');
let value = ""; let value = '';
switch (e.target.type) { switch (e.target.type) {
case "checkbox": case 'checkbox':
value = e.target.checked; value = e.target.checked;
break; break;
case "text": case 'text':
case "tel": case 'tel':
case "email": case 'email':
value = e.target.value.trim(); value = e.target.value.trim();
break; break;
default: default:
@@ -57,7 +57,7 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
// This will handle an empty input // This will handle an empty input
if (value?.length === 0) { if (value?.length === 0) {
validate({ [e.target.name]: e.target.value, isValid: true }); validate({ [e.target.name]: e.target.value, isValid: true });
formRef.current.classList.remove("was-validated"); formRef.current.classList.remove('was-validated');
return; return;
} }
@@ -99,7 +99,7 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
}} }}
onSubmit={(submitButton && onSubmitLocal) || submitWhenNoSubmitButton} onSubmit={(submitButton && onSubmitLocal) || submitWhenNoSubmitButton}
id={id} id={id}
className={`requires-validation ${className || ""}`} className={`requires-validation ${className || ''}`}
noValidate={noValidate} noValidate={noValidate}
> >
{children} {children}
@@ -0,0 +1,144 @@
import React, { memo, RefObject } from 'react';
import { Modal } from 'Components/Modal';
import { areEqual } from 'Utils/equalityChecks';
import { ValidateBackGround } from 'Components/Validation';
import { PurpleButton } from 'Components/Button';
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 classes from './contractFormsModal.module.css';
const templateTags = [
'~~~name~~~',
'~~~project~~~',
'~~~job_no~~~',
'~~~company~~~',
'~~~current_date~~~',
'~~~date_of_loss~~~',
'~~~company_address~~~',
'~~~policy_holder_name~~~',
'~~~policy_number~~~',
'~~~claim_number~~~',
'~~~input~~~',
'~~~checkbox~~~',
'~~~company_logo~~~',
];
interface Props {
isOpen: boolean;
formData: any;
formErrors: any;
title: string;
submitText: string;
showToast: boolean;
toastMessage: string;
onFormSubmit: (e: any) => void;
onClickClose: (e: any) => void;
handleChange: (e: any) => void;
textAreaRef: RefObject<HTMLTextAreaElement>;
handleTagClick: (tag: string) => void;
submitting: boolean;
}
const ContractFormsModal = ({
isOpen,
title,
submitText,
formData,
formErrors,
showToast,
toastMessage,
onFormSubmit,
onClickClose,
handleChange,
textAreaRef,
handleTagClick,
submitting,
}: Props) => (
<Modal
id="contractsFormModal"
classes={classes}
title={title}
isOpen={isOpen}
leftHeaderIcon="projects"
modalHeader
modalCloseClick={onClickClose}
toast={<ContractFormsSuccessToast showToast={showToast} message={toastMessage} />}
>
<form className={classes.form} onSubmit={onFormSubmit}>
<ValidateBackGround isValid={!formErrors?.name.length}>
<Label ariaLabel="Form Name" className={classes.formNameLabel} htmlFor="formName">
Form Name
</Label>
<TextBox
name="name"
id="name"
type="text"
className={`${classes.validateField} ${formErrors?.name.length ? classes.invalidField : classes.validField} ${
formErrors?.name.length ? 'is-invalid' : ''
}`}
placeholder="Authorization form"
ariaLabel="Form Name"
value={formData.name}
onChange={handleChange}
/>
<div className={`${classes.invalidFieldFeedback} invalid-feedback`}>{formErrors?.name?.[0]}</div>
</ValidateBackGround>
<div className={classes.requireSignatureContainer}>
<Label ariaLabel="Require signature" className={classes.requireSignatureLabel} htmlFor="requireSignature">
Require Signature
</Label>
<CheckBox
checked={formData.requireSignature}
name="requireSignature"
id="requireSignature"
onChange={handleChange}
className={classes.requireSignatureCheckbox}
/>
</div>
<div className={classes.contractTemplateContainer}>
<Label ariaLabel="Contract Template" htmlFor="template" className="text-right pt-2">
Contract Template
</Label>
<div className={classes.contractTemplateFlex}>
<div className={classes.templateTags}>
{templateTags.map((tag) => (
<button key={tag} type="button" onKeyDown={() => {}} className="" onClick={() => handleTagClick(tag)}>
{tag}
</button>
))}
</div>
<div className={classes.textAreaWrapper}>
{formErrors?.template?.[0] && <p className={classes.textAreaWarning}>{formErrors?.template?.[0]}</p>}
<TextArea
id="template"
minRows={15}
name="template"
ariaLabel="contract template"
ref={textAreaRef}
value={formData.template}
onChange={handleChange}
className={classes.templateTextarea}
placeholder="Type your contract template here or click on tags from the left panel..."
/>
</div>
</div>
</div>
<div className={classes.footer}>
<PurpleButton type="submit" className={classes.submitButton} disabled={submitting} outlined>
{!submitting ? submitText : '...'}
</PurpleButton>
</div>
</form>
</Modal>
);
const ContractFormsModalMemo = memo(ContractFormsModal, areEqual);
export { ContractFormsModalMemo as ContractFormsModal };
@@ -0,0 +1,85 @@
.modalDialog {
max-width: 1200px;
/* height: 80%; */
}
.form {
gap: 24px;
height: 100%;
display: flex;
flex-direction: column;
}
.requireSignatureContainer {
display: flex;
gap: 10px;
align-items: start;
}
.requireSignatureContainer label {
padding: 0px;
margin-top: -6px;
}
.contractTemplateContainer {
flex: 1 1 0px;
display: flex;
flex-direction: column;
gap: 8px;
margin-top: -10px;
}
.contractTemplateFlex {
display: flex;
width: 100%;
}
.templateTags {
border: 1px solid black;
border-radius: 8px;
padding: 10px 4px;
display: flex;
flex-direction: column;
gap: 4px;
width: fit-content;
}
.templateTags button {
background: none;
border: none;
cursor: pointer;
text-align: left;
padding: 2px 0;
}
.templateTags button:hover {
background-color: aliceblue;
}
.textAreaWrapper {
flex: 1 1 0px;
max-height: 400px;
height: fit-content;
overflow-y: auto;
border: 1px solid rgba(102, 98, 98, 0.695);
border-radius: 8px;
}
.textAreaWarning {
background: rgba(222, 84, 84, 0.288);
width: 100%;
padding: 3px;
font-size: 12px;
color: rgba(188, 14, 14, 0.911);
}
.textAreaWrapper:focus-within {
outline: 4px solid rgba(98, 172, 238, 0.866);
outline-offset: 2px;
transition: all;
}
.templateTextarea {
width: 100%;
resize: none;
border: none;
}
.footer {
margin-top: 10px;
padding-top: 10px;
}
.submitButton {
width: fit-content;
margin: 0 auto;
}
.submitButton:disabled {
opacity: 0.6;
}
@@ -0,0 +1 @@
export { ContractFormsModal } from './ContractFormsModal';
@@ -0,0 +1,35 @@
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 };
@@ -0,0 +1,78 @@
.toastBase {
height: 40px !important;
width: 100% !important;
z-index: 1000;
box-shadow: none;
border-radius: 0;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
padding-top: 1rem !important;
padding-bottom: 1rem !important;
transition: opacity 0.15s linear;
position: absolute;
right: 0;
text-align: center;
}
.toast-body-override {
width: 95%;
padding: unset ip !important;
}
.toastCloseButtonContainer {
display: flex;
justify-content: flex-end;
width: 10%;
}
.toastCloseButton {
border-radius: 50% !important;
color: #5b476b !important;
background-color: #fff !important;
padding: 0.25rem !important;
opacity: unset !important;
/* This is the actual svg pulled from bootstrap. not the 0.5em beside center on the 2nd line. This is what has changed */
background: transparent
url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e")
center/0.5em auto no-repeat;
}
.toastIcon {
padding-left: 10px;
}
.toastCloseButton:hover {
color: #000;
text-decoration: none;
opacity: unset !important;
}
.toastText {
font-family: IBM Plex Sans !important;
font-style: normal !important;
font-weight: 600 !important;
font-size: 16px !important;
line-height: 24px !important;
color: #5b476b !important;
}
.toastSuccess {
background-color: #dcf5f0;
border-bottom: 1px solid #40c9ae !important;
}
.toastWarning {
background-color: #fff0f0;
border-bottom: 1px solid #e82828;
}
.toastCloseIcon {
position: absolute;
right: 22px;
}
@media (max-width: 768px) {
.toastBase {
width: 100%;
/* height: auto; */
padding: 12px 66px 12px 22px;
}
.toastText {
font-size: 14px;
}
}
@@ -0,0 +1 @@
export { ContractFormsSuccessToast } from './ContractFormsSuccessToast';
@@ -1,4 +1,4 @@
import { FormTemplateResponse } from 'Containers/Forms/Models'; import { FormTemplate, FormTemplateResponse } from 'Containers/Forms/Models';
import React, { memo } from 'react'; import React, { memo } from 'react';
import { TabContent } from 'Components/Tabs'; import { TabContent } from 'Components/Tabs';
import { areEqual } from 'Utils/equalityChecks'; import { areEqual } from 'Utils/equalityChecks';
@@ -11,8 +11,8 @@ import classes from './contractForms.module.css';
interface Props { interface Props {
forms: FormTemplateResponse; forms: FormTemplateResponse;
fetching: boolean; fetching: boolean;
onClickRow?: (e: any) => void; onClickRow?: (form: FormTemplate) => void;
onAdd: () => void; onAdd: (e: any) => void;
onDelete: (id: number) => void; onDelete: (id: number) => void;
} }
@@ -26,7 +26,7 @@ const ContractForms = ({ forms, onClickRow, fetching, onAdd, onDelete }: Props)
</PurpleButton> </PurpleButton>
</div> </div>
{fetching && <Spinner loading />} {fetching && <Spinner loading />}
{!fetching && <FormsList forms={forms} onClickRow={onClickRow} onDelete={onDelete} />} {!fetching && <FormsList forms={forms} onClickRow={onClickRow} onDelete={onDelete} onAdd={onAdd} />}
</div> </div>
</TabContent> </TabContent>
); );
@@ -16,6 +16,7 @@ const DeleteFormModal = ({ id, isOpen, modalCloseClick, onDelete }: Props) => (
<div> <div>
<Modal <Modal
id={id && id.toString()} id={id && id.toString()}
leftHeaderIcon="projects"
classes={classes} classes={classes}
title="Delete Contract Form?" title="Delete Contract Form?"
isOpen={isOpen} isOpen={isOpen}
@@ -12,10 +12,17 @@ import classes from './formsList.module.css';
interface Props { interface Props {
forms: FormTemplateResponse; forms: FormTemplateResponse;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onClickRow?: (e: any) => void; onAdd: (e: any) => void;
onClickRow?: (form: FormTemplate) => void;
} }
const FormsList = ({ forms, onClickRow, onDelete }: Props) => ( const FormsList = ({ forms, onClickRow, onDelete, onAdd }: Props) => {
const onRowClick = (e: any, form: any) => {
e.preventDefault();
onClickRow(form);
};
return (
<> <>
{forms?.data?.length > 0 ? ( {forms?.data?.length > 0 ? (
<Table className={`table ${classes.formListWrapper}`}> <Table className={`table ${classes.formListWrapper}`}>
@@ -27,20 +34,20 @@ const FormsList = ({ forms, onClickRow, onDelete }: Props) => (
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{forms.data.map(({ id, created_at: createdAt, name }: FormTemplate) => ( {forms.data.map((form: FormTemplate) => (
<TableRow key={id}> <TableRow key={form.id}>
<TableColumn dataId={id} tdOnClick={onClickRow}> <TableColumn dataId={form.id} tdOnClick={(e) => onRowClick(e, form)}>
<p>{name}</p> <p>{form.name}</p>
</TableColumn> </TableColumn>
<TableColumn dataId={id} tdOnClick={onClickRow} className={classes.columnContent}> <TableColumn dataId={form.id} tdOnClick={(e) => onRowClick(e, form)} className={classes.columnContent}>
<p className={classes.numberAndDate}>{formatDate(createdAt, 'PP')}</p> <p className={classes.numberAndDate}>{formatDate(form.created_at, 'PP')}</p>
</TableColumn> </TableColumn>
<TableColumn> <TableColumn>
<button <button
className={classes.deleteBtn} className={classes.deleteBtn}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(id); onDelete(form.id);
}} }}
> >
<Icon type="trash" /> <Icon type="trash" />
@@ -51,10 +58,11 @@ const FormsList = ({ forms, onClickRow, onDelete }: Props) => (
</TableBody> </TableBody>
</Table> </Table>
) : ( ) : (
<NoFormsTable /> <NoFormsTable onAdd={onAdd} />
)} )}
</> </>
); );
};
FormsList.defaultProps = { FormsList.defaultProps = {
onClickRow: null, onClickRow: null,
@@ -1,11 +1,12 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { areEqual } from 'Utils/equalityChecks'; import { areEqual } from 'Utils/equalityChecks';
import { Table, TableHeader, TableRow, Th } from 'Components/Table'; import { Table, TableHeader, TableRow, Th } from 'Components/Table';
import { PurpleButton } from 'Components/Button';
import { Icon } from 'Components/Icons'; import { Icon } from 'Components/Icons';
import classes from './noFormsTable.module.css'; import classes from './noFormsTable.module.css';
const NoFormsTable = () => ( const NoFormsTable = ({ onAdd }: { onAdd: (e) => void }) => (
<div> <div>
<Table className={`table ${classes.formListWrapper}`}> <Table className={`table ${classes.formListWrapper}`}>
<TableHeader> <TableHeader>
@@ -17,8 +18,11 @@ const NoFormsTable = () => (
</TableHeader> </TableHeader>
</Table> </Table>
<div className={`d-flex justify-content-center align-items-center flex-column w-100 ${classes.noFormsContent}`}> <div className={`d-flex justify-content-center align-items-center flex-column w-100 ${classes.noFormsContent}`}>
<p className={classes.noFormsText}>No forms yet. Create a new form.</p>
<Icon type="projects" /> <Icon type="projects" />
<p className={classes.noFormsText}>No forms yet. Create a new form.</p>
<PurpleButton className={classes.addButton} onClick={onAdd}>
Add +
</PurpleButton>
</div> </div>
</div> </div>
); );
@@ -48,3 +48,11 @@
line-height: 30px; line-height: 30px;
color: #b3abc6; color: #b3abc6;
} }
.addButton {
background: #9a00ff;
color: #fff;
font-weight: 500;
width: 160px;
border-radius: 25px;
margin-left: 2.4rem;
}
+2
View File
@@ -1 +1,3 @@
export { FormsList, ContractForms } from './FormTabs'; export { FormsList, ContractForms } from './FormTabs';
export { ContractFormsSuccessToast } from './ContractFormsSuccessToast';
export { ContractFormsModal } from './ContractFormsModal';
@@ -0,0 +1,140 @@
import React, { useEffect, useState, useRef, useCallback, memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { useDispatch, useSelector } from 'react-redux';
import { ContractFormsModal } from 'Components/Forms/ContractFormsModal';
import {
addingContractFormSelector,
companyIdSelector,
contractFormAddedSelector,
contractFormNameErrorSelector,
contractFormTemplateErrorSelector,
} from '../selectors';
import { addContractForm, listCompanyContractForms, setContractFormAdded, setContractFormErrors } from '../actions';
const initData = {
name: '',
template: '',
requireSignature: false,
};
interface Props {
isOpen: boolean;
onClose: () => void;
}
const AddContractForms = ({ isOpen, onClose }: Props) => {
const dispatch = useDispatch();
const isAdding = useSelector(addingContractFormSelector, areEqual);
const isAdded = useSelector(contractFormAddedSelector, areEqual);
const companyId = useSelector(companyIdSelector, areEqual);
const nameErrors = useSelector(contractFormNameErrorSelector, areEqual);
const templateErrors = useSelector(contractFormTemplateErrorSelector, areEqual);
const [formData, setFormData] = useState({ ...initData });
const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
const newValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
setFormData((prev) => ({
...prev,
[name]: newValue,
}));
}, []);
const handleTagClick = useCallback(
(tag: string) => {
const textarea = textAreaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newText = formData.template.substring(0, start) + tag + formData.template.substring(end);
setFormData((prev) => ({ ...prev, template: newText }));
setTimeout(() => {
textarea.selectionStart = start + tag.length;
textarea.selectionEnd = start + tag.length;
textarea.focus();
}, 0);
}
},
[formData.template]
);
const onFormSubmit = useCallback(
(e) => {
e.preventDefault();
const errors: any = {};
if (!formData.name.trim()) errors.name = ['Form name is required'];
if (!formData.template.trim()) errors.template = ['Template is required'];
if (Object.keys(errors).length > 0) {
dispatch(setContractFormErrors(errors));
return;
}
if (companyId) {
const payload = {
company_id: companyId,
name: formData.name,
replacement_tags: Array.from(new Set(formData.template.match(/~~~.*?~~~/g) || [])).join(', '),
status: 'active',
template: formData.template,
has_signature: formData.requireSignature,
};
dispatch(addContractForm(payload));
}
},
[formData, companyId, dispatch]
);
useEffect(() => {
if (isAdded) {
setToastMessage('Contract Form Added');
setShowToast(true);
setFormData({ ...initData });
if (companyId) {
dispatch(listCompanyContractForms(companyId));
}
dispatch(setContractFormErrors({}));
setTimeout(() => {
setShowToast(false);
onClose();
dispatch(setContractFormAdded(false));
}, 1500);
}
}, [isAdded]);
useEffect(() => {
if (!isOpen) {
setFormData({ ...initData });
dispatch(setContractFormErrors({}));
}
}, [isOpen, dispatch]);
return (
<ContractFormsModal
isOpen={isOpen}
title="Add Contract Form"
submitText="Add Contract"
formData={formData}
formErrors={{
name: nameErrors,
template: templateErrors,
}}
showToast={showToast}
toastMessage={toastMessage}
onFormSubmit={onFormSubmit}
onClickClose={(e) => {
e.preventDefault();
onClose();
}}
handleChange={handleChange}
textAreaRef={textAreaRef}
handleTagClick={handleTagClick}
submitting={isAdding}
/>
);
};
const AddContractFormsMemo = memo(AddContractForms, areEqual);
export { AddContractFormsMemo as AddContractForms };
@@ -0,0 +1 @@
export { AddContractForms } from './AddContractForms';
@@ -0,0 +1,156 @@
import React, { useEffect, useState, useRef, useCallback, memo } from 'react';
import { areEqual } from 'Utils/equalityChecks';
import { useDispatch, useSelector } from 'react-redux';
import { ContractFormsModal } from 'Components/Forms/ContractFormsModal';
import {
editingContractFormSelector,
contractFormEditedSelector,
contractFormNameErrorSelector,
contractFormTemplateErrorSelector,
companyIdSelector,
} from '../selectors';
import { editContractForm, setContractFormEdited, setContractFormErrors, listCompanyContractForms } from '../actions';
import { FormTemplate } from '../Models/FormsModel/FormModel';
interface Props {
isOpen: boolean;
onClose: () => void;
initData: FormTemplate;
}
const EditContractForms = ({ isOpen, onClose, initData }: Props) => {
const dispatch = useDispatch();
const isEditing = useSelector(editingContractFormSelector, areEqual);
const isEdited = useSelector(contractFormEditedSelector, areEqual);
const companyId = useSelector(companyIdSelector, areEqual);
const nameErrors = useSelector(contractFormNameErrorSelector, areEqual);
const templateErrors = useSelector(contractFormTemplateErrorSelector, areEqual);
const [formData, setFormData] = useState({
name: initData?.name ?? '',
template: initData?.template ?? '',
requireSignature: initData?.has_signature ?? false,
});
const [toastMessage, setToastMessage] = useState('');
const [showToast, setShowToast] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (initData) {
setFormData({
name: initData.name,
template: initData.template,
requireSignature: initData.has_signature,
});
}
}, [initData]);
useEffect(() => {
if (isEdited) {
setToastMessage('Contract Form Updated');
setShowToast(true);
if (companyId) {
dispatch(listCompanyContractForms(companyId));
}
dispatch(setContractFormErrors({}));
setTimeout(() => {
setShowToast(false);
onClose();
dispatch(setContractFormEdited(false));
}, 1500);
}
// eslint-disable-next-line
}, [isEdited]);
useEffect(() => {
if (!isOpen) {
setFormData({
name: '',
template: '',
requireSignature: false,
});
dispatch(setContractFormErrors({}));
}
}, [isOpen, dispatch, initData]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
const newValue = type === 'checkbox' ? (e.target as HTMLInputElement).checked : value;
setFormData((prev) => ({
...prev,
[name]: newValue,
}));
}, []);
const handleTagClick = useCallback(
(tag: string) => {
const textarea = textAreaRef.current;
if (textarea) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newText = formData.template.substring(0, start) + tag + formData.template.substring(end);
setFormData((prev) => ({ ...prev, template: newText }));
setTimeout(() => {
textarea.selectionStart = start + tag.length;
textarea.selectionEnd = start + tag.length;
textarea.focus();
}, 0);
}
},
[formData.template]
);
const onFormSubmit = useCallback(
(e) => {
e.preventDefault();
// Client-side validation
const errors: any = {};
if (!formData.name.trim()) errors.name = ['Form name is required'];
if (!formData.template.trim()) errors.template = ['Template is required'];
if (Object.keys(errors).length > 0) {
dispatch(setContractFormErrors(errors));
return;
}
if (companyId) {
// Prepare API payload
const payload = {
name: formData.name,
company_id: companyId,
replacement_tags: Array.from(new Set(formData.template.match(/~~~.*?~~~/g) || [])).join(', '),
status: 'active',
template: formData.template,
has_signature: formData.requireSignature,
};
dispatch(editContractForm(payload, companyId));
}
},
[formData, companyId, dispatch]
);
return (
<ContractFormsModal
isOpen={isOpen}
title="Edit Contract Form"
submitText="Edit Contract"
formData={formData}
formErrors={{
name: nameErrors,
template: templateErrors,
}}
showToast={showToast}
toastMessage={toastMessage}
onFormSubmit={onFormSubmit}
onClickClose={(e) => {
e.preventDefault();
onClose();
}}
handleChange={handleChange}
textAreaRef={textAreaRef}
handleTagClick={handleTagClick}
submitting={isEditing}
/>
);
};
const EditContractFormsMemo = memo(EditContractForms, areEqual);
export { EditContractFormsMemo as EditContractForms };
@@ -0,0 +1 @@
export { EditContractForms } from './EditContractForms';
@@ -5,11 +5,14 @@ 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';
import { DeleteFormModal, DeleteToast } from 'Components/Forms/FormTabs'; import { DeleteFormModal, DeleteToast } from 'Components/Forms/FormTabs';
import { AddContractForms } from 'Containers/Forms/AddContractForms';
import { FormTemplate } from 'Containers/Forms/Models';
import { EditContractForms } from 'Containers/Forms/EditContractForms';
import { listCompanyContractForms, deleteContractForm, setDeletedFormId } from '../../actions'; import { listCompanyContractForms, deleteContractForm, setDeletedFormId } from '../../actions';
const ContractFormsContainer = () => { const ContractFormsContainer = () => {
@@ -17,13 +20,21 @@ 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 [showAddFormsModal, setShowAddFormsModal] = useState(false);
const [editModal, setEditModal] = useState<{
isOpen: boolean;
form: FormTemplate | null;
}>({
isOpen: false,
form: null,
});
const getContractForms = useCallback(() => { const getContractForms = useCallback(() => {
if (companyId) { if (companyId) {
@@ -49,11 +60,33 @@ const ContractFormsContainer = () => {
[dispatch] [dispatch]
); );
const closeToast = useCallback((e: any) => { const handleOpenEdit = useCallback((form: FormTemplate) => {
e.preventDefault(); setEditModal({
isOpen: true,
form,
});
}, []);
const closeEditModal = useCallback(() => {
setEditModal({
isOpen: false,
form: null,
});
}, []);
const closeToast = useCallback(() => {
setShowDeletedToast(false); setShowDeletedToast(false);
}, []); }, []);
const openAddForms = useCallback((e: any) => {
e.preventDefault();
setShowAddFormsModal(true);
}, []);
const closeAddForms = useCallback(() => {
setShowAddFormsModal(false);
}, []);
useEffect(() => { useEffect(() => {
if (deletedId) { if (deletedId) {
dispatch(listCompanyContractForms(companyId)); dispatch(listCompanyContractForms(companyId));
@@ -75,13 +108,15 @@ const ContractFormsContainer = () => {
<ContractForms <ContractForms
forms={contractForms} forms={contractForms}
fetching={fetching} fetching={fetching}
onAdd={() => {}} onAdd={openAddForms}
onDelete={deleteButtonClick} onDelete={deleteButtonClick}
onClickRow={() => {}} onClickRow={handleOpenEdit}
// deleting={deleting}
// deletedId={deletedId}
/> />
<AddContractForms isOpen={showAddFormsModal} onClose={closeAddForms} />
<EditContractForms isOpen={editModal.isOpen} initData={editModal.form as FormTemplate} onClose={closeEditModal} />
<DeleteFormModal <DeleteFormModal
id={deleteModal.id as number} id={deleteModal.id as number}
isOpen={deleteModal.isOpen} isOpen={deleteModal.isOpen}
@@ -16,3 +16,12 @@ export type FormTemplate = {
export type FormTemplateResponse = { export type FormTemplateResponse = {
data: FormTemplate[]; data: FormTemplate[];
}; };
export type FormRequestBody = {
company_id: number;
name: string;
replacement_tags: string;
status: string;
template: string;
has_signature: boolean;
};
@@ -1 +1 @@
export type { FormTemplate, FormTemplateResponse } from './FormModel'; export type { FormTemplate, FormTemplateResponse, FormRequestBody } from './FormModel';
+1 -1
View File
@@ -1 +1 @@
export type { FormTemplate, FormTemplateResponse } from './FormsModel'; export type { FormTemplate, FormTemplateResponse, FormRequestBody } from './FormsModel';
+70 -4
View File
@@ -1,17 +1,27 @@
import { handleApiRequest } from 'Utils/handleApiRequest'; import { handleApiRequest } from 'Utils/handleApiRequest';
import { Api } from 'Utils/api'; import { Api } from 'Utils/api';
import { FormTemplateResponse } from 'Containers/Forms/Models'; import { FormRequestBody, FormTemplateResponse } from 'Containers/Forms/Models';
export const CONTRACT_FORMS = 'CONTRACT_FORMS'; export const CONTRACT_FORMS = 'CONTRACT_FORMS';
export const FETCHING_CONTRACT_FORMS = 'FETCHING_CONTRACT_FORMS'; export const FETCHING_CONTRACT_FORMS = 'FETCHING_CONTRACT_FORMS';
export const DELETING_CONTRACT_FORM = 'DELETING_CONTRACT_FORM'; export const DELETING_CONTRACT_FORM = 'DELETING_CONTRACT_FORM';
export const CONTRACT_FORM_DELETED = 'CONTRACT_FORM_DELETED'; export const CONTRACT_FORM_DELETED = 'CONTRACT_FORM_DELETED';
export const ADDING_CONTRACT_FORM = 'ADDING_CONTRACT_FORM';
export const CONTRACT_FORM_ADDED = 'CONTRACT_FORM_ADDED';
export const EDITING_CONTRACT_FORM = 'EDITING_CONTRACT_FORM';
export const CONTRACT_FORM_EDITED = 'CONTRACT_FORM_EDITED';
export const CONTRACT_FORM_ERRORS = 'CONTRACT_FORM_ERRORS';
interface FormsActionTypes { interface FormsActionTypes {
CONTRACT_FORMS: FormTemplateResponse; CONTRACT_FORMS: FormTemplateResponse;
FETCHING_CONTRACT_FORMS: boolean; FETCHING_CONTRACT_FORMS: boolean;
DELETING_CONTRACT_FORM: boolean; DELETING_CONTRACT_FORM: boolean;
CONTRACT_FORM_DELETED: number; CONTRACT_FORM_DELETED: number;
ADDING_CONTRACT_FORM: boolean;
CONTRACT_FORM_ADDED: boolean;
EDITING_CONTRACT_FORM: boolean;
CONTRACT_FORM_EDITED: boolean;
CONTRACT_FORM_ERRORS: any;
} }
interface MessageAction { interface MessageAction {
@@ -36,18 +46,74 @@ export const listCompanyContractForms = (companyId: number) => async (dispatch:
}); });
}; };
export const setDeletedFormId = (value: number | null) => async (dispatch: any) => {
dispatch({ type: CONTRACT_FORM_DELETED, payload: value });
};
// 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({ type: CONTRACT_FORM_DELETED, payload: contractFormId }); dispatch(setDeletedFormId(contractFormId));
// dispatch({ type: CONTRACT_FORM_DELETED, payload: contractFormId });
} catch (error) { } catch (error) {
// handle error if needed // handle error if needed
dispatch({ type: DELETING_CONTRACT_FORM, payload: false }); dispatch({ type: DELETING_CONTRACT_FORM, payload: false });
} }
}; };
export const setDeletedFormId = (value: number | null) => async (dispatch: any) => { export const setContractFormAdded = (value: boolean) => (dispatch: any) => {
dispatch({ type: CONTRACT_FORM_DELETED, payload: value }); dispatch({
type: CONTRACT_FORM_ADDED,
payload: value,
});
};
// 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
);
dispatch(setContractFormAdded(true));
} catch (error) {
dispatch({ type: ADDING_CONTRACT_FORM, payload: false });
dispatch(setContractFormAdded(false));
}
};
export const setContractFormEdited = (value: boolean) => (dispatch: any) => {
dispatch({
type: CONTRACT_FORM_EDITED,
payload: value,
});
};
// 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
);
dispatch(setContractFormEdited(true));
} catch (error) {
dispatch({ type: EDITING_CONTRACT_FORM, payload: false });
dispatch(setContractFormEdited(false));
}
};
export const setContractFormErrors = (errors: any) => (dispatch: any) => {
dispatch({
type: CONTRACT_FORM_ERRORS,
payload: errors,
});
}; };
+39
View File
@@ -4,6 +4,10 @@ import {
SetFormsActionTypes, SetFormsActionTypes,
DELETING_CONTRACT_FORM, DELETING_CONTRACT_FORM,
CONTRACT_FORM_DELETED, CONTRACT_FORM_DELETED,
ADDING_CONTRACT_FORM,
CONTRACT_FORM_ADDED,
EDITING_CONTRACT_FORM,
CONTRACT_FORM_EDITED,
} from './actions'; } from './actions';
const initialState = { const initialState = {
@@ -11,6 +15,11 @@ const initialState = {
fetchingContractForms: false, fetchingContractForms: false,
deletingContractForm: false, deletingContractForm: false,
contractFormDeleted: null, contractFormDeleted: null,
addingContractForm: false,
contractFormAdded: false,
editingContractForm: false,
contractFormEdited: false,
formErrors: {},
}; };
export const formsReducer = (state = initialState, action: SetFormsActionTypes) => { export const formsReducer = (state = initialState, action: SetFormsActionTypes) => {
@@ -39,6 +48,36 @@ export const formsReducer = (state = initialState, action: SetFormsActionTypes)
contractFormDeleted: action.payload, contractFormDeleted: action.payload,
}; };
} }
case ADDING_CONTRACT_FORM: {
return {
...state,
addingContractForm: action.payload,
};
}
case CONTRACT_FORM_ADDED: {
return {
...state,
contractFormAdded: action.payload,
};
}
case EDITING_CONTRACT_FORM: {
return {
...state,
editingContractForm: action.payload,
};
}
case CONTRACT_FORM_EDITED: {
return {
...state,
contractFormEdited: action.payload,
};
}
case 'CONTRACT_FORM_ERRORS': {
return {
...state,
formErrors: action.payload || {},
};
}
default: default:
return state; return state;
} }
+7
View File
@@ -4,3 +4,10 @@ export const fetchingContractFormsSelector = ({ forms }: any) => forms?.fetching
export { firstCompanyIdSelector as companyIdSelector } from '../Projects/selectors'; export { firstCompanyIdSelector as companyIdSelector } from '../Projects/selectors';
export const deletingContractFormSelector = ({ forms }: any) => forms?.deletingContractForm || false; export const deletingContractFormSelector = ({ forms }: any) => forms?.deletingContractForm || false;
export const contractFormDeletedSelector = ({ forms }: any) => forms?.contractFormDeleted || null; export const contractFormDeletedSelector = ({ forms }: any) => forms?.contractFormDeleted || null;
export const addingContractFormSelector = ({ forms }: any) => forms?.addingContractForm || false;
export const contractFormAddedSelector = ({ forms }: any) => forms?.contractFormAdded || false;
export const editingContractFormSelector = ({ forms }: any) => forms?.editingContractForm || false;
export const contractFormEditedSelector = ({ forms }: any) => forms?.contractFormEdited || false;
export const contractFormErrorsSelector = ({ forms }: any) => forms?.formErrors || {};
export const contractFormNameErrorSelector = ({ forms }: any) => forms?.formErrors?.name || [];
export const contractFormTemplateErrorSelector = ({ forms }: any) => forms?.formErrors?.template || [];