From 8ad127c6c4819987f283e19cecec9e77410e8328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Mon, 17 Jan 2022 00:03:18 +0000 Subject: [PATCH] feat: enable customer data manual entry --- .../src/pages/Customers/CustomerData.js | 48 ++-- .../src/pages/Customers/CustomerProfile.js | 35 ++- .../src/pages/Customers/Wizard.js | 60 +++-- .../src/pages/Customers/components/Upload.js | 15 +- .../src/pages/Customers/helper.js | 212 ++++++++++++++---- 5 files changed, 274 insertions(+), 96 deletions(-) diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index a5346a0e..3f792db3 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -26,7 +26,11 @@ import { URI } from 'src/utils/apollo' import styles from './CustomerData.styles.js' import { EditableCard } from './components' -import { customerDataElements, customerDataschemas } from './helper.js' +import { + customerDataElements, + customerDataSchemas, + formatDates +} from './helper.js' const useStyles = makeStyles(styles) @@ -64,7 +68,8 @@ const CustomerData = ({ editCustomer, deleteEditedData, updateCustomRequest, - authorizeCustomRequest + authorizeCustomRequest, + updateCustomEntry }) => { const classes = useStyles() const [listView, setListView] = useState(false) @@ -96,7 +101,7 @@ const CustomerData = ({ const getVisibleCards = _.filter(elem => elem.isAvailable) const initialValues = { - idScan: { + idCardData: { firstName: R.path(['firstName'])(idData) ?? '', lastName: R.path(['lastName'])(idData) ?? '', documentNumber: R.path(['documentNumber'])(idData) ?? '', @@ -124,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 = [ { - fields: customerDataElements.idScanElements, + fields: customerDataElements.idCardData, title: 'ID Scan', titleIcon: , state: R.path(['idCardDataOverride'])(customer), @@ -148,8 +143,8 @@ const CustomerData = ({ editCustomer({ idCardData: _.merge(idData, formatDates(values)) }), - validationSchema: customerDataschemas.idScan, - initialValues: initialValues.idScan, + validationSchema: customerDataSchemas.idCardData, + initialValues: initialValues.idCardData, isAvailable: !_.isNil(idData) }, { @@ -179,7 +174,7 @@ const CustomerData = ({ isAvailable: !_.isNil(sanctions) }, { - fields: customerDataElements.frontCameraElements, + fields: customerDataElements.frontCamera, title: 'Front facing camera', titleIcon: , state: R.path(['frontCameraOverride'])(customer), @@ -201,12 +196,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: customerDataschemas.frontCamera, + validationSchema: customerDataSchemas.frontCamera, initialValues: initialValues.frontCamera, isAvailable: !_.isNil(customer.frontCameraPath) }, { - fields: customerDataElements.idCardPhotoElements, + fields: customerDataElements.idCardPhoto, title: 'ID card image', titleIcon: , state: R.path(['idCardPhotoOverride'])(customer), @@ -226,20 +221,20 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: customerDataschemas.idCardPhoto, + validationSchema: customerDataSchemas.idCardPhoto, initialValues: initialValues.idCardPhoto, isAvailable: !_.isNil(customer.idCardPhotoPath) }, { - fields: customerDataElements.usSsnElements, + fields: customerDataElements.usSsn, title: 'US SSN', titleIcon: , state: R.path(['usSsnOverride'])(customer), authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }), reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), - save: values => editCustomer({ usSsn: values.usSsn }), + save: values => editCustomer(values), deleteEditedData: () => deleteEditedData({ usSsn: null }), - validationSchema: customerDataschemas.usSsn, + validationSchema: customerDataSchemas.usSsn, initialValues: initialValues.usSsn, isAvailable: !_.isNil(customer.usSsn) } @@ -308,7 +303,12 @@ const CustomerData = ({ ], title: it.label, titleIcon: , - save: () => {}, + save: values => { + updateCustomEntry({ + fieldId: it.id, + value: values[it.label] + }) + }, deleteEditedData: () => {}, validationSchema: Yup.object().shape({ [it.label]: Yup.string() diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index d83960d3..71b24ba2 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -242,6 +242,12 @@ const SET_CUSTOM_ENTRY = gql` } ` +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) { @@ -280,6 +286,10 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const [editCustomEntry] = useMutation(EDIT_CUSTOM_ENTRY, { + onCompleted: () => getCustomer() + }) + const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { onCompleted: () => getCustomer() }) @@ -330,6 +340,16 @@ const CustomerProfile = memo(() => { setWizard(null) } + const updateCustomEntry = it => { + editCustomEntry({ + variables: { + customerId, + fieldId: it.fieldId, + value: it.value + } + }) + } + const updateCustomer = it => setCustomer({ variables: { @@ -338,7 +358,7 @@ const CustomerProfile = memo(() => { } }) - const replacePhoto = it => + const replacePhoto = it => { replaceCustomerPhoto({ variables: { customerId, @@ -346,14 +366,18 @@ const CustomerProfile = memo(() => { photoType: it.photoType } }) + setWizard(null) + } - const editCustomer = it => + const editCustomer = it => { editCustomerData({ variables: { customerId, customerEdit: it } }) + setWizard(null) + } const deleteEditedData = it => deleteCustomerEditedData({ @@ -421,7 +445,7 @@ const CustomerProfile = memo(() => { const timezone = R.path(['config', 'locale_timezone'], configResponse) - const customRequirementOptions = + const customInfoRequirementOptions = activeCustomRequests?.customInfoRequests?.map(it => ({ value: it.id, display: it.customRequest.name @@ -564,7 +588,8 @@ const CustomerProfile = memo(() => { editCustomer={editCustomer} deleteEditedData={deleteEditedData} updateCustomRequest={setCustomerCustomInfoRequest} - authorizeCustomRequest={authorizeCustomRequest}> + authorizeCustomRequest={authorizeCustomRequest} + updateCustomEntry={updateCustomEntry}> )} {isNotes && ( @@ -590,7 +615,7 @@ const CustomerProfile = memo(() => { addPhoto={replacePhoto} addCustomerData={editCustomer} onClose={() => setWizard(null)} - customRequirementOptions={customRequirementOptions} + customInfoRequirementOptions={customInfoRequirementOptions} /> )} diff --git a/new-lamassu-admin/src/pages/Customers/Wizard.js b/new-lamassu-admin/src/pages/Customers/Wizard.js index 16db1647..f257e23f 100644 --- a/new-lamassu-admin/src/pages/Customers/Wizard.js +++ b/new-lamassu-admin/src/pages/Customers/Wizard.js @@ -1,5 +1,5 @@ import { makeStyles } from '@material-ui/core' -import { Form, Formik, Field } from 'formik' +import { Form, Formik } from 'formik' import * as R from 'ramda' import React, { useState, Fragment } from 'react' @@ -7,10 +7,16 @@ import ErrorMessage from 'src/components/ErrorMessage' import Modal from 'src/components/Modal' import Stepper from 'src/components/Stepper' import { Button } from 'src/components/buttons' -import { Dropdown } from 'src/components/inputs/formik' 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 @@ -52,11 +58,17 @@ const styles = { const useStyles = makeStyles(styles) const getStep = (step, selectedValues) => { + const elements = + selectedValues?.entryType === REQUIREMENT && + !R.isNil(selectedValues?.requirement) + ? requirementElements[selectedValues?.requirement] + : customElements[selectedValues?.dataType] + switch (step) { case 1: return entryType case 2: - return customElements[selectedValues?.dataType] + return elements default: return Fragment } @@ -66,7 +78,7 @@ const Wizard = ({ onClose, save, error, - customRequirementOptions, + customInfoRequirementOptions, addCustomerData, addPhoto }) => { @@ -78,7 +90,10 @@ const Wizard = ({ step: 1 }) - const isCustom = values => values?.requirement === 'custom' + 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 stepOptions = getStep(step, selectedValues) @@ -87,7 +102,23 @@ const Wizard = ({ setSelectedValues(newConfig) 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({ @@ -120,22 +151,9 @@ const Wizard = ({
- {isCustom(values) && ( -
- -
- )}
{error && Failed to save}
)} - {!R.isEmpty(data) && type === IMAGE && ( + {!R.isEmpty(data) && isImage && (
)} - {!R.isEmpty(data) && type !== IMAGE && ( + {!R.isEmpty(data) && !isImage && (

{data.preview}

diff --git a/new-lamassu-admin/src/pages/Customers/helper.js b/new-lamassu-admin/src/pages/Customers/helper.js index e6834459..009b21a5 100644 --- a/new-lamassu-admin/src/pages/Customers/helper.js +++ b/new-lamassu-admin/src/pages/Customers/helper.js @@ -1,12 +1,16 @@ import { makeStyles, Box } from '@material-ui/core' import classnames from 'classnames' -import { parse, isValid } from 'date-fns/fp' +import { parse, isValid, format } from 'date-fns/fp' import { Field, useFormikContext } from 'formik' import { parsePhoneNumberFromString } from 'libphonenumber-js' import * as R from 'ramda' 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 { errorColor } from 'src/styling/variables' @@ -35,10 +39,16 @@ const useStyles = makeStyles({ specialGrid: { display: 'grid', gridTemplateColumns: [[182, 162, 141]] + }, + picker: { + width: 150 } }) const CUSTOMER_BLOCKED = 'blocked' +const CUSTOM = 'custom' +const REQUIREMENT = 'requirement' +const ID_CARD_DATA = 'idCardData' const getAuthorizedStatus = it => it.authorizedOverride === CUSTOMER_BLOCKED @@ -82,18 +92,28 @@ const requirementOptions = [ { display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID data', code: 'idCardData' }, { display: 'US SSN', code: 'usSsn' }, - { display: 'Customer camera', code: 'facephoto' } + { display: 'Customer camera', code: 'frontCamera' } ] const customTextOptions = [ - { display: 'Data entry title', code: 'title' }, - { display: 'Data entry', code: 'data' } + { label: 'Data entry title', name: 'title' }, + { 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({ - entryType: Yup.string().required() +const entryTypeSchema = Yup.lazy(values => { + 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({ @@ -111,13 +131,18 @@ const customTextSchema = Yup.object().shape({ data: Yup.string().required() }) -const EntryType = ({ hasCustomRequirementOptions }) => { +const updateRequirementOptions = it => [ + { + display: 'Custom information requirement', + code: 'custom' + }, + ...it +] + +const EntryType = ({ customInfoRequirementOptions }) => { const classes = useStyles() const { values } = useFormikContext() - const CUSTOM = 'custom' - const REQUIREMENT = 'requirement' - const displayCustomOptions = values.entryType === CUSTOM const displayRequirementOptions = values.entryType === REQUIREMENT @@ -158,14 +183,8 @@ const EntryType = ({ hasCustomRequirementOptions }) => { component={RadioGroup} name="requirement" options={ - hasCustomRequirementOptions - ? [ - { - display: 'Custom information requirement', - code: 'custom' - }, - ...requirementOptions - ] + !R.isEmpty(customInfoRequirementOptions) + ? updateRequirementOptions(requirementOptions) : requirementOptions } labelClassName={classes.label} @@ -178,18 +197,68 @@ const EntryType = ({ hasCustomRequirementOptions }) => { ) } -const CustomData = ({ selectedValues }) => { +const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => { + const classes = useStyles() + + const typeOfEntrySelected = selectedValues?.entryType 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 ( <> -

{`Custom ${dataTypeSelected} entry`}

+

{title}

- {customElements[dataTypeSelected].options.map(({ display, code }) => ( - - ))} - {upload && } + {isCustomInfoRequirement && ( + { + // dispatch({ type: 'form', form: it }) + }} + /> + )} + {!upload && + !isCustomInfoRequirement && + elements.options.map(({ label, name }) => ( + + ))} + {upload && ( + + )} ) } @@ -198,20 +267,23 @@ const customElements = { text: { schema: customTextSchema, options: customTextOptions, - Component: CustomData, - initialValues: { data: '', title: '' } + Component: ManualDataEntry, + initialValues: { data: '', title: '' }, + saveType: 'customEntry' }, file: { schema: customFileSchema, options: customUploadOptions, - Component: CustomData, - initialValues: { file: '', title: '' } + Component: ManualDataEntry, + initialValues: { file: null, title: '' }, + saveType: 'customEntryUpload' }, image: { schema: customImageSchema, options: customUploadOptions, - Component: CustomData, - initialValues: { image: '', title: '' } + Component: ManualDataEntry, + initialValues: { image: null, title: '' }, + saveType: 'customEntryUpload' } } @@ -225,7 +297,7 @@ const entryType = { // Customer data const customerDataElements = { - idScanElements: [ + idCardData: [ { name: 'firstName', label: 'First name', @@ -262,7 +334,7 @@ const customerDataElements = { component: TextInput } ], - usSsnElements: [ + usSsn: [ { name: 'usSsn', label: 'US SSN', @@ -270,12 +342,12 @@ const customerDataElements = { size: 190 } ], - idCardPhotoElements: [{ name: 'idCardPhoto' }], - frontCameraElements: [{ name: 'frontCamera' }] + idCardPhoto: [{ name: 'idCardPhoto' }], + frontCamera: [{ name: 'frontCamera' }] } -const customerDataschemas = { - idScan: Yup.object().shape({ +const customerDataSchemas = { + idCardData: Yup.object().shape({ firstName: Yup.string().required(), lastName: Yup.string().required(), documentNumber: Yup.string().required(), @@ -303,6 +375,61 @@ const customerDataschemas = { }) } +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 [key, value] = pair if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') { @@ -339,7 +466,12 @@ export { getName, entryType, customElements, + requirementElements, formatPhotosData, customerDataElements, - customerDataschemas + customerDataSchemas, + formatDates, + REQUIREMENT, + CUSTOM, + ID_CARD_DATA }