Merge pull request #1041 from josepfo/feat/manual-entry-populate-existing-requirement

feat: enable custom entries and custom information requirements
This commit is contained in:
Rafael Taranto 2022-01-18 19:15:14 +00:00 committed by GitHub
commit abcce7ff06
11 changed files with 525 additions and 192 deletions

View file

@ -993,6 +993,7 @@ function addCustomField (customerId, label, value) {
} }
}) })
) )
.then(res => !_.isNil(res))
} }
function saveCustomField (customerId, fieldId, newValue) { function saveCustomField (customerId, fieldId, newValue) {

View file

@ -21,7 +21,7 @@ const resolvers = {
return customers.updateCustomer(customerId, customerInput, token) return customers.updateCustomer(customerId, customerInput, token)
}, },
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value), addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
saveCustomField: (...[, { customerId, fieldId, newValue }]) => customers.saveCustomField(customerId, fieldId, newValue), saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value),
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId), removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
editCustomer: async (root, { customerId, customerEdit }, context) => { editCustomer: async (root, { customerId, customerEdit }, context) => {
const token = authentication.getToken(context) const token = authentication.getToken(context)

View file

@ -1,12 +1,6 @@
const { gql } = require('apollo-server-express') const { gql } = require('apollo-server-express')
const typeDef = gql` const typeDef = gql`
type CustomerCustomField {
id: ID
label: String
value: String
}
type Customer { type Customer {
id: ID! id: ID!
authorizedOverride: String authorizedOverride: String
@ -86,6 +80,12 @@ const typeDef = gql`
content: String content: String
} }
type CustomerCustomField {
id: ID
label: String
value: String
}
type Query { type Query {
customers(phone: String, name: String, address: String, id: String): [Customer] @auth customers(phone: String, name: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth customer(customerId: ID!): Customer @auth
@ -94,9 +94,9 @@ const typeDef = gql`
type Mutation { type Mutation {
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth
addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth addCustomField(customerId: ID!, label: String!, value: String!): Boolean @auth
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean @auth
removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth

View file

@ -38,6 +38,12 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
opacity: 1 opacity: 1
} }
}} }}
// navButtonsWrapperProps={{
// style: {
// background: 'linear-gradient(to right, black 10%, transparent 80%)',
// opacity: '0.4'
// }
// }}
autoPlay={false} autoPlay={false}
indicators={false} indicators={false}
navButtonsAlwaysVisible={true} navButtonsAlwaysVisible={true}

View file

@ -1,6 +1,6 @@
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { parse, format, isValid } from 'date-fns/fp' import { parse, format } from 'date-fns/fp'
import _ from 'lodash/fp' import _ from 'lodash/fp'
import * as R from 'ramda' import * as R from 'ramda'
import { useState, React } from 'react' import { useState, React } from 'react'
@ -26,6 +26,11 @@ import { URI } from 'src/utils/apollo'
import styles from './CustomerData.styles.js' import styles from './CustomerData.styles.js'
import { EditableCard } from './components' import { EditableCard } from './components'
import {
customerDataElements,
customerDataSchemas,
formatDates
} from './helper.js'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -63,7 +68,8 @@ const CustomerData = ({
editCustomer, editCustomer,
deleteEditedData, deleteEditedData,
updateCustomRequest, updateCustomRequest,
authorizeCustomRequest authorizeCustomRequest,
updateCustomEntry
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const [listView, setListView] = useState(false) const [listView, setListView] = useState(false)
@ -84,8 +90,8 @@ const CustomerData = ({
R.compose(R.toLower, R.path(['customInfoRequest', 'customRequest', 'name'])) R.compose(R.toLower, R.path(['customInfoRequest', 'customRequest', 'name']))
) )
const customEntries = null // get customer custom entries const customFields = []
const customRequirements = [] // get customer custom requirements const customRequirements = []
const customInfoRequests = sortByName( const customInfoRequests = sortByName(
R.path(['customInfoRequests'])(customer) ?? [] R.path(['customInfoRequests'])(customer) ?? []
) )
@ -94,87 +100,8 @@ const CustomerData = ({
const getVisibleCards = _.filter(elem => elem.isAvailable) const getVisibleCards = _.filter(elem => elem.isAvailable)
const schemas = {
idScan: Yup.object().shape({
firstName: Yup.string().required(),
lastName: Yup.string().required(),
documentNumber: Yup.string().required(),
dateOfBirth: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
})
.required(),
gender: Yup.string().required(),
country: Yup.string().required(),
expirationDate: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
})
.required()
}),
usSsn: Yup.object().shape({
usSsn: Yup.string().required()
}),
idCardPhoto: Yup.object().shape({
idCardPhoto: Yup.mixed().required()
}),
frontCamera: Yup.object().shape({
frontCamera: Yup.mixed().required()
})
}
const idScanElements = [
{
name: 'firstName',
label: 'First name',
component: TextInput
},
{
name: 'documentNumber',
label: 'ID number',
component: TextInput
},
{
name: 'dateOfBirth',
label: 'Birthdate',
component: TextInput
},
{
name: 'gender',
label: 'Gender',
component: TextInput
},
{
name: 'lastName',
label: 'Last name',
component: TextInput
},
{
name: 'expirationDate',
label: 'Expiration Date',
component: TextInput
},
{
name: 'country',
label: 'Country',
component: TextInput
}
]
const usSsnElements = [
{
name: 'usSsn',
label: 'US SSN',
component: TextInput,
size: 190
}
]
const idCardPhotoElements = [{ name: 'idCardPhoto' }]
const frontCameraElements = [{ name: 'frontCamera' }]
const initialValues = { const initialValues = {
idScan: { idCardData: {
firstName: R.path(['firstName'])(idData) ?? '', firstName: R.path(['firstName'])(idData) ?? '',
lastName: R.path(['lastName'])(idData) ?? '', lastName: R.path(['lastName'])(idData) ?? '',
documentNumber: R.path(['documentNumber'])(idData) ?? '', documentNumber: R.path(['documentNumber'])(idData) ?? '',
@ -202,19 +129,9 @@ const CustomerData = ({
} }
} }
const formatDates = values => {
_.map(
elem =>
(values[elem] = format('yyyyMMdd')(
parse(new Date(), 'yyyy-MM-dd', values[elem])
))
)(['dateOfBirth', 'expirationDate'])
return values
}
const cards = [ const cards = [
{ {
fields: idScanElements, fields: customerDataElements.idCardData,
title: 'ID Scan', title: 'ID Scan',
titleIcon: <CardIcon className={classes.cardIcon} />, titleIcon: <CardIcon className={classes.cardIcon} />,
state: R.path(['idCardDataOverride'])(customer), state: R.path(['idCardDataOverride'])(customer),
@ -226,8 +143,8 @@ const CustomerData = ({
editCustomer({ editCustomer({
idCardData: _.merge(idData, formatDates(values)) idCardData: _.merge(idData, formatDates(values))
}), }),
validationSchema: schemas.idScan, validationSchema: customerDataSchemas.idCardData,
initialValues: initialValues.idScan, initialValues: initialValues.idCardData,
isAvailable: !_.isNil(idData) isAvailable: !_.isNil(idData)
}, },
{ {
@ -257,7 +174,7 @@ const CustomerData = ({
isAvailable: !_.isNil(sanctions) isAvailable: !_.isNil(sanctions)
}, },
{ {
fields: frontCameraElements, fields: customerDataElements.frontCamera,
title: 'Front facing camera', title: 'Front facing camera',
titleIcon: <EditIcon className={classes.editIcon} />, titleIcon: <EditIcon className={classes.editIcon} />,
state: R.path(['frontCameraOverride'])(customer), state: R.path(['frontCameraOverride'])(customer),
@ -279,12 +196,12 @@ const CustomerData = ({
/> />
) : null, ) : null,
hasImage: true, hasImage: true,
validationSchema: schemas.frontCamera, validationSchema: customerDataSchemas.frontCamera,
initialValues: initialValues.frontCamera, initialValues: initialValues.frontCamera,
isAvailable: !_.isNil(customer.frontCameraPath) isAvailable: !_.isNil(customer.frontCameraPath)
}, },
{ {
fields: idCardPhotoElements, fields: customerDataElements.idCardPhoto,
title: 'ID card image', title: 'ID card image',
titleIcon: <EditIcon className={classes.editIcon} />, titleIcon: <EditIcon className={classes.editIcon} />,
state: R.path(['idCardPhotoOverride'])(customer), state: R.path(['idCardPhotoOverride'])(customer),
@ -304,20 +221,20 @@ const CustomerData = ({
/> />
) : null, ) : null,
hasImage: true, hasImage: true,
validationSchema: schemas.idCardPhoto, validationSchema: customerDataSchemas.idCardPhoto,
initialValues: initialValues.idCardPhoto, initialValues: initialValues.idCardPhoto,
isAvailable: !_.isNil(customer.idCardPhotoPath) isAvailable: !_.isNil(customer.idCardPhotoPath)
}, },
{ {
fields: usSsnElements, fields: customerDataElements.usSsn,
title: 'US SSN', title: 'US SSN',
titleIcon: <CardIcon className={classes.cardIcon} />, titleIcon: <CardIcon className={classes.cardIcon} />,
state: R.path(['usSsnOverride'])(customer), state: R.path(['usSsnOverride'])(customer),
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }), authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
save: values => editCustomer({ usSsn: values.usSsn }), save: values => editCustomer(values),
deleteEditedData: () => deleteEditedData({ usSsn: null }), deleteEditedData: () => deleteEditedData({ usSsn: null }),
validationSchema: schemas.usSsn, validationSchema: customerDataSchemas.usSsn,
initialValues: initialValues.usSsn, initialValues: initialValues.usSsn,
isAvailable: !_.isNil(customer.usSsn) isAvailable: !_.isNil(customer.usSsn)
} }
@ -374,6 +291,34 @@ const CustomerData = ({
}) })
}, customInfoRequests) }, customInfoRequests)
R.forEach(it => {
customFields.push({
fields: [
{
name: it.label,
label: it.label,
value: it.value ?? '',
component: TextInput
}
],
title: it.label,
titleIcon: <EditIcon className={classes.editIcon} />,
save: values => {
updateCustomEntry({
fieldId: it.id,
value: values[it.label]
})
},
deleteEditedData: () => {},
validationSchema: Yup.object().shape({
[it.label]: Yup.string()
}),
initialValues: {
[it.label]: it.value ?? ''
}
})
}, R.path(['customFields'])(customer) ?? [])
const editableCard = ( const editableCard = (
{ {
title, title,
@ -415,19 +360,24 @@ const CustomerData = ({
<div> <div>
<div className={classes.header}> <div className={classes.header}>
<H3 className={classes.title}>{'Customer data'}</H3> <H3 className={classes.title}>{'Customer data'}</H3>
<FeatureButton {// TODO: Remove false condition for next release
active={!listView} false && (
className={classes.viewIcons} <>
Icon={OverviewIcon} <FeatureButton
InverseIcon={OverviewReversedIcon} active={!listView}
onClick={() => setListView(false)} className={classes.viewIcons}
/> Icon={OverviewIcon}
<FeatureButton InverseIcon={OverviewReversedIcon}
active={listView} onClick={() => setListView(false)}
className={classes.viewIcons} />
Icon={CustomerListViewIcon} <FeatureButton
InverseIcon={CustomerListViewReversedIcon} active={listView}
onClick={() => setListView(true)}></FeatureButton> className={classes.viewIcons}
Icon={CustomerListViewIcon}
InverseIcon={CustomerListViewReversedIcon}
onClick={() => setListView(true)}></FeatureButton>
</>
)}
</div> </div>
<div> <div>
{!listView && customer && ( {!listView && customer && (
@ -444,9 +394,21 @@ const CustomerData = ({
</Grid> </Grid>
</Grid> </Grid>
)} )}
{customEntries && ( {!_.isEmpty(customFields) && (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<span className={classes.separator}>Custom data entry</span> <span className={classes.separator}>Custom data entry</span>
<Grid container>
<Grid container direction="column" item xs={6}>
{customFields.map((elem, idx) => {
return isEven(idx) ? editableCard(elem, idx) : null
})}
</Grid>
<Grid container direction="column" item xs={6}>
{customFields.map((elem, idx) => {
return !isEven(idx) ? editableCard(elem, idx) : null
})}
</Grid>
</Grid>
</div> </div>
)} )}
{!R.isEmpty(customRequirements) && ( {!R.isEmpty(customRequirements) && (

View file

@ -18,8 +18,9 @@ import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/bl
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg' import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
import { ReactComponent as DataReversedIcon } from 'src/styling/icons/button/data/white.svg' import { ReactComponent as DataReversedIcon } from 'src/styling/icons/button/data/white.svg'
import { ReactComponent as DataIcon } from 'src/styling/icons/button/data/zodiac.svg' import { ReactComponent as DataIcon } from 'src/styling/icons/button/data/zodiac.svg'
import { ReactComponent as DiscountReversedIcon } from 'src/styling/icons/button/discount/white.svg' // TODO: Enable for next release
import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zodiac.svg' // import { ReactComponent as DiscountReversedIcon } from 'src/styling/icons/button/discount/white.svg'
// import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zodiac.svg'
import { fromNamespace, namespaces } from 'src/utils/config' import { fromNamespace, namespaces } from 'src/utils/config'
import CustomerData from './CustomerData' import CustomerData from './CustomerData'
@ -236,6 +237,27 @@ const GET_DATA = gql`
} }
` `
const SET_CUSTOM_ENTRY = gql`
mutation addCustomField($customerId: ID!, $label: String!, $value: String!) {
addCustomField(customerId: $customerId, label: $label, value: $value)
}
`
const EDIT_CUSTOM_ENTRY = gql`
mutation saveCustomField($customerId: ID!, $fieldId: ID!, $value: String!) {
saveCustomField(customerId: $customerId, fieldId: $fieldId, value: $value)
}
`
const GET_ACTIVE_CUSTOM_REQUESTS = gql`
query customInfoRequests($onlyEnabled: Boolean) {
customInfoRequests(onlyEnabled: $onlyEnabled) {
id
customRequest
}
}
`
const CustomerProfile = memo(() => { const CustomerProfile = memo(() => {
const history = useHistory() const history = useHistory()
@ -255,6 +277,20 @@ const CustomerProfile = memo(() => {
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const { data: activeCustomRequests } = useQuery(GET_ACTIVE_CUSTOM_REQUESTS, {
variables: {
onlyEnabled: true
}
})
const [setCustomEntry] = useMutation(SET_CUSTOM_ENTRY, {
onCompleted: () => getCustomer()
})
const [editCustomEntry] = useMutation(EDIT_CUSTOM_ENTRY, {
onCompleted: () => getCustomer()
})
const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, {
onCompleted: () => getCustomer() onCompleted: () => getCustomer()
}) })
@ -294,6 +330,27 @@ const CustomerProfile = memo(() => {
onCompleted: () => getCustomer() onCompleted: () => getCustomer()
}) })
const saveCustomEntry = it => {
setCustomEntry({
variables: {
customerId,
label: it.title,
value: it.data
}
})
setWizard(null)
}
const updateCustomEntry = it => {
editCustomEntry({
variables: {
customerId,
fieldId: it.fieldId,
value: it.value
}
})
}
const updateCustomer = it => const updateCustomer = it =>
setCustomer({ setCustomer({
variables: { variables: {
@ -302,7 +359,7 @@ const CustomerProfile = memo(() => {
} }
}) })
const replacePhoto = it => const replacePhoto = it => {
replaceCustomerPhoto({ replaceCustomerPhoto({
variables: { variables: {
customerId, customerId,
@ -310,14 +367,18 @@ const CustomerProfile = memo(() => {
photoType: it.photoType photoType: it.photoType
} }
}) })
setWizard(null)
}
const editCustomer = it => const editCustomer = it => {
editCustomerData({ editCustomerData({
variables: { variables: {
customerId, customerId,
customerEdit: it customerEdit: it
} }
}) })
setWizard(null)
}
const deleteEditedData = it => const deleteEditedData = it =>
deleteCustomerEditedData({ deleteCustomerEditedData({
@ -385,6 +446,12 @@ const CustomerProfile = memo(() => {
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const customInfoRequirementOptions =
activeCustomRequests?.customInfoRequests?.map(it => ({
value: it.id,
display: it.customRequest.name
})) ?? []
const classes = useStyles() const classes = useStyles()
return ( return (
@ -428,14 +495,17 @@ const CustomerProfile = memo(() => {
onClick={() => setWizard(true)}> onClick={() => setWizard(true)}>
{`Manual data entry`} {`Manual data entry`}
</ActionButton> </ActionButton>
<ActionButton {
// TODO: Enable for next release
/* <ActionButton
className={classes.actionButton} className={classes.actionButton}
color="primary" color="primary"
Icon={Discount} Icon={Discount}
InverseIcon={DiscountReversedIcon} InverseIcon={DiscountReversedIcon}
onClick={() => {}}> onClick={() => {}}>
{`Add individual discount`} {`Add individual discount`}
</ActionButton> </ActionButton> */
}
{isSuspended && ( {isSuspended && (
<ActionButton <ActionButton
className={classes.actionButton} className={classes.actionButton}
@ -522,7 +592,8 @@ const CustomerProfile = memo(() => {
editCustomer={editCustomer} editCustomer={editCustomer}
deleteEditedData={deleteEditedData} deleteEditedData={deleteEditedData}
updateCustomRequest={setCustomerCustomInfoRequest} updateCustomRequest={setCustomerCustomInfoRequest}
authorizeCustomRequest={authorizeCustomRequest}></CustomerData> authorizeCustomRequest={authorizeCustomRequest}
updateCustomEntry={updateCustomEntry}></CustomerData>
</div> </div>
)} )}
{isNotes && ( {isNotes && (
@ -544,8 +615,11 @@ const CustomerProfile = memo(() => {
{wizard && ( {wizard && (
<Wizard <Wizard
error={error?.message} error={error?.message}
save={() => {}} save={saveCustomEntry}
addPhoto={replacePhoto}
addCustomerData={editCustomer}
onClose={() => setWizard(null)} onClose={() => setWizard(null)}
customInfoRequirementOptions={customInfoRequirementOptions}
/> />
)} )}
</div> </div>

View file

@ -9,7 +9,14 @@ import Stepper from 'src/components/Stepper'
import { Button } from 'src/components/buttons' import { Button } from 'src/components/buttons'
import { comet } from 'src/styling/variables' import { comet } from 'src/styling/variables'
import { entryType, customElements } from './helper' import {
entryType,
customElements,
requirementElements,
formatDates,
REQUIREMENT,
ID_CARD_DATA
} from './helper'
const LAST_STEP = 2 const LAST_STEP = 2
@ -41,23 +48,40 @@ const styles = {
margin: [[0, 4, 0, 2]], margin: [[0, 4, 0, 2]],
borderBottom: `1px solid ${comet}`, borderBottom: `1px solid ${comet}`,
display: 'inline-block' display: 'inline-block'
},
dropdownField: {
marginTop: 16,
minWidth: 155
} }
} }
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const getStep = (step, selectedValues) => { const getStep = (step, selectedValues) => {
const elements =
selectedValues?.entryType === REQUIREMENT &&
!R.isNil(selectedValues?.requirement)
? requirementElements[selectedValues?.requirement]
: customElements[selectedValues?.dataType]
switch (step) { switch (step) {
case 1: case 1:
return entryType return entryType
case 2: case 2:
return customElements[selectedValues?.dataType] return elements
default: default:
return Fragment return Fragment
} }
} }
const Wizard = ({ onClose, save, error }) => { const Wizard = ({
onClose,
save,
error,
customInfoRequirementOptions,
addCustomerData,
addPhoto
}) => {
const classes = useStyles() const classes = useStyles()
const [selectedValues, setSelectedValues] = useState(null) const [selectedValues, setSelectedValues] = useState(null)
@ -66,6 +90,10 @@ const Wizard = ({ onClose, save, error }) => {
step: 1 step: 1
}) })
const isIdCardData = values => values?.requirement === ID_CARD_DATA
const formatCustomerData = (it, newConfig) =>
isIdCardData(newConfig) ? { [newConfig.requirement]: formatDates(it) } : it
const isLastStep = step === LAST_STEP const isLastStep = step === LAST_STEP
const stepOptions = getStep(step, selectedValues) const stepOptions = getStep(step, selectedValues)
@ -74,7 +102,23 @@ const Wizard = ({ onClose, save, error }) => {
setSelectedValues(newConfig) setSelectedValues(newConfig)
if (isLastStep) { if (isLastStep) {
return save(newConfig) switch (stepOptions.saveType) {
case 'customerData':
return addCustomerData(formatCustomerData(it, newConfig))
case 'customerDataUpload':
return addPhoto({
newPhoto: R.head(R.values(it)),
photoType: R.head(R.keys(it))
})
case 'customEntry':
return save(newConfig)
case 'customInfoRequirement':
return
// case 'customerEntryUpload':
// break
default:
break
}
} }
setState({ setState({
@ -106,6 +150,7 @@ const Wizard = ({ onClose, save, error }) => {
<Form className={classes.form}> <Form className={classes.form}>
<stepOptions.Component <stepOptions.Component
selectedValues={selectedValues} selectedValues={selectedValues}
customInfoRequirementOptions={customInfoRequirementOptions}
{...stepOptions.props} {...stepOptions.props}
/> />
<div className={classes.submit}> <div className={classes.submit}>

View file

@ -150,7 +150,7 @@ const EditableCard = ({
<H3 className={classes.cardTitle}>{title}</H3> <H3 className={classes.cardTitle}>{title}</H3>
<Tooltip width={304}></Tooltip> <Tooltip width={304}></Tooltip>
</div> </div>
{state && ( {state && authorize && (
<div className={classnames(label1ClassNames)}> <div className={classnames(label1ClassNames)}>
<MainStatus statuses={[authorized]} /> <MainStatus statuses={[authorized]} />
</div> </div>
@ -207,17 +207,19 @@ const EditableCard = ({
<div className={classes.edit}> <div className={classes.edit}>
{!editing && ( {!editing && (
<div className={classes.editButton}> <div className={classes.editButton}>
<div className={classes.deleteButton}> {// TODO: Remove false condition for next release
<ActionButton false && (
color="primary" <div className={classes.deleteButton}>
type="button" <ActionButton
Icon={DeleteIcon} color="primary"
InverseIcon={DeleteReversedIcon} type="button"
onClick={() => deleteEditedData()}> Icon={DeleteIcon}
{`Delete`} InverseIcon={DeleteReversedIcon}
</ActionButton> onClick={() => deleteEditedData()}>
</div> {`Delete`}
</ActionButton>
</div>
)}
<ActionButton <ActionButton
color="primary" color="primary"
Icon={EditIcon} Icon={EditIcon}
@ -279,7 +281,7 @@ const EditableCard = ({
Cancel Cancel
</ActionButton> </ActionButton>
</div> </div>
{authorized.label !== 'Accepted' && ( {authorize && authorized.label !== 'Accepted' && (
<div className={classes.button}> <div className={classes.button}>
<ActionButton <ActionButton
color="spring" color="spring"
@ -291,7 +293,7 @@ const EditableCard = ({
</ActionButton> </ActionButton>
</div> </div>
)} )}
{authorized.label !== 'Rejected' && ( {authorize && authorized.label !== 'Rejected' && (
<ActionButton <ActionButton
color="tomato" color="tomato"
type="button" type="button"

View file

@ -12,7 +12,6 @@ import { offColor, subheaderColor } from 'src/styling/variables'
const useStyles = makeStyles({ const useStyles = makeStyles({
box: { box: {
boxSizing: 'border-box', boxSizing: 'border-box',
marginTop: 40,
width: 450, width: 450,
height: 120, height: 120,
borderStyle: 'dashed', borderStyle: 'dashed',
@ -32,6 +31,7 @@ const useStyles = makeStyles({
display: 'flex' display: 'flex'
}, },
board: { board: {
marginTop: 40,
width: 450, width: 450,
height: 120 height: 120
}, },
@ -48,12 +48,15 @@ const Upload = ({ type }) => {
const { setFieldValue } = useFormikContext() const { setFieldValue } = useFormikContext()
const IMAGE = 'image' const IMAGE = 'image'
const isImage = type === IMAGE const ID_CARD_PHOTO = 'idCardPhoto'
const FRONT_CAMERA = 'frontCamera'
const isImage =
type === IMAGE || type === FRONT_CAMERA || type === ID_CARD_PHOTO
const onDrop = useCallback( const onDrop = useCallback(
acceptedData => { acceptedData => {
// TODO: attach the uploaded data to the form as well setFieldValue(type, R.head(acceptedData))
setFieldValue(type, R.head(acceptedData).name)
setData({ setData({
preview: isImage preview: isImage
@ -84,12 +87,12 @@ const Upload = ({ type }) => {
</div> </div>
</div> </div>
)} )}
{!R.isEmpty(data) && type === IMAGE && ( {!R.isEmpty(data) && isImage && (
<div key={data.name}> <div key={data.name}>
<img src={data.preview} className={classes.box} alt=""></img> <img src={data.preview} className={classes.box} alt=""></img>
</div> </div>
)} )}
{!R.isEmpty(data) && type !== IMAGE && ( {!R.isEmpty(data) && !isImage && (
<div className={classes.box}> <div className={classes.box}>
<H3 className={classes.uploadContent}>{data.preview}</H3> <H3 className={classes.uploadContent}>{data.preview}</H3>
</div> </div>

View file

@ -1,11 +1,16 @@
import { makeStyles, Box } from '@material-ui/core' import { makeStyles, Box } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import { parse, isValid, format } from 'date-fns/fp'
import { Field, useFormikContext } from 'formik' import { Field, useFormikContext } from 'formik'
import { parsePhoneNumberFromString } from 'libphonenumber-js' import { parsePhoneNumberFromString } from 'libphonenumber-js'
import * as R from 'ramda' import * as R from 'ramda'
import * as Yup from 'yup' import * as Yup from 'yup'
import { RadioGroup, TextInput } from 'src/components/inputs/formik' import {
RadioGroup,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { H4 } from 'src/components/typography' import { H4 } from 'src/components/typography'
import { errorColor } from 'src/styling/variables' import { errorColor } from 'src/styling/variables'
import { MANUAL } from 'src/utils/constants' import { MANUAL } from 'src/utils/constants'
@ -35,10 +40,21 @@ const useStyles = makeStyles({
specialGrid: { specialGrid: {
display: 'grid', display: 'grid',
gridTemplateColumns: [[182, 162, 141]] gridTemplateColumns: [[182, 162, 141]]
},
picker: {
width: 150
},
field: {
'& > *:last-child': {
marginBottom: 24
}
} }
}) })
const CUSTOMER_BLOCKED = 'blocked' const CUSTOMER_BLOCKED = 'blocked'
const CUSTOM = 'custom'
const REQUIREMENT = 'requirement'
const ID_CARD_DATA = 'idCardData'
const getAuthorizedStatus = (it, triggers) => { const getAuthorizedStatus = (it, triggers) => {
const fields = [ const fields = [
@ -97,34 +113,46 @@ const getName = it => {
) ?? ''}`.trim() ) ?? ''}`.trim()
} }
// Manual Entry Wizard
const entryOptions = [ const entryOptions = [
{ display: 'Custom entry', code: 'custom' }, { display: 'Custom entry', code: 'custom' },
{ display: 'Populate existing requirement', code: 'requirement' } { display: 'Populate existing requirement', code: 'requirement' }
] ]
const dataOptions = [ const dataOptions = [
{ display: 'Text', code: 'text' }, { display: 'Text', code: 'text' }
{ display: 'File', code: 'file' }, // TODO: Requires backend modifications to support File and Image
{ display: 'Image', code: 'image' } // { display: 'File', code: 'file' },
// { display: 'Image', code: 'image' }
] ]
const requirementOptions = [ const requirementOptions = [
{ display: 'Birthdate', code: 'birthdate' },
{ display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID card image', code: 'idCardPhoto' },
{ display: 'ID data', code: 'idCardData' }, { display: 'ID data', code: 'idCardData' },
{ display: 'Customer camera', code: 'facephoto' }, { display: 'US SSN', code: 'usSsn' },
{ display: 'US SSN', code: 'usSsn' } { display: 'Customer camera', code: 'frontCamera' }
] ]
const customTextOptions = [ const customTextOptions = [
{ display: 'Data entry title', code: 'title' }, { label: 'Data entry title', name: 'title' },
{ display: 'Data entry', code: 'data' } { label: 'Data entry', name: 'data' }
] ]
const customUploadOptions = [{ display: 'Data entry title', code: 'title' }] const customUploadOptions = [{ label: 'Data entry title', name: 'title' }]
const entryTypeSchema = Yup.object().shape({ const entryTypeSchema = Yup.lazy(values => {
entryType: Yup.string().required() if (values.entryType === 'custom') {
return Yup.object().shape({
entryType: Yup.string().required(),
dataType: Yup.string().required()
})
} else if (values.entryType === 'requirement') {
return Yup.object().shape({
entryType: Yup.string().required(),
requirement: Yup.string().required()
})
}
}) })
const customFileSchema = Yup.object().shape({ const customFileSchema = Yup.object().shape({
@ -142,13 +170,18 @@ const customTextSchema = Yup.object().shape({
data: Yup.string().required() data: Yup.string().required()
}) })
const EntryType = () => { const updateRequirementOptions = it => [
{
display: 'Custom information requirement',
code: 'custom'
},
...it
]
const EntryType = ({ customInfoRequirementOptions }) => {
const classes = useStyles() const classes = useStyles()
const { values } = useFormikContext() const { values } = useFormikContext()
const CUSTOM = 'custom'
const REQUIREMENT = 'requirement'
const displayCustomOptions = values.entryType === CUSTOM const displayCustomOptions = values.entryType === CUSTOM
const displayRequirementOptions = values.entryType === REQUIREMENT const displayRequirementOptions = values.entryType === REQUIREMENT
@ -188,7 +221,13 @@ const EntryType = () => {
<Field <Field
component={RadioGroup} component={RadioGroup}
name="requirement" name="requirement"
options={requirementOptions} options={
requirementOptions
// TODO: Enable once custom info requirement manual entry is finished
// !R.isEmpty(customInfoRequirementOptions)
// ? updateRequirementOptions(requirementOptions)
// : requirementOptions
}
labelClassName={classes.label} labelClassName={classes.label}
radioClassName={classes.radio} radioClassName={classes.radio}
className={classnames(classes.radioGroup, classes.specialGrid)} className={classnames(classes.radioGroup, classes.specialGrid)}
@ -199,18 +238,73 @@ const EntryType = () => {
) )
} }
const CustomData = ({ selectedValues }) => { const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => {
const classes = useStyles()
const typeOfEntrySelected = selectedValues?.entryType
const dataTypeSelected = selectedValues?.dataType const dataTypeSelected = selectedValues?.dataType
const upload = dataTypeSelected === 'file' || dataTypeSelected === 'image' const requirementSelected = selectedValues?.requirement
const displayRequirements = typeOfEntrySelected === 'requirement'
const isCustomInfoRequirement = requirementSelected === CUSTOM
const updatedRequirementOptions = !R.isEmpty(customInfoRequirementOptions)
? updateRequirementOptions(requirementOptions)
: requirementOptions
const requirementName = displayRequirements
? R.find(R.propEq('code', requirementSelected))(updatedRequirementOptions)
.display
: ''
const title = displayRequirements
? `Requirement ${requirementName}`
: `Custom ${dataTypeSelected} entry`
const elements = displayRequirements
? requirementElements[requirementSelected]
: customElements[dataTypeSelected]
const upload = displayRequirements
? requirementSelected === 'idCardPhoto' ||
requirementSelected === 'frontCamera'
: dataTypeSelected === 'file' || dataTypeSelected === 'image'
return ( return (
<> <>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<H4>{`Custom ${dataTypeSelected} entry`}</H4> <H4>{title}</H4>
</Box> </Box>
{customElements[dataTypeSelected].options.map(({ display, code }) => ( {isCustomInfoRequirement && (
<Field name={code} label={display} component={TextInput} width={390} /> <Autocomplete
))} fullWidth
{upload && <Upload type={dataTypeSelected}></Upload>} label={`Available requests`}
className={classes.picker}
getOptionSelected={R.eqProps('code')}
labelProp={'display'}
options={customInfoRequirementOptions}
onChange={(evt, it) => {}}
/>
)}
<div className={classes.field}>
{!upload &&
!isCustomInfoRequirement &&
elements.options.map(({ label, name }) => (
<Field
name={name}
label={label}
component={TextInput}
width={390}
/>
))}
</div>
{upload && (
<Upload
type={
displayRequirements ? requirementSelected : dataTypeSelected
}></Upload>
)}
</> </>
) )
} }
@ -219,20 +313,23 @@ const customElements = {
text: { text: {
schema: customTextSchema, schema: customTextSchema,
options: customTextOptions, options: customTextOptions,
Component: CustomData, Component: ManualDataEntry,
initialValues: { data: '', title: '' } initialValues: { data: '', title: '' },
saveType: 'customEntry'
}, },
file: { file: {
schema: customFileSchema, schema: customFileSchema,
options: customUploadOptions, options: customUploadOptions,
Component: CustomData, Component: ManualDataEntry,
initialValues: { file: '', title: '' } initialValues: { file: null, title: '' },
saveType: 'customEntryUpload'
}, },
image: { image: {
schema: customImageSchema, schema: customImageSchema,
options: customUploadOptions, options: customUploadOptions,
Component: CustomData, Component: ManualDataEntry,
initialValues: { image: '', title: '' } initialValues: { image: null, title: '' },
saveType: 'customEntryUpload'
} }
} }
@ -243,6 +340,142 @@ const entryType = {
initialValues: { entryType: '' } initialValues: { entryType: '' }
} }
// Customer data
const customerDataElements = {
idCardData: [
{
name: 'firstName',
label: 'First name',
component: TextInput
},
{
name: 'documentNumber',
label: 'ID number',
component: TextInput
},
{
name: 'dateOfBirth',
label: 'Birthdate',
component: TextInput
},
{
name: 'gender',
label: 'Gender',
component: TextInput
},
{
name: 'lastName',
label: 'Last name',
component: TextInput
},
{
name: 'expirationDate',
label: 'Expiration Date',
component: TextInput
},
{
name: 'country',
label: 'Country',
component: TextInput
}
],
usSsn: [
{
name: 'usSsn',
label: 'US SSN',
component: TextInput,
size: 190
}
],
idCardPhoto: [{ name: 'idCardPhoto' }],
frontCamera: [{ name: 'frontCamera' }]
}
const customerDataSchemas = {
idCardData: Yup.object().shape({
firstName: Yup.string().required(),
lastName: Yup.string().required(),
documentNumber: Yup.string().required(),
dateOfBirth: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
})
.required(),
gender: Yup.string().required(),
country: Yup.string().required(),
expirationDate: Yup.string()
.test({
test: val => isValid(parse(new Date(), 'yyyy-MM-dd', val))
})
.required()
}),
usSsn: Yup.object().shape({
usSsn: Yup.string().required()
}),
idCardPhoto: Yup.object().shape({
idCardPhoto: Yup.mixed().required()
}),
frontCamera: Yup.object().shape({
frontCamera: Yup.mixed().required()
})
}
const requirementElements = {
idCardData: {
schema: customerDataSchemas.idCardData,
options: customerDataElements.idCardData,
Component: ManualDataEntry,
initialValues: {
firstName: '',
lastName: '',
documentNumber: '',
dateOfBirth: '',
gender: '',
country: '',
expirationDate: ''
},
saveType: 'customerData'
},
usSsn: {
schema: customerDataSchemas.usSsn,
options: customerDataElements.usSsn,
Component: ManualDataEntry,
initialValues: { usSsn: '' },
saveType: 'customerData'
},
idCardPhoto: {
schema: customerDataSchemas.idCardPhoto,
options: customerDataElements.idCardPhoto,
Component: ManualDataEntry,
initialValues: { idCardPhoto: null },
saveType: 'customerDataUpload'
},
frontCamera: {
schema: customerDataSchemas.frontCamera,
options: customerDataElements.frontCamera,
Component: ManualDataEntry,
initialValues: { frontCamera: null },
saveType: 'customerDataUpload'
},
custom: {
// schema: customerDataSchemas.customInfoRequirement,
Component: ManualDataEntry,
initialValues: { customInfoRequirement: null },
saveType: 'customInfoRequirement'
}
}
const formatDates = values => {
R.map(
elem =>
(values[elem] = format('yyyyMMdd')(
parse(new Date(), 'yyyy-MM-dd', values[elem])
))
)(['dateOfBirth', 'expirationDate'])
return values
}
const mapKeys = pair => { const mapKeys = pair => {
const [key, value] = pair const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') { if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
@ -279,5 +512,12 @@ export {
getName, getName,
entryType, entryType,
customElements, customElements,
formatPhotosData requirementElements,
formatPhotosData,
customerDataElements,
customerDataSchemas,
formatDates,
REQUIREMENT,
CUSTOM,
ID_CARD_DATA
} }

View file

@ -565,7 +565,7 @@ const Requirement = ({ customInfoRequests }) => {
} }
const options = enableCustomRequirement const options = enableCustomRequirement
? [...requirementOptions, customInfoOption] ? [...requirementOptions, customInfoOption]
: [...requirementOptions, { ...customInfoOption, disabled: true }] : [...requirementOptions]
const titleClass = { const titleClass = {
[classes.error]: [classes.error]:
(!!errors.requirement && !isSuspend && !isCustom) || (!!errors.requirement && !isSuspend && !isCustom) ||