diff --git a/lib/customers.js b/lib/customers.js index e96155f9..0f765bc9 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -993,6 +993,7 @@ function addCustomField (customerId, label, value) { } }) ) + .then(res => !_.isNil(res)) } function saveCustomField (customerId, fieldId, newValue) { diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js index bdbf3a94..765f65e1 100644 --- a/lib/new-admin/graphql/types/customer.type.js +++ b/lib/new-admin/graphql/types/customer.type.js @@ -1,12 +1,6 @@ const { gql } = require('apollo-server-express') const typeDef = gql` - type CustomerCustomField { - id: ID - label: String - value: String - } - type Customer { id: ID! authorizedOverride: String @@ -86,6 +80,12 @@ const typeDef = gql` content: String } + type CustomerCustomField { + id: ID + label: String + value: String + } + type Query { customers(phone: String, name: String, address: String, id: String): [Customer] @auth customer(customerId: ID!): Customer @auth @@ -94,9 +94,9 @@ const typeDef = gql` type Mutation { setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth - addCustomField(customerId: ID!, label: String!, value: String!): CustomerCustomField @auth - saveCustomField(customerId: ID!, fieldId: ID!, value: String!): CustomerCustomField @auth - removeCustomField(customerId: ID!, fieldId: ID!): CustomerCustomField @auth + addCustomField(customerId: ID!, label: String!, value: String!): Boolean @auth + saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean @auth + removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth replacePhoto(customerId: ID!, photoType: String, newPhoto: UploadGQL): Customer @auth diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index 18a53cac..a5346a0e 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -1,6 +1,6 @@ import Grid from '@material-ui/core/Grid' 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 * as R from 'ramda' import { useState, React } from 'react' @@ -26,6 +26,7 @@ import { URI } from 'src/utils/apollo' import styles from './CustomerData.styles.js' import { EditableCard } from './components' +import { customerDataElements, customerDataschemas } from './helper.js' const useStyles = makeStyles(styles) @@ -84,8 +85,8 @@ const CustomerData = ({ R.compose(R.toLower, R.path(['customInfoRequest', 'customRequest', 'name'])) ) - const customEntries = null // get customer custom entries - const customRequirements = [] // get customer custom requirements + const customFields = [] + const customRequirements = [] const customInfoRequests = sortByName( R.path(['customInfoRequests'])(customer) ?? [] ) @@ -94,85 +95,6 @@ const CustomerData = ({ 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 = { idScan: { firstName: R.path(['firstName'])(idData) ?? '', @@ -214,7 +136,7 @@ const CustomerData = ({ const cards = [ { - fields: idScanElements, + fields: customerDataElements.idScanElements, title: 'ID Scan', titleIcon: , state: R.path(['idCardDataOverride'])(customer), @@ -226,7 +148,7 @@ const CustomerData = ({ editCustomer({ idCardData: _.merge(idData, formatDates(values)) }), - validationSchema: schemas.idScan, + validationSchema: customerDataschemas.idScan, initialValues: initialValues.idScan, isAvailable: !_.isNil(idData) }, @@ -257,7 +179,7 @@ const CustomerData = ({ isAvailable: !_.isNil(sanctions) }, { - fields: frontCameraElements, + fields: customerDataElements.frontCameraElements, title: 'Front facing camera', titleIcon: , state: R.path(['frontCameraOverride'])(customer), @@ -279,12 +201,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: schemas.frontCamera, + validationSchema: customerDataschemas.frontCamera, initialValues: initialValues.frontCamera, isAvailable: !_.isNil(customer.frontCameraPath) }, { - fields: idCardPhotoElements, + fields: customerDataElements.idCardPhotoElements, title: 'ID card image', titleIcon: , state: R.path(['idCardPhotoOverride'])(customer), @@ -304,12 +226,12 @@ const CustomerData = ({ /> ) : null, hasImage: true, - validationSchema: schemas.idCardPhoto, + validationSchema: customerDataschemas.idCardPhoto, initialValues: initialValues.idCardPhoto, isAvailable: !_.isNil(customer.idCardPhotoPath) }, { - fields: usSsnElements, + fields: customerDataElements.usSsnElements, title: 'US SSN', titleIcon: , state: R.path(['usSsnOverride'])(customer), @@ -317,7 +239,7 @@ const CustomerData = ({ reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }), save: values => editCustomer({ usSsn: values.usSsn }), deleteEditedData: () => deleteEditedData({ usSsn: null }), - validationSchema: schemas.usSsn, + validationSchema: customerDataschemas.usSsn, initialValues: initialValues.usSsn, isAvailable: !_.isNil(customer.usSsn) } @@ -374,6 +296,29 @@ const CustomerData = ({ }) }, customInfoRequests) + R.forEach(it => { + customFields.push({ + fields: [ + { + name: it.label, + label: it.label, + value: it.value ?? '', + component: TextInput + } + ], + title: it.label, + titleIcon: , + save: () => {}, + deleteEditedData: () => {}, + validationSchema: Yup.object().shape({ + [it.label]: Yup.string() + }), + initialValues: { + [it.label]: it.value ?? '' + } + }) + }, R.path(['customFields'])(customer) ?? []) + const editableCard = ( { title, @@ -444,9 +389,21 @@ const CustomerData = ({ )} - {customEntries && ( + {!_.isEmpty(customFields) && (
Custom data entry + + + {customFields.map((elem, idx) => { + return isEven(idx) ? editableCard(elem, idx) : null + })} + + + {customFields.map((elem, idx) => { + return !isEven(idx) ? editableCard(elem, idx) : null + })} + +
)} {!R.isEmpty(customRequirements) && ( diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 91b076ee..d83960d3 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -236,6 +236,21 @@ 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 GET_ACTIVE_CUSTOM_REQUESTS = gql` + query customInfoRequests($onlyEnabled: Boolean) { + customInfoRequests(onlyEnabled: $onlyEnabled) { + id + customRequest + } + } +` + const CustomerProfile = memo(() => { const history = useHistory() @@ -255,6 +270,16 @@ const CustomerProfile = memo(() => { 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 [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, { onCompleted: () => getCustomer() }) @@ -294,6 +319,17 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const saveCustomEntry = it => { + setCustomEntry({ + variables: { + customerId, + label: it.title, + value: it.data + } + }) + setWizard(null) + } + const updateCustomer = it => setCustomer({ variables: { @@ -385,6 +421,12 @@ const CustomerProfile = memo(() => { const timezone = R.path(['config', 'locale_timezone'], configResponse) + const customRequirementOptions = + activeCustomRequests?.customInfoRequests?.map(it => ({ + value: it.id, + display: it.customRequest.name + })) ?? [] + const classes = useStyles() return ( @@ -544,8 +586,11 @@ const CustomerProfile = memo(() => { {wizard && ( {}} + save={saveCustomEntry} + addPhoto={replacePhoto} + addCustomerData={editCustomer} onClose={() => setWizard(null)} + customRequirementOptions={customRequirementOptions} /> )} diff --git a/new-lamassu-admin/src/pages/Customers/Wizard.js b/new-lamassu-admin/src/pages/Customers/Wizard.js index 87bebaa8..16db1647 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 } from 'formik' +import { Form, Formik, Field } from 'formik' import * as R from 'ramda' import React, { useState, Fragment } from 'react' @@ -7,6 +7,7 @@ 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' @@ -41,6 +42,10 @@ const styles = { margin: [[0, 4, 0, 2]], borderBottom: `1px solid ${comet}`, display: 'inline-block' + }, + dropdownField: { + marginTop: 16, + minWidth: 155 } } @@ -57,7 +62,14 @@ const getStep = (step, selectedValues) => { } } -const Wizard = ({ onClose, save, error }) => { +const Wizard = ({ + onClose, + save, + error, + customRequirementOptions, + addCustomerData, + addPhoto +}) => { const classes = useStyles() const [selectedValues, setSelectedValues] = useState(null) @@ -66,6 +78,7 @@ const Wizard = ({ onClose, save, error }) => { step: 1 }) + const isCustom = values => values?.requirement === 'custom' const isLastStep = step === LAST_STEP const stepOptions = getStep(step, selectedValues) @@ -103,18 +116,34 @@ const Wizard = ({ onClose, save, error }) => { onSubmit={onContinue} initialValues={stepOptions.initialValues} validationSchema={stepOptions.schema}> -
- -
- {error && Failed to save} - -
- + {({ values }) => ( +
+ + {isCustom(values) && ( +
+ +
+ )} +
+ {error && Failed to save} + +
+ + )} diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js index 0239db8a..65507e91 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js @@ -150,7 +150,7 @@ const EditableCard = ({

{title}

- {state && ( + {state && authorize && (
@@ -279,7 +279,7 @@ const EditableCard = ({ Cancel - {authorized.label !== 'Accepted' && ( + {authorize && authorized.label !== 'Accepted' && (
)} - {authorized.label !== 'Rejected' && ( + {authorize && authorized.label !== 'Rejected' && ( { ) ?? ''}`.trim() } +// Manual Entry Wizard + const entryOptions = [ { display: 'Custom entry', code: 'custom' }, { display: 'Populate existing requirement', code: 'requirement' } ] const dataOptions = [ - { display: 'Text', code: 'text' }, - { display: 'File', code: 'file' }, - { display: 'Image', code: 'image' } + { display: 'Text', code: 'text' } + // TODO: Requires backend modifications to support File and Image + // { display: 'File', code: 'file' }, + // { display: 'Image', code: 'image' } ] const requirementOptions = [ - { display: 'Birthdate', code: 'birthdate' }, { display: 'ID card image', code: 'idCardPhoto' }, { display: 'ID data', code: 'idCardData' }, - { display: 'Customer camera', code: 'facephoto' }, - { display: 'US SSN', code: 'usSsn' } + { display: 'US SSN', code: 'usSsn' }, + { display: 'Customer camera', code: 'facephoto' } ] const customTextOptions = [ @@ -108,7 +111,7 @@ const customTextSchema = Yup.object().shape({ data: Yup.string().required() }) -const EntryType = () => { +const EntryType = ({ hasCustomRequirementOptions }) => { const classes = useStyles() const { values } = useFormikContext() @@ -154,7 +157,17 @@ const EntryType = () => { 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 mapKeys = pair => { const [key, value] = pair if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') { @@ -245,5 +339,7 @@ export { getName, entryType, customElements, - formatPhotosData + formatPhotosData, + customerDataElements, + customerDataschemas } diff --git a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js index f2eb4e78..ef092185 100644 --- a/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js +++ b/new-lamassu-admin/src/pages/Dashboard/SystemPerformance/Graphs/RefScatterplot.js @@ -62,7 +62,7 @@ const Graph = ({ data, timeFrame, timezone }) => { [] ) - const filterDay = useMemo( + const filterDay = useCallback( x => (timeFrame === 'day' ? x.getUTCHours() === 0 : x.getUTCDate() === 1), [timeFrame] ) diff --git a/new-lamassu-admin/src/pages/Triggers/helper.js b/new-lamassu-admin/src/pages/Triggers/helper.js index fcf91e71..20a11664 100644 --- a/new-lamassu-admin/src/pages/Triggers/helper.js +++ b/new-lamassu-admin/src/pages/Triggers/helper.js @@ -554,7 +554,7 @@ const Requirement = () => { } const options = enableCustomRequirement ? [...requirementOptions, customInfoOption] - : [...requirementOptions, { ...customInfoOption, disabled: true }] + : [...requirementOptions] const titleClass = { [classes.error]: (!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError)