diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 2d58e00..f6c5c1c 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -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 = () => ( ); +// Form route +const FormsRoute = () => ( + + + +); + const PhotoViewRoute = () => ( @@ -378,6 +386,8 @@ export const Routes = () => ( + + diff --git a/src/shared/components/Forms/FormTabs/ContractForms/ContractForms.tsx b/src/shared/components/Forms/FormTabs/ContractForms/ContractForms.tsx new file mode 100644 index 0000000..ba0ec10 --- /dev/null +++ b/src/shared/components/Forms/FormTabs/ContractForms/ContractForms.tsx @@ -0,0 +1,40 @@ +import { 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?: (e: any) => void; + onAdd: () => void; + onDelete: (id: number) => void; +} + +const ContractForms = ({ forms, onClickRow, fetching, onAdd, onDelete }: Props) => ( + + + + Form Templates + + Add + + + + {fetching && } + {!fetching && } + + +); + +ContractForms.defaultProps = { + onClickRow: null, +}; + +const ContractFormsMemo = memo(ContractForms, areEqual); + +export { ContractFormsMemo as ContractForms }; diff --git a/src/shared/components/Forms/FormTabs/ContractForms/contractForms.module.css b/src/shared/components/Forms/FormTabs/ContractForms/contractForms.module.css new file mode 100644 index 0000000..8881271 --- /dev/null +++ b/src/shared/components/Forms/FormTabs/ContractForms/contractForms.module.css @@ -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; +} diff --git a/src/shared/components/Forms/FormTabs/ContractForms/index.ts b/src/shared/components/Forms/FormTabs/ContractForms/index.ts new file mode 100644 index 0000000..0a0c82a --- /dev/null +++ b/src/shared/components/Forms/FormTabs/ContractForms/index.ts @@ -0,0 +1 @@ +export { ContractForms } from './ContractForms'; diff --git a/src/shared/components/Forms/FormTabs/FormsList/FormsList.tsx b/src/shared/components/Forms/FormTabs/FormsList/FormsList.tsx new file mode 100644 index 0000000..9ce4502 --- /dev/null +++ b/src/shared/components/Forms/FormTabs/FormsList/FormsList.tsx @@ -0,0 +1,65 @@ +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; + onClickRow?: (e: any) => void; +} + +const FormsList = ({ forms, onClickRow, onDelete }: Props) => ( + <> + {forms?.data?.length > 0 ? ( + + + + Name + Date Created + + + + + {forms.data.map(({ id, created_at: createdAt, name }: FormTemplate) => ( + + + {name} + + + {formatDate(createdAt, 'PP')} + + + { + e.stopPropagation(); + onDelete(id); + }} + > + + + + + ))} + + + ) : ( + + )} + > +); + +FormsList.defaultProps = { + onClickRow: null, +}; + +const FormsListMemo = memo(FormsList, areEqual); + +export { FormsListMemo as FormsList }; diff --git a/src/shared/components/Forms/FormTabs/FormsList/formsList.module.css b/src/shared/components/Forms/FormTabs/FormsList/formsList.module.css new file mode 100644 index 0000000..716343c --- /dev/null +++ b/src/shared/components/Forms/FormTabs/FormsList/formsList.module.css @@ -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); +} diff --git a/src/shared/components/Forms/FormTabs/FormsList/index.ts b/src/shared/components/Forms/FormTabs/FormsList/index.ts new file mode 100644 index 0000000..6d9e34c --- /dev/null +++ b/src/shared/components/Forms/FormTabs/FormsList/index.ts @@ -0,0 +1 @@ +export { FormsList } from './FormsList'; diff --git a/src/shared/components/Forms/FormTabs/NoFormsTable/NoFormsTable.tsx b/src/shared/components/Forms/FormTabs/NoFormsTable/NoFormsTable.tsx new file mode 100644 index 0000000..25df04a --- /dev/null +++ b/src/shared/components/Forms/FormTabs/NoFormsTable/NoFormsTable.tsx @@ -0,0 +1,28 @@ +import React, { memo } from 'react'; +import { areEqual } from 'Utils/equalityChecks'; +import { Table, TableHeader, TableRow, Th } from 'Components/Table'; +import { Icon } from 'Components/Icons'; + +import classes from './noFormsTable.module.css'; + +const NoFormsTable = () => ( + + + + + Address + Date Created + + + + + + No forms yet. Create a new form. + + + +); + +const NoFormsTableMemo = memo(NoFormsTable, areEqual); + +export { NoFormsTableMemo as NoFormsTable }; diff --git a/src/shared/components/Forms/FormTabs/NoFormsTable/index.ts b/src/shared/components/Forms/FormTabs/NoFormsTable/index.ts new file mode 100644 index 0000000..ceeefbc --- /dev/null +++ b/src/shared/components/Forms/FormTabs/NoFormsTable/index.ts @@ -0,0 +1 @@ +export { NoFormsTable } from './NoFormsTable'; diff --git a/src/shared/components/Forms/FormTabs/NoFormsTable/noFormsTable.module.css b/src/shared/components/Forms/FormTabs/NoFormsTable/noFormsTable.module.css new file mode 100644 index 0000000..eba36f9 --- /dev/null +++ b/src/shared/components/Forms/FormTabs/NoFormsTable/noFormsTable.module.css @@ -0,0 +1,50 @@ +.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; +} diff --git a/src/shared/components/Forms/FormTabs/index.ts b/src/shared/components/Forms/FormTabs/index.ts new file mode 100644 index 0000000..6b5814f --- /dev/null +++ b/src/shared/components/Forms/FormTabs/index.ts @@ -0,0 +1,2 @@ +export { ContractForms } from './ContractForms'; +export { FormsList } from './FormsList'; diff --git a/src/shared/components/Forms/index.ts b/src/shared/components/Forms/index.ts new file mode 100644 index 0000000..aca1491 --- /dev/null +++ b/src/shared/components/Forms/index.ts @@ -0,0 +1 @@ +export { FormsList, ContractForms } from './FormTabs'; diff --git a/src/shared/components/Tabs/FormTabs/FormTabs.tsx b/src/shared/components/Tabs/FormTabs/FormTabs.tsx new file mode 100644 index 0000000..2702ece --- /dev/null +++ b/src/shared/components/Tabs/FormTabs/FormTabs.tsx @@ -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) => ( + <> + + <> + + Contract Forms + > + + > +); + +/* + 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 ( + + + + + + + {createTabs(activeTab, onTabClick)} + + + + {children} + + + + + + ); +}; + +FormTabs.defaultProps = { + id: undefined, + className: undefined, + children: undefined, +}; +const FormTabsMemo = memo(FormTabs, areEqual); +export { FormTabsMemo as FormTabs }; diff --git a/src/shared/components/Tabs/FormTabs/forms.tabs.module.css b/src/shared/components/Tabs/FormTabs/forms.tabs.module.css new file mode 100644 index 0000000..b864ef0 --- /dev/null +++ b/src/shared/components/Tabs/FormTabs/forms.tabs.module.css @@ -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; +} diff --git a/src/shared/components/Tabs/FormTabs/index.ts b/src/shared/components/Tabs/FormTabs/index.ts new file mode 100644 index 0000000..3499307 --- /dev/null +++ b/src/shared/components/Tabs/FormTabs/index.ts @@ -0,0 +1 @@ +export { FormTabs } from './FormTabs'; diff --git a/src/shared/components/Tabs/index.ts b/src/shared/components/Tabs/index.ts index a29af18..d70196c 100644 --- a/src/shared/components/Tabs/index.ts +++ b/src/shared/components/Tabs/index.ts @@ -4,3 +4,4 @@ export { MobileProjectsTabs } from './ProjectsTabs'; export { TabContent } from './TabContent'; export { ProjectTabMenu } from './ProjectTabMenu'; export { ProjectsTabMenu } from './ProjectsTabMenu'; +export { FormTabs } from './FormTabs'; diff --git a/src/shared/containers/Forms/FormTabs/ContractForms/ContractForms.tsx b/src/shared/containers/Forms/FormTabs/ContractForms/ContractForms.tsx new file mode 100644 index 0000000..4599f65 --- /dev/null +++ b/src/shared/containers/Forms/FormTabs/ContractForms/ContractForms.tsx @@ -0,0 +1,37 @@ +import React, { memo, useEffect, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { areEqual } from 'Utils/equalityChecks'; +import { contractFormsSelector, fetchingContractFormsSelector, companyIdSelector } from 'Containers/Forms/selectors'; +import { ContractForms } from 'Components/Forms'; +import { listCompanyContractForms } from 'Containers/Forms/actions'; + +const ContractFormsContainer = () => { + const dispatch = useDispatch(); + const contractForms = useSelector(contractFormsSelector, areEqual); + const fetching = useSelector(fetchingContractFormsSelector, areEqual); + const companyId = useSelector(companyIdSelector, areEqual); + + const getContractForms = useCallback(() => { + if (companyId) { + dispatch(listCompanyContractForms(companyId)); + } + }, [dispatch, companyId]); + + useEffect(() => { + getContractForms(); + }, [getContractForms]); + + return ( + + ); +}; + +const ContractFormsContainerMemo = memo(ContractFormsContainer, areEqual); + +export { ContractFormsContainerMemo as ContractFormsContainer }; diff --git a/src/shared/containers/Forms/FormTabs/ContractForms/index.ts b/src/shared/containers/Forms/FormTabs/ContractForms/index.ts new file mode 100644 index 0000000..406f913 --- /dev/null +++ b/src/shared/containers/Forms/FormTabs/ContractForms/index.ts @@ -0,0 +1 @@ +export { ContractFormsContainer as ContractForms } from './ContractForms'; diff --git a/src/shared/containers/Forms/FormTabs/FormTabs/FormTabs.tsx b/src/shared/containers/Forms/FormTabs/FormTabs/FormTabs.tsx new file mode 100644 index 0000000..4e486b7 --- /dev/null +++ b/src/shared/containers/Forms/FormTabs/FormTabs/FormTabs.tsx @@ -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 = () => ( + <> + + + + > +); + +const FormTabsContainerMemo = memo(FormTabsContainer, areEqual); + +export { FormTabsContainerMemo as FormTabs }; diff --git a/src/shared/containers/Forms/FormTabs/FormTabs/index.ts b/src/shared/containers/Forms/FormTabs/FormTabs/index.ts new file mode 100644 index 0000000..3499307 --- /dev/null +++ b/src/shared/containers/Forms/FormTabs/FormTabs/index.ts @@ -0,0 +1 @@ +export { FormTabs } from './FormTabs'; diff --git a/src/shared/containers/Forms/FormTabs/index.ts b/src/shared/containers/Forms/FormTabs/index.ts new file mode 100644 index 0000000..50a802f --- /dev/null +++ b/src/shared/containers/Forms/FormTabs/index.ts @@ -0,0 +1,2 @@ +export { FormTabs } from './FormTabs'; +export { ContractForms } from './ContractForms'; diff --git a/src/shared/containers/Forms/Forms.tsx b/src/shared/containers/Forms/Forms.tsx new file mode 100644 index 0000000..50c0cfb --- /dev/null +++ b/src/shared/containers/Forms/Forms.tsx @@ -0,0 +1,9 @@ +import React, { memo } from 'react'; +import { areEqual } from 'Utils/equalityChecks'; +import { FormTabs } from './FormTabs'; + +const FormsContainer = () => ; + +const FormsContainerMemo = memo(FormsContainer, areEqual); + +export { FormsContainerMemo as FormsContainer }; diff --git a/src/shared/containers/Forms/Models/FormsModel/FormModel.ts b/src/shared/containers/Forms/Models/FormsModel/FormModel.ts new file mode 100644 index 0000000..db89aa6 --- /dev/null +++ b/src/shared/containers/Forms/Models/FormsModel/FormModel.ts @@ -0,0 +1,18 @@ +/* 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[]; +}; diff --git a/src/shared/containers/Forms/Models/FormsModel/index.ts b/src/shared/containers/Forms/Models/FormsModel/index.ts new file mode 100644 index 0000000..4a728ef --- /dev/null +++ b/src/shared/containers/Forms/Models/FormsModel/index.ts @@ -0,0 +1 @@ +export type { FormTemplate, FormTemplateResponse } from './FormModel'; diff --git a/src/shared/containers/Forms/Models/index.ts b/src/shared/containers/Forms/Models/index.ts new file mode 100644 index 0000000..480f730 --- /dev/null +++ b/src/shared/containers/Forms/Models/index.ts @@ -0,0 +1 @@ +export type { FormTemplate, FormTemplateResponse } from './FormsModel'; diff --git a/src/shared/containers/Forms/actions.ts b/src/shared/containers/Forms/actions.ts new file mode 100644 index 0000000..6710554 --- /dev/null +++ b/src/shared/containers/Forms/actions.ts @@ -0,0 +1,33 @@ +import { handleApiRequest } from 'Utils/handleApiRequest'; +import { Api } from 'Utils/api'; +import { FormTemplateResponse } from 'Containers/Forms/Models'; + +export const CONTRACT_FORMS = 'CONTRACT_FORMS'; +export const FETCHING_CONTRACT_FORMS = 'FETCHING_CONTRACT_FORMS'; + +interface FormsActionTypes { + CONTRACT_FORMS: FormTemplateResponse; + FETCHING_CONTRACT_FORMS: boolean; +} + +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, + }); +}; diff --git a/src/shared/containers/Forms/index.ts b/src/shared/containers/Forms/index.ts new file mode 100644 index 0000000..51e3e6a --- /dev/null +++ b/src/shared/containers/Forms/index.ts @@ -0,0 +1 @@ +export { FormsContainer as Forms } from './Forms'; diff --git a/src/shared/containers/Forms/reducer.ts b/src/shared/containers/Forms/reducer.ts new file mode 100644 index 0000000..6165e3b --- /dev/null +++ b/src/shared/containers/Forms/reducer.ts @@ -0,0 +1,25 @@ +import { CONTRACT_FORMS, FETCHING_CONTRACT_FORMS, SetFormsActionTypes } from './actions'; + +const initialState = { + data: [], + fetchingContractForms: false, +}; + +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, + }; + } + default: + return state; + } +}; diff --git a/src/shared/containers/Forms/selectors.ts b/src/shared/containers/Forms/selectors.ts new file mode 100644 index 0000000..5dbe34a --- /dev/null +++ b/src/shared/containers/Forms/selectors.ts @@ -0,0 +1,4 @@ +// 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'; diff --git a/src/shared/utils/navItems.ts b/src/shared/utils/navItems.ts index e13a664..fe3b445 100644 --- a/src/shared/utils/navItems.ts +++ b/src/shared/utils/navItems.ts @@ -11,4 +11,10 @@ export const navItems = [ path: '/people', icon: 'people', }, + { + id: 4, + title: 'Forms', + path: '/form', + icon: 'projects', + }, ]; diff --git a/src/store/combinedReducers.ts b/src/store/combinedReducers.ts index 8cafd18..73cbae1 100644 --- a/src/store/combinedReducers.ts +++ b/src/store/combinedReducers.ts @@ -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, });
{name}
{formatDate(createdAt, 'PP')}
No forms yet. Create a new form.