Compare commits
11 Commits
fb79e0ef12
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96e074cc2d | |||
| 680a7c3268 | |||
| fa9788a10f | |||
| 6a9db34a6e | |||
| 1d4e242054 | |||
| e9f51d6ea0 | |||
| b7bbcc50c6 | |||
| fda58bf19a | |||
| aa66f262b2 | |||
| 3b46073ee8 | |||
| b4a7a56200 |
+19
-2
@@ -16,6 +16,18 @@ module.exports = merge(common, {
|
||||
filename: 'js/[name].[contenthash].bundle.js',
|
||||
},
|
||||
// Spin up a server for quick development
|
||||
// devServer: {
|
||||
// historyApiFallback: true,
|
||||
// contentBase: path.normalize(paths.build),
|
||||
// index: '/',
|
||||
// open: true,
|
||||
// compress: true,
|
||||
// hot: true,
|
||||
// port: 3000,
|
||||
// host: 'test.manaknightdev.com',
|
||||
// https: true,
|
||||
// noInfo: true, //This turns off information regarding the bundle. Set to false if you need to view the messages
|
||||
// },
|
||||
devServer: {
|
||||
historyApiFallback: true,
|
||||
contentBase: path.normalize(paths.build),
|
||||
@@ -24,9 +36,14 @@ module.exports = merge(common, {
|
||||
compress: true,
|
||||
hot: true,
|
||||
port: 3000,
|
||||
host: 'test.manaknightdev.com',
|
||||
host: '0.0.0.0',
|
||||
https: true,
|
||||
noInfo: true, //This turns off information regarding the bundle. Set to false if you need to view the messages
|
||||
allowedHosts: [
|
||||
'.ngrok-free.app', // allow any ngrok subdomain
|
||||
// 'localhost',
|
||||
'test.manaknightdev.com',
|
||||
],
|
||||
noInfo: true,
|
||||
},
|
||||
|
||||
module: {
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@
|
||||
"prepare-husky": "husky install",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"test": "npx cypress open",
|
||||
"commit": "git add . && git commit -m \"Update Host\" && git push"
|
||||
"commit": "git add . && git commit -m \"Update Host\" && git push",
|
||||
"build": "webpack --config config/webpack.base.js --mode production"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { Project } from 'Containers/Project';
|
||||
import { Account, About } from 'Containers/User';
|
||||
import { ProjectData } from 'Containers/ProjectData';
|
||||
import { RocketDry } from 'Containers/RocketDry';
|
||||
import { Forms } from 'Containers/Forms';
|
||||
|
||||
// route components
|
||||
import { PhotoShareProvider } from 'Context/PhotoShare/PhotoShareProvider';
|
||||
@@ -182,6 +183,13 @@ const PeopleRoute = () => (
|
||||
</DashboardWrapper>
|
||||
);
|
||||
|
||||
// Form route
|
||||
const FormsRoute = () => (
|
||||
<DashboardWrapper>
|
||||
<Forms />
|
||||
</DashboardWrapper>
|
||||
);
|
||||
|
||||
const PhotoViewRoute = () => (
|
||||
<PhotoViewWrapper>
|
||||
<PhotoView />
|
||||
@@ -378,6 +386,8 @@ export const Routes = () => (
|
||||
|
||||
<PrivateRoute exact path="/people" render={PeopleRoute} />
|
||||
|
||||
<PrivateRoute exact path="/form" render={FormsRoute} />
|
||||
|
||||
<PrivateRoute exact path="/user/account" render={AccountRoute} />
|
||||
<PrivateRoute exact path="/user/about" render={AboutRoute} />
|
||||
|
||||
|
||||
@@ -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 { useDebounce } from "Hooks/useDebounce";
|
||||
import { areEqualShallow } from 'Utils/equalityChecks';
|
||||
import { useDebounce } from 'Hooks/useDebounce';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
@@ -38,16 +38,16 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
|
||||
const onSubmitLocal = useDebounce((e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
formRef.current.classList.add("was-validated");
|
||||
formRef.current.classList.add('was-validated');
|
||||
|
||||
let value = "";
|
||||
let value = '';
|
||||
switch (e.target.type) {
|
||||
case "checkbox":
|
||||
case 'checkbox':
|
||||
value = e.target.checked;
|
||||
break;
|
||||
case "text":
|
||||
case "tel":
|
||||
case "email":
|
||||
case 'text':
|
||||
case 'tel':
|
||||
case 'email':
|
||||
value = e.target.value.trim();
|
||||
break;
|
||||
default:
|
||||
@@ -57,7 +57,7 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
|
||||
// This will handle an empty input
|
||||
if (value?.length === 0) {
|
||||
validate({ [e.target.name]: e.target.value, isValid: true });
|
||||
formRef.current.classList.remove("was-validated");
|
||||
formRef.current.classList.remove('was-validated');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ const Form = ({ id, className, noValidate = true, submitButton, onSubmit, childr
|
||||
}}
|
||||
onSubmit={(submitButton && onSubmitLocal) || submitWhenNoSubmitButton}
|
||||
id={id}
|
||||
className={`requires-validation ${className || ""}`}
|
||||
className={`requires-validation ${className || ''}`}
|
||||
noValidate={noValidate}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
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 { ContractFormsToast } from '../ContractFormsToast';
|
||||
|
||||
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;
|
||||
toastType?: 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,
|
||||
toastType = 'success',
|
||||
onFormSubmit,
|
||||
onClickClose,
|
||||
handleChange,
|
||||
textAreaRef,
|
||||
handleTagClick,
|
||||
submitting,
|
||||
}: Props) => (
|
||||
<Modal
|
||||
id="contractsFormModal"
|
||||
classes={classes}
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
leftHeaderIcon="projects"
|
||||
modalHeader
|
||||
modalCloseClick={onClickClose}
|
||||
toast={<ContractFormsToast type={toastType as 'success' | 'error'} 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>
|
||||
);
|
||||
|
||||
ContractFormsModal.defaultProps = {
|
||||
toastType: 'success',
|
||||
};
|
||||
|
||||
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,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 };
|
||||
@@ -0,0 +1,82 @@
|
||||
.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 !important;
|
||||
}
|
||||
|
||||
.toastWarning .toastText {
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.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 { ContractFormsToast } from './ContractFormsToast';
|
||||
@@ -0,0 +1,40 @@
|
||||
import { FormTemplate, FormTemplateResponse } from 'Containers/Forms/Models';
|
||||
import React, { memo } from 'react';
|
||||
import { TabContent } from 'Components/Tabs';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { Spinner } from 'Components/Spinner';
|
||||
import { PurpleButton } from 'Components/Button';
|
||||
|
||||
import { FormsList } from '../FormsList';
|
||||
import classes from './contractForms.module.css';
|
||||
|
||||
interface Props {
|
||||
forms: FormTemplateResponse;
|
||||
fetching: boolean;
|
||||
onClickRow?: (form: FormTemplate) => void;
|
||||
onAdd: (e: any) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
const ContractForms = ({ forms, onClickRow, fetching, onAdd, onDelete }: Props) => (
|
||||
<TabContent key="tab-content-contract-forms" id="contract-forms" className="show active position-relative">
|
||||
<div className={classes.formsContent}>
|
||||
<div className={`d-flex justify-content-start align-items-center ${classes.contentHeader}`}>
|
||||
<h2>Form Templates</h2>
|
||||
<PurpleButton className={classes.addButton} onClick={onAdd}>
|
||||
Add +
|
||||
</PurpleButton>
|
||||
</div>
|
||||
{fetching && <Spinner loading />}
|
||||
{!fetching && <FormsList forms={forms} onClickRow={onClickRow} onDelete={onDelete} onAdd={onAdd} />}
|
||||
</div>
|
||||
</TabContent>
|
||||
);
|
||||
|
||||
ContractForms.defaultProps = {
|
||||
onClickRow: null,
|
||||
};
|
||||
|
||||
const ContractFormsMemo = memo(ContractForms, areEqual);
|
||||
|
||||
export { ContractFormsMemo as ContractForms };
|
||||
@@ -0,0 +1,31 @@
|
||||
.formsContent {
|
||||
min-height: calc(42px + (64px * 15));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: baseline;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.contentHeader {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.contentHeader h2 {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 32px;
|
||||
line-height: 19px;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
background: #ffffff;
|
||||
color: #9a00ff;
|
||||
font-weight: 500;
|
||||
width: 160px;
|
||||
border-radius: 25px;
|
||||
margin-left: 2.4rem;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ContractForms } from './ContractForms';
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { memo } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { Modal } from 'Components/Modal';
|
||||
import { Button } from 'Components/Button';
|
||||
|
||||
import classes from './deleteFormModal.module.css';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
isOpen: boolean;
|
||||
modalCloseClick: (e: any) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
const DeleteFormModal = ({ id, isOpen, modalCloseClick, onDelete }: Props) => (
|
||||
<div>
|
||||
<Modal
|
||||
id={id && id.toString()}
|
||||
leftHeaderIcon="projects"
|
||||
classes={classes}
|
||||
title="Delete Contract Form?"
|
||||
isOpen={isOpen}
|
||||
modalHeader
|
||||
footerButtons={
|
||||
<Button
|
||||
className={`${classes.delete}`}
|
||||
id={id && id.toString()}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete(id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}
|
||||
modalFooter
|
||||
closeButtonText="Cancel"
|
||||
dataBsBackdrop="static"
|
||||
dataBsKeyboard="false"
|
||||
modalCloseClick={modalCloseClick}
|
||||
>
|
||||
<div className={classes.deleteModalCopy}>
|
||||
<p>Are you sure you want to delete this form?</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DeleteFormModalMemo = memo(DeleteFormModal, areEqual);
|
||||
|
||||
export { DeleteFormModalMemo as DeleteFormModal };
|
||||
@@ -0,0 +1,86 @@
|
||||
.modalContent {
|
||||
padding: 1.3em 1.5em;
|
||||
box-shadow: 0px 24px 44px rgba(119, 113, 133, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.modalDialog {
|
||||
max-width: 648px;
|
||||
}
|
||||
.modalHeader {
|
||||
padding: 0 0 0.5em 0;
|
||||
border-color: #e8e7ed;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
text-transform: capitalize;
|
||||
line-height: 24px;
|
||||
color: #5b476b;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
.modalBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 1.3rem 0 1.1rem 0;
|
||||
}
|
||||
.modalBody {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #000000;
|
||||
}
|
||||
.deleteModalHeader {
|
||||
width: 100%;
|
||||
max-width: 343px;
|
||||
border-bottom: 1px solid #d2cfda;
|
||||
}
|
||||
.deleteModalCopy {
|
||||
width: 350px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
padding: 1rem 0 0 0;
|
||||
border: none;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
.modalFooter button {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #5b476b;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #5b476b;
|
||||
border-radius: 5px;
|
||||
width: 151px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.modalFooter .closeButtonClass:hover,
|
||||
.modalFooter .closeButtonClass:focus {
|
||||
background-color: #f3f3f6;
|
||||
border-color: #d2cfda;
|
||||
color: #777185;
|
||||
}
|
||||
.modalFooter .delete {
|
||||
border-color: #e82828 !important;
|
||||
color: #e82828 !important;
|
||||
}
|
||||
.modalFooter .delete:hover,
|
||||
.modalFooter .delete:focus {
|
||||
background-color: #e82828 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DeleteFormModal } from './DeleteFormModal';
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { CheckedMarkSvg } from 'Components/Icons/CheckedMark';
|
||||
import { Button } from 'Components/Button';
|
||||
|
||||
import classes from './deleteToast.module.css';
|
||||
|
||||
export interface Props {
|
||||
isDisplayed: boolean;
|
||||
message: string;
|
||||
closeToast: (e: any) => void;
|
||||
type?: 'success' | 'error';
|
||||
}
|
||||
|
||||
const DeleteToast = ({ isDisplayed = false, message, closeToast, type = 'success' }: Props) => {
|
||||
const getToastClass = () => (type === 'success' ? classes.toastSuccess : classes.toastWarning);
|
||||
|
||||
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);
|
||||
|
||||
export { DeleteToastMemo as DeleteToast };
|
||||
@@ -0,0 +1,77 @@
|
||||
.toastBase {
|
||||
height: 40px !important;
|
||||
width: calc(100% - (315px + 1.5rem)) !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;
|
||||
left: calc(290px + 0.75rem);
|
||||
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 { DeleteToast } from './DeleteToast';
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { memo } from 'react';
|
||||
import { FormTemplateResponse, FormTemplate } from 'Containers/Forms/Models';
|
||||
import { Table, TableBody, TableColumn, TableHeader, TableRow, Th } from 'Components/Table';
|
||||
import { Icon } from 'Components/Icons';
|
||||
|
||||
import { formatDate } from 'Utils/helpers';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { NoFormsTable } from '../NoFormsTable';
|
||||
|
||||
import classes from './formsList.module.css';
|
||||
|
||||
interface Props {
|
||||
forms: FormTemplateResponse;
|
||||
onDelete: (id: number) => void;
|
||||
onAdd: (e: any) => void;
|
||||
onClickRow?: (form: FormTemplate) => void;
|
||||
}
|
||||
|
||||
const FormsList = ({ forms, onClickRow, onDelete, onAdd }: Props) => {
|
||||
const onRowClick = (e: any, form: any) => {
|
||||
e.preventDefault();
|
||||
onClickRow(form);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{forms?.data?.length > 0 ? (
|
||||
<Table className={`table ${classes.formListWrapper}`}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<Th>Name</Th>
|
||||
<Th>Date Created</Th>
|
||||
<Th />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{forms.data.map((form: FormTemplate) => (
|
||||
<TableRow key={form.id}>
|
||||
<TableColumn dataId={form.id} tdOnClick={(e) => onRowClick(e, form)}>
|
||||
<p>{form.name}</p>
|
||||
</TableColumn>
|
||||
<TableColumn dataId={form.id} tdOnClick={(e) => onRowClick(e, form)} className={classes.columnContent}>
|
||||
<p className={classes.numberAndDate}>{formatDate(form.created_at, 'PP')}</p>
|
||||
</TableColumn>
|
||||
<TableColumn>
|
||||
<button
|
||||
className={classes.deleteBtn}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(form.id);
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" />
|
||||
</button>
|
||||
</TableColumn>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<NoFormsTable onAdd={onAdd} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FormsList.defaultProps = {
|
||||
onClickRow: null,
|
||||
};
|
||||
|
||||
const FormsListMemo = memo(FormsList, areEqual);
|
||||
|
||||
export { FormsListMemo as FormsList };
|
||||
@@ -0,0 +1,52 @@
|
||||
.formListContainer {
|
||||
min-height: 700px;
|
||||
}
|
||||
.formListWrapper {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: #5b476b;
|
||||
border-color: #e8e7ed;
|
||||
background-color: #ffffff;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.formListWrapper thead th:first-child,
|
||||
.formListWrapper tbody td:first-child {
|
||||
padding-left: 72px;
|
||||
}
|
||||
.formListWrapper thead th:last-child,
|
||||
.formListWrapper tbody td:last-child {
|
||||
padding-right: 72px;
|
||||
}
|
||||
.formListWrapper thead th {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: #777185;
|
||||
padding-bottom: 1.2em;
|
||||
border-bottom-color: #e8e7ed !important;
|
||||
}
|
||||
.formListWrapper tbody tr:hover {
|
||||
background-color: #f4e5ff;
|
||||
transition: 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
.formListWrapper tbody td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.formListWrapper p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.deleteBtn {
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
aspect-ratio: 1;
|
||||
background-color: white;
|
||||
border: none;
|
||||
}
|
||||
.deleteBtn:hover {
|
||||
background-color: rgba(255, 0, 0, 0.399);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { FormsList } from './FormsList';
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { memo } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { Table, TableHeader, TableRow, Th } from 'Components/Table';
|
||||
import { PurpleButton } from 'Components/Button';
|
||||
import { Icon } from 'Components/Icons';
|
||||
|
||||
import classes from './noFormsTable.module.css';
|
||||
|
||||
const NoFormsTable = ({ onAdd }: { onAdd: (e) => void }) => (
|
||||
<div>
|
||||
<Table className={`table ${classes.formListWrapper}`}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<Th>Address</Th>
|
||||
<Th>Date Created</Th>
|
||||
<Th />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
<div className={`d-flex justify-content-center align-items-center flex-column w-100 ${classes.noFormsContent}`}>
|
||||
<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>
|
||||
);
|
||||
|
||||
const NoFormsTableMemo = memo(NoFormsTable, areEqual);
|
||||
|
||||
export { NoFormsTableMemo as NoFormsTable };
|
||||
@@ -0,0 +1 @@
|
||||
export { NoFormsTable } from './NoFormsTable';
|
||||
@@ -0,0 +1,58 @@
|
||||
.noFormsContent {
|
||||
padding-top: 240px;
|
||||
padding-bottom: 310px;
|
||||
}
|
||||
|
||||
.formListWrapper {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: #5b476b;
|
||||
border-color: #e8e7ed;
|
||||
background-color: #ffffff;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.formListWrapper thead th:first-child,
|
||||
.formListWrapper tbody td:first-child {
|
||||
padding-left: 72px;
|
||||
}
|
||||
.formListWrapper thead th:last-child,
|
||||
.formListWrapper tbody td:last-child {
|
||||
padding-right: 72px;
|
||||
}
|
||||
.formListWrapper thead th {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: #777185;
|
||||
padding-bottom: 1.2em;
|
||||
border-bottom-color: #e8e7ed !important;
|
||||
}
|
||||
.formListWrapper tbody tr:hover {
|
||||
background-color: #f4e5ff;
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
.formListWrapper tbody td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.formListWrapper p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.noFormsText {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
color: #b3abc6;
|
||||
}
|
||||
.addButton {
|
||||
background: #9a00ff;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
width: 160px;
|
||||
border-radius: 25px;
|
||||
margin-left: 2.4rem;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { ContractForms } from './ContractForms';
|
||||
export { FormsList } from './FormsList';
|
||||
export { DeleteFormModal } from './DeleteFormModal';
|
||||
export { DeleteToast } from './DeleteToast';
|
||||
@@ -0,0 +1,3 @@
|
||||
export { FormsList, ContractForms } from './FormTabs';
|
||||
export { ContractFormsToast } from './ContractFormsToast';
|
||||
export { ContractFormsModal } from './ContractFormsModal';
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Icon } from 'Components/Icons';
|
||||
import React, { memo, ReactNode, useState } from 'react';
|
||||
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { width } from 'Utils/screen';
|
||||
import { Tab } from '../Tab';
|
||||
import classes from './forms.tabs.module.css';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const createTabs = (activeTab: string, onTabClick: (e: any) => void) => (
|
||||
<>
|
||||
<Tab
|
||||
key="contract-forms-tab"
|
||||
id="contract-forms-tab"
|
||||
className={`${classes.flexCenter} ${classes.button} ${
|
||||
activeTab === 'contract-forms-tab' ? `active ${classes['active-Tab']}` : ''
|
||||
}`}
|
||||
target="contract-forms"
|
||||
onClick={onTabClick}
|
||||
>
|
||||
<>
|
||||
<Icon type="projects" className={classes.icon} />
|
||||
<span>Contract Forms</span>
|
||||
</>
|
||||
</Tab>
|
||||
</>
|
||||
);
|
||||
|
||||
/*
|
||||
In order to override bootstraps active class on tabs, there is a click event onTabClick, which will get the name of the tab that was clicked
|
||||
and then trigger a re-render. Note in the createTabs method above, where the active class is added or not, based on which tab was clicked.
|
||||
*/
|
||||
const FormTabs = ({ id = 'tabs', className, children }: Props) => {
|
||||
// We want to set the initial active tab to the first tab in the incoming tabList
|
||||
const [activeTab, setActiveTab] = useState('contract-forms-tab');
|
||||
|
||||
const onTabClick = (e: any) => {
|
||||
// Occasionally, e.currentTarget is undefined. Set the current activeTab if we run into this bug
|
||||
setActiveTab(e?.currentTarget?.id || activeTab);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className={classes.formTabWrapper}>
|
||||
<div className={classes.tabsContainer}>
|
||||
<ul
|
||||
className={`nav nav-tabs ${width < 576 ? 'flex-sm-column' : 'width'} ${classes.tabs} ${
|
||||
className || ''
|
||||
}`}
|
||||
id={id}
|
||||
role="tablist"
|
||||
>
|
||||
{createTabs(activeTab, onTabClick)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tab-content w-100 h-100 d-inline-block" id="formTabContent" style={{ height: 'auto' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FormTabs.defaultProps = {
|
||||
id: undefined,
|
||||
className: undefined,
|
||||
children: undefined,
|
||||
};
|
||||
const FormTabsMemo = memo(FormTabs, areEqual);
|
||||
export { FormTabsMemo as FormTabs };
|
||||
@@ -0,0 +1,87 @@
|
||||
.formTabWrapper {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.tabsContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.tabs {
|
||||
border: none;
|
||||
}
|
||||
.active-Tab,
|
||||
.active-Tab:hover {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
border-radius: 0 0 16px 16px;
|
||||
background: linear-gradient(316.14deg, #6d00e6 1.84%, #9a00ff 96.25%) !important;
|
||||
}
|
||||
.active-Tab::after {
|
||||
content: attr(data-text);
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.icon path {
|
||||
fill: #9a00ff;
|
||||
}
|
||||
.active-Tab .icon path {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.icon-phone {
|
||||
padding-bottom: 0.265em;
|
||||
}
|
||||
|
||||
.flexTop {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
.flexCenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filterButtonContainer {
|
||||
margin-top: 12px;
|
||||
margin-right: 24px;
|
||||
width: 55px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
font-family: IBM Plex Sans;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: capitalize;
|
||||
color: #9a00ff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filterButton:hover,
|
||||
.filterButton:focus {
|
||||
color: #9a00ff;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { FormTabs } from './FormTabs';
|
||||
@@ -4,3 +4,4 @@ export { MobileProjectsTabs } from './ProjectsTabs';
|
||||
export { TabContent } from './TabContent';
|
||||
export { ProjectTabMenu } from './ProjectTabMenu';
|
||||
export { ProjectsTabMenu } from './ProjectsTabMenu';
|
||||
export { FormTabs } from './FormTabs';
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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 [toastType, setToastType] = useState<'success' | 'error'>('success');
|
||||
const [wasAdding, setWasAdding] = 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) {
|
||||
setToastType('success');
|
||||
setToastMessage('Contract Form Added');
|
||||
setShowToast(true);
|
||||
setFormData({ ...initData });
|
||||
if (companyId) {
|
||||
dispatch(listCompanyContractForms(companyId));
|
||||
}
|
||||
dispatch(setContractFormErrors({}));
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
onClose();
|
||||
dispatch(setContractFormAdded(false));
|
||||
}, 1500);
|
||||
}
|
||||
}, [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) {
|
||||
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}
|
||||
toastType={toastType}
|
||||
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,174 @@
|
||||
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 [toastType, setToastType] = useState<'success' | 'error'>('success');
|
||||
const [wasEditing, setWasEditing] = 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) {
|
||||
setToastType('success');
|
||||
setToastMessage('Contract Form Updated');
|
||||
setShowToast(true);
|
||||
if (companyId) {
|
||||
dispatch(listCompanyContractForms(companyId));
|
||||
}
|
||||
dispatch(setContractFormErrors({}));
|
||||
setTimeout(() => {
|
||||
setShowToast(false);
|
||||
onClose();
|
||||
dispatch(setContractFormEdited(false));
|
||||
}, 1500);
|
||||
}
|
||||
}, [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) {
|
||||
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));
|
||||
}
|
||||
},
|
||||
[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}
|
||||
toastType={toastType}
|
||||
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';
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, { memo, useEffect, useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import {
|
||||
contractFormsSelector,
|
||||
fetchingContractFormsSelector,
|
||||
companyIdSelector,
|
||||
deletingContractFormSelector,
|
||||
contractFormDeletedSelector,
|
||||
} from 'Containers/Forms/selectors';
|
||||
import { ContractForms } from 'Components/Forms';
|
||||
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';
|
||||
|
||||
const ContractFormsContainer = () => {
|
||||
const dispatch = useDispatch();
|
||||
const contractForms = useSelector(contractFormsSelector, areEqual);
|
||||
const fetching = useSelector(fetchingContractFormsSelector, areEqual);
|
||||
const companyId = useSelector(companyIdSelector, 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;
|
||||
form: FormTemplate | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
form: null,
|
||||
});
|
||||
|
||||
const getContractForms = useCallback(() => {
|
||||
if (companyId) {
|
||||
dispatch(listCompanyContractForms(companyId));
|
||||
}
|
||||
}, [dispatch, companyId]);
|
||||
|
||||
const deleteButtonClick = useCallback((id: number) => {
|
||||
setDeleteModal({
|
||||
isOpen: true,
|
||||
id,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(deleteContractForm(id));
|
||||
setDeleteModal({
|
||||
isOpen: false,
|
||||
id: null,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleOpenEdit = useCallback((form: FormTemplate) => {
|
||||
setEditModal({
|
||||
isOpen: true,
|
||||
form,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeEditModal = useCallback(() => {
|
||||
setEditModal({
|
||||
isOpen: false,
|
||||
form: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeToast = useCallback(() => {
|
||||
setShowDeletedToast(false);
|
||||
}, []);
|
||||
|
||||
const openAddForms = useCallback((e: any) => {
|
||||
e.preventDefault();
|
||||
setShowAddFormsModal(true);
|
||||
}, []);
|
||||
|
||||
const closeAddForms = useCallback(() => {
|
||||
setShowAddFormsModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (deletedId) {
|
||||
// Success case
|
||||
setDeleteToastType('success');
|
||||
setDeleteToastMessage('Form Deleted');
|
||||
setShowDeletedToast(true);
|
||||
dispatch(listCompanyContractForms(companyId));
|
||||
|
||||
setTimeout(() => {
|
||||
setShowDeletedToast(false);
|
||||
dispatch(setDeletedFormId(null));
|
||||
}, 1500);
|
||||
}
|
||||
}, [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();
|
||||
}, [getContractForms]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractForms
|
||||
forms={contractForms}
|
||||
fetching={fetching}
|
||||
onAdd={openAddForms}
|
||||
onDelete={deleteButtonClick}
|
||||
onClickRow={handleOpenEdit}
|
||||
/>
|
||||
|
||||
<AddContractForms isOpen={showAddFormsModal} onClose={closeAddForms} />
|
||||
|
||||
<EditContractForms isOpen={editModal.isOpen} initData={editModal.form as FormTemplate} onClose={closeEditModal} />
|
||||
|
||||
<DeleteFormModal
|
||||
id={deleteModal.id as number}
|
||||
isOpen={deleteModal.isOpen}
|
||||
modalCloseClick={() =>
|
||||
setDeleteModal({
|
||||
isOpen: false,
|
||||
id: null,
|
||||
})
|
||||
}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
<DeleteToast
|
||||
isDisplayed={showDeletedToast}
|
||||
closeToast={closeToast}
|
||||
message={deleteToastMessage}
|
||||
type={deleteToastType}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ContractFormsContainerMemo = memo(ContractFormsContainer, areEqual);
|
||||
|
||||
export { ContractFormsContainerMemo as ContractFormsContainer };
|
||||
@@ -0,0 +1 @@
|
||||
export { ContractFormsContainer as ContractForms } from './ContractForms';
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, { memo } from 'react';
|
||||
import { FormTabs } from 'Components/Tabs';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { ContractForms } from '../ContractForms';
|
||||
|
||||
const FormTabsContainer = () => (
|
||||
<>
|
||||
<FormTabs id="form-tabs">
|
||||
<ContractForms />
|
||||
</FormTabs>
|
||||
</>
|
||||
);
|
||||
|
||||
const FormTabsContainerMemo = memo(FormTabsContainer, areEqual);
|
||||
|
||||
export { FormTabsContainerMemo as FormTabs };
|
||||
@@ -0,0 +1 @@
|
||||
export { FormTabs } from './FormTabs';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { FormTabs } from './FormTabs';
|
||||
export { ContractForms } from './ContractForms';
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { memo } from 'react';
|
||||
import { areEqual } from 'Utils/equalityChecks';
|
||||
import { FormTabs } from './FormTabs';
|
||||
|
||||
const FormsContainer = () => <FormTabs />;
|
||||
|
||||
const FormsContainerMemo = memo(FormsContainer, areEqual);
|
||||
|
||||
export { FormsContainerMemo as FormsContainer };
|
||||
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable */
|
||||
|
||||
export type FormTemplate = {
|
||||
company_id: number;
|
||||
name: string;
|
||||
replacement_tags: string;
|
||||
template: string;
|
||||
has_signature: boolean;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type FormTemplateResponse = {
|
||||
data: FormTemplate[];
|
||||
};
|
||||
|
||||
export type FormRequestBody = {
|
||||
company_id: number;
|
||||
name: string;
|
||||
replacement_tags: string;
|
||||
status: string;
|
||||
template: string;
|
||||
has_signature: boolean;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type { FormTemplate, FormTemplateResponse, FormRequestBody } from './FormModel';
|
||||
@@ -0,0 +1 @@
|
||||
export type { FormTemplate, FormTemplateResponse, FormRequestBody } from './FormsModel';
|
||||
@@ -0,0 +1,126 @@
|
||||
/*eslint-disable */
|
||||
import { handleApiRequest } from 'Utils/handleApiRequest';
|
||||
import { Api } from 'Utils/api';
|
||||
import { FormRequestBody, FormTemplateResponse } from 'Containers/Forms/Models';
|
||||
|
||||
export const CONTRACT_FORMS = 'CONTRACT_FORMS';
|
||||
export const FETCHING_CONTRACT_FORMS = 'FETCHING_CONTRACT_FORMS';
|
||||
export const DELETING_CONTRACT_FORM = 'DELETING_CONTRACT_FORM';
|
||||
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 {
|
||||
CONTRACT_FORMS: FormTemplateResponse;
|
||||
FETCHING_CONTRACT_FORMS: boolean;
|
||||
DELETING_CONTRACT_FORM: boolean;
|
||||
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 {
|
||||
type: keyof FormsActionTypes;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export type SetFormsActionTypes = MessageAction;
|
||||
|
||||
// Thunk to fetch contract forms for a company
|
||||
export const listCompanyContractForms = (companyId: number) => async (dispatch: any) => {
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
Api.get(`companies/${companyId}/contract-forms`),
|
||||
'',
|
||||
FETCHING_CONTRACT_FORMS
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: CONTRACT_FORMS,
|
||||
payload: response,
|
||||
});
|
||||
};
|
||||
|
||||
export const setDeletedFormId = (value: number | null) => async (dispatch: any) => {
|
||||
dispatch({ type: CONTRACT_FORM_DELETED, payload: value });
|
||||
};
|
||||
|
||||
// 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));
|
||||
} catch (error) {
|
||||
dispatch({ type: DELETING_CONTRACT_FORM, payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
export const setContractFormAdded = (value: boolean) => (dispatch: any) => {
|
||||
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 });
|
||||
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
Api.post('/contract-forms', formData),
|
||||
'CONTRACT_FORM_ERRORS',
|
||||
ADDING_CONTRACT_FORM
|
||||
);
|
||||
|
||||
if (response) {
|
||||
// API call succeeded
|
||||
dispatch(setContractFormAdded(true));
|
||||
} else {
|
||||
// API call failed
|
||||
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) => async (dispatch: any) => {
|
||||
dispatch({ type: EDITING_CONTRACT_FORM, payload: true });
|
||||
|
||||
const response = await handleApiRequest(
|
||||
dispatch,
|
||||
Api.put('/contract-forms', formData),
|
||||
'CONTRACT_FORM_ERRORS',
|
||||
EDITING_CONTRACT_FORM
|
||||
);
|
||||
|
||||
if (response) {
|
||||
// API call succeeded
|
||||
dispatch(setContractFormEdited(true));
|
||||
} else {
|
||||
// API call failed
|
||||
dispatch(setContractFormEdited(false));
|
||||
}
|
||||
};
|
||||
|
||||
export const setContractFormErrors = (errors: any) => (dispatch: any) => {
|
||||
dispatch({
|
||||
type: CONTRACT_FORM_ERRORS,
|
||||
payload: errors,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { FormsContainer as Forms } from './Forms';
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
CONTRACT_FORMS,
|
||||
FETCHING_CONTRACT_FORMS,
|
||||
SetFormsActionTypes,
|
||||
DELETING_CONTRACT_FORM,
|
||||
CONTRACT_FORM_DELETED,
|
||||
ADDING_CONTRACT_FORM,
|
||||
CONTRACT_FORM_ADDED,
|
||||
EDITING_CONTRACT_FORM,
|
||||
CONTRACT_FORM_EDITED,
|
||||
} from './actions';
|
||||
|
||||
const initialState = {
|
||||
data: [],
|
||||
fetchingContractForms: false,
|
||||
deletingContractForm: false,
|
||||
contractFormDeleted: null,
|
||||
addingContractForm: false,
|
||||
contractFormAdded: false,
|
||||
editingContractForm: false,
|
||||
contractFormEdited: false,
|
||||
formErrors: {},
|
||||
};
|
||||
|
||||
export const formsReducer = (state = initialState, action: SetFormsActionTypes) => {
|
||||
switch (action.type) {
|
||||
case CONTRACT_FORMS: {
|
||||
return {
|
||||
...state,
|
||||
data: action.payload,
|
||||
};
|
||||
}
|
||||
case FETCHING_CONTRACT_FORMS: {
|
||||
return {
|
||||
...state,
|
||||
fetchingContractForms: action.payload,
|
||||
};
|
||||
}
|
||||
case DELETING_CONTRACT_FORM: {
|
||||
return {
|
||||
...state,
|
||||
deletingContractForm: action.payload,
|
||||
};
|
||||
}
|
||||
case CONTRACT_FORM_DELETED: {
|
||||
return {
|
||||
...state,
|
||||
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:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
// Contract Forms selectors
|
||||
export const contractFormsSelector = ({ forms }: any) => forms?.data || [];
|
||||
export const fetchingContractFormsSelector = ({ forms }: any) => forms?.fetchingContractForms || false;
|
||||
export { firstCompanyIdSelector as companyIdSelector } from '../Projects/selectors';
|
||||
export const deletingContractFormSelector = ({ forms }: any) => forms?.deletingContractForm || false;
|
||||
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 || [];
|
||||
@@ -11,4 +11,10 @@ export const navItems = [
|
||||
path: '/people',
|
||||
icon: 'people',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Forms',
|
||||
path: '/form',
|
||||
icon: 'projects',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -55,6 +55,7 @@ import { lossDataReducer } from 'Containers/ProjectData/LossData/reducer';
|
||||
import { propertyDataReducer } from 'Containers/ProjectData/PropertyData/reducer';
|
||||
import { reportsReducer } from 'Containers/ReportsAndDocuments';
|
||||
import { rocketDryReducer } from 'Containers/RocketDry/reducer';
|
||||
import { formsReducer } from 'Containers/Forms/reducer';
|
||||
|
||||
// Combine them
|
||||
export default combineReducers({
|
||||
@@ -113,4 +114,5 @@ export default combineReducers({
|
||||
propertyData: propertyDataReducer,
|
||||
reports: reportsReducer,
|
||||
rocketDry: rocketDryReducer,
|
||||
forms: formsReducer,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user