Chore: Add basic screen and toggle

Chore: form skeleton

Feat: wizard step 1 and 2

Feat: toggle button group for formik

Feat: select input type Form and styling

Feat: text entry page

Feat: Choice list and CSS

Fix: scroll to bottom on Add choice button click

Feat: format data at end of wizard

Feat: wizard toggle button and background blur

Feat: data table for custom info requests

Feat: editing and deleting custom info request

Feat: add icons

Fix: Wizard changes

Feat: custom requests migrations

Feat: fetch custom info requests

Feat: add mutations

Feat: add custom request option in trigger wizard

Feat: show customrequests on table

Feat: Triggers page code refactor

Feat: integrate custom info requests on Customer graphql type

Feat: Show custom info requests on user page

Fix: use normal table instead of datatable

Feat: modal for custom information request details

Feat: poller returns custom request information details

Feat: send customer custom info requests to machine

Chore: add field CustomInfoRequestsData on customer updates

Feat: customer custom info request data saving

Chore: variable name changes and lots of fixes

Feat: remove default value in query, sort request on customer profile

Signed-off-by: csrapr <26280794+csrapr@users.noreply.github.com>

Fix: return promise when array of ids is empty

Feat: TitleSection can receive more than one button
This commit is contained in:
csrapr 2021-04-16 18:03:53 +01:00 committed by Sérgio Salgado
parent 3de2bb3d86
commit ba8cac60f8
48 changed files with 2424 additions and 146 deletions

View file

@ -43,6 +43,7 @@ function add (customer) {
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(getCustomInfoRequestsData)
.then(camelize)
}
@ -61,6 +62,7 @@ function get (phone) {
const sql = 'select * from customers where phone=$1'
return db.oneOrNone(sql, [phone])
.then(populateDailyVolume)
.then(getCustomInfoRequestsData)
.then(camelize)
}
@ -90,6 +92,7 @@ function update (id, data, userToken, txId) {
.then(populateOverrideUsernames)
.then(computeStatus)
.then((it) => populateDailyVolume(it, txId))
.then(getCustomInfoRequestsData)
.then(camelize)
}
@ -315,6 +318,7 @@ function getById (id, userToken) {
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(getCustomInfoRequestsData)
.then(camelize)
}
@ -606,6 +610,7 @@ function batch () {
return populateOverrideUsernames(customer)
.then(computeStatus)
.then(populateDailyVolume)
.then(getCustomInfoRequestsData)
.then(camelize)
}, customers)))
}
@ -1001,6 +1006,12 @@ function removeCustomField (customerId, fieldId) {
}))
}
function getCustomInfoRequestsData (customer) {
if (!customer) return
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1`
return db.any(sql, [customer.id]).then(res => _.set('custom_info_request_data', res, customer))
}
module.exports = {
add,
get,

View file

@ -0,0 +1,28 @@
const queries = require('../../services/customInfoRequests')
const DataLoader = require('dataloader')
const customerCustomInfoRequestsLoader = new DataLoader(ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), { cache: false })
const customInfoRequestLoader = new DataLoader(ids => queries.batchGetCustomInfoRequest(ids), { cache: false })
const resolvers = {
Customer: {
customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id)
},
CustomRequestData: {
customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId)
},
Query: {
customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled),
customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId),
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId)
},
Mutation: {
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, isAuthorized }]) => queries.setAuthorizedCustomRequest(customerId, infoRequestId, isAuthorized)
}
}
module.exports = resolvers

View file

@ -6,6 +6,7 @@ const cashbox = require('./cashbox.resolver')
const config = require('./config.resolver')
const currency = require('./currency.resolver')
const customer = require('./customer.resolver')
const customInfoRequests = require('./customInfoRequests.resolver')
const funding = require('./funding.resolver')
const log = require('./log.resolver')
const loyalty = require('./loyalty.resolver')
@ -28,6 +29,7 @@ const resolvers = [
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,

View file

@ -0,0 +1,54 @@
const { gql } = require('apollo-server-express')
const typeDef = gql`
type CustomInfoRequest {
id: ID!,
enabled: Boolean,
customRequest: JSON
}
input CustomRequestInputField {
choiceList: [String]
constraintType: String
type: String
numDigits: String
label1: String
label2: String
}
input CustomRequestInputScreen {
text: String
title: String
}
input CustomRequestInput {
name: String
input: CustomRequestInputField
screen1: CustomRequestInputScreen
screen2: CustomRequestInputScreen
}
type CustomRequestData {
customerId: ID
infoRequestId: ID
approved: Boolean
customerData: JSON
customInfoRequest: CustomInfoRequest
}
type Query {
customInfoRequests(onlyEnabled: Boolean): [CustomInfoRequest] @auth
customerCustomInfoRequests(customerId: ID!): [CustomRequestData] @auth
customerCustomInfoRequest(customerId: ID!, infoRequestId: ID!): CustomRequestData @auth
}
type Mutation {
insertCustomInfoRequest(customRequest: CustomRequestInput!): CustomInfoRequest @auth
removeCustomInfoRequest(id: ID!): CustomInfoRequest @auth
editCustomInfoRequest(id: ID!, customRequest: CustomRequestInput!): CustomInfoRequest @auth
setAuthorizedCustomRequest(customerId: ID!, infoRequestId: ID!, isAuthorized: Boolean!): Boolean @auth
}
`
module.exports = typeDef

View file

@ -40,6 +40,7 @@ const typeDef = gql`
transactions: [Transaction]
subscriberInfo: JSONObject
customFields: [CustomerCustomField]
customInfoRequests: [CustomRequestData]
}
input CustomerInput {

View file

@ -6,6 +6,7 @@ const cashbox = require('./cashbox.type')
const config = require('./config.type')
const currency = require('./currency.type')
const customer = require('./customer.type')
const customInfoRequests = require('./customInfoRequests.type')
const funding = require('./funding.type')
const log = require('./log.type')
const loyalty = require('./loyalty.type')
@ -28,6 +29,7 @@ const types = [
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,

View file

@ -0,0 +1,122 @@
const db = require('../../db')
const uuid = require('uuid')
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const getCustomInfoRequests = (onlyEnabled = false) => {
const sql = onlyEnabled
? `SELECT * FROM custom_info_requests WHERE enabled = true ORDER BY custom_request->>'name'`
: `SELECT * FROM custom_info_requests ORDER BY custom_request->>'name'`
return db.any(sql).then(res => {
return res.map(item => ({
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}))
})
}
const addCustomInfoRequest = (customRequest) => {
const sql = 'INSERT INTO custom_info_requests (id, custom_request) VALUES ($1, $2)'
const id = uuid.v4()
return db.none(sql, [id, customRequest]).then(() => ({ id }))
}
const removeCustomInfoRequest = (id) => {
return db.none('UPDATE custom_info_requests SET enabled = false WHERE id = $1', [id]).then(() => ({ id }))
}
const editCustomInfoRequest = (id, customRequest) => {
return db.none('UPDATE custom_info_requests SET custom_request = $1 WHERE id=$2', [customRequest, id]).then(() => ({ id, customRequest }))
}
const getAllCustomInfoRequestsForCustomer = (customerId) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1`
return db.any(sql, [customerId]).then(res => res.map(item => ({
customerId: item.customer_id,
infoRequestId: item.info_request_id,
approved: item.approved,
customerData: item.customer_data
})))
}
const getCustomInfoRequestForCustomer = (customerId, infoRequestId) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1 AND info_request_id = $2`
return db.one(sql, [customerId, infoRequestId]).then(item => {
return {
customerId: item.customer_id,
infoRequestId: item.info_request_id,
approved: item.approved,
customerData: item.customer_data
}
})
}
const batchGetAllCustomInfoRequestsForCustomer = (customerIds) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id IN ($1^)`
return db.any(sql, [_.map(pgp.as.text, customerIds).join(',')]).then(res => {
const map = _.groupBy('customer_id', res)
return customerIds.map(id => {
const items = map[id] || []
return items.map(item => ({
customerId: item.customer_id,
infoRequestId: item.info_request_id,
approved: item.approved,
customerData: item.customer_data
}))
})
})
}
const getCustomInfoRequest = (infoRequestId) => {
const sql = `SELECT * FROM custom_info_requests WHERE id = $1`
return db.one(sql, [infoRequestId]).then(item => ({
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}))
}
const batchGetCustomInfoRequest = (infoRequestIds) => {
if (infoRequestIds.length === 0) return Promise.resolve([])
const sql = `SELECT * FROM custom_info_requests WHERE id IN ($1^)`
return db.any(sql, [_.map(pgp.as.text, infoRequestIds).join(',')]).then(res => {
const map = _.groupBy('id', res)
return infoRequestIds.map(id => {
const item = map[id][0] // since id is primary key the array always has 1 element
return {
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}
})
})
}
const setAuthorizedCustomRequest = (customerId, infoRequestId, isAuthorized) => {
const sql = `UPDATE customers_custom_info_requests SET approved = $1 WHERE customer_id = $2 AND info_request_id = $3`
return db.none(sql, [isAuthorized, customerId, infoRequestId]).then(() => true)
}
const setCustomerData = (customerId, infoRequestId, data) => {
const sql = `
INSERT INTO customers_custom_info_requests (customer_id, info_request_id, customer_data)
VALUES ($1, $2, $3)
ON CONFLICT (customer_id, info_request_id)
DO UPDATE SET customer_data = $3, approved = null`
return db.none(sql, [customerId, infoRequestId, data])
}
module.exports = {
getCustomInfoRequests,
addCustomInfoRequest,
removeCustomInfoRequest,
editCustomInfoRequest,
getAllCustomInfoRequestsForCustomer,
getCustomInfoRequestForCustomer,
batchGetAllCustomInfoRequestsForCustomer,
getCustomInfoRequest,
batchGetCustomInfoRequest,
setAuthorizedCustomRequest,
setCustomerData
}

View file

@ -18,6 +18,13 @@ const { getTx } = require('../new-admin/services/transactions.js')
const { getCustomerById } = require('../customers')
const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
function updateCustomerCustomInfoRequest (customerId, dataToSave, req, res) {
return customInfoRequestQueries.setCustomerData(customerId, dataToSave.info_request_id, dataToSave)
.then(() => customers.getById(customerId))
.then(customer => respond(req, res, { customer }))
}
function updateCustomer (req, res, next) {
const id = req.params.id
@ -27,6 +34,10 @@ function updateCustomer (req, res, next) {
const triggers = configManager.getTriggers(req.settings.config)
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
if (patch.customRequestPatch) {
return updateCustomerCustomInfoRequest(id, patch.dataToSave, req, res).catch(next)
}
customers.getById(id)
.then(customer => {
if (!customer) { throw httpError('Not Found', 404) }

View file

@ -10,6 +10,7 @@ const plugins = require('../plugins')
const semver = require('semver')
const state = require('../middlewares/state')
const version = require('../../package.json').version
const customRequestQueries = require('../new-admin/services/customInfoRequests')
const urlsToPing = [
`us.archive.ubuntu.com`,
@ -37,6 +38,24 @@ const createTerms = terms => (terms.active && terms.text) ? ({
cancel: terms.cancelButtonText
}) : null
const buildTriggers = (allTriggers) => {
const normalTriggers = []
const customTriggers = _.filter(o => {
if (o.customInfoRequestId === '') normalTriggers.push(o)
return o.customInfoRequestId !== ''
}, allTriggers)
return _.flow([_.map(_.get('customInfoRequestId')), customRequestQueries.batchGetCustomInfoRequest])(customTriggers)
.then(res => {
res.forEach((details, index) => {
// make sure we aren't attaching the details to the wrong trigger
if (customTriggers[index].customInfoRequestId !== details.id) return
customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
})
return [...normalTriggers, ...customTriggers]
})
}
function poll (req, res, next) {
const machineVersion = req.query.version
const machineModel = req.query.model
@ -54,8 +73,8 @@ function poll (req, res, next) {
const pi = plugins(settings, deviceId)
const hasLightning = checkHasLightning(settings)
const triggers = configManager.getTriggers(settings.config)
const triggersAutomation = configManager.getTriggersAutomation(settings.config)
const triggersPromise = buildTriggers(configManager.getTriggers(settings.config))
const operatorInfo = configManager.getOperatorInfo(settings.config)
const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName }
@ -65,8 +84,8 @@ function poll (req, res, next) {
state.pids[operatorId] = { [deviceId]: { pid, ts: Date.now() } }
return pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel)
.then(results => {
return Promise.all([pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel), triggersPromise])
.then(([results, triggers]) => {
const cassettes = results.cassettes
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid

View file

@ -0,0 +1,24 @@
const db = require('./db')
exports.up = function (next) {
const sql = [
`CREATE TABLE custom_info_requests(
id UUID PRIMARY KEY,
enabled BOOLEAN NOT NULL DEFAULT true,
custom_request JSONB
);
CREATE TABLE customers_custom_info_requests(
customer_id UUID REFERENCES customers,
info_request_id UUID REFERENCES custom_info_requests,
approved BOOLEAN,
customer_data JSONB NOT NULL,
PRIMARY KEY(customer_id, info_request_id)
);`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -10,15 +10,21 @@ import subpageButtonStyles from './SubpageButton.styles'
const useStyles = makeStyles(subpageButtonStyles)
const SubpageButton = memo(
({ className, Icon, InverseIcon, toggle, children }) => {
({
className,
Icon,
InverseIcon,
toggle,
forceDisable = false,
children
}) => {
const [active, setActive] = useState(false)
const isActive = forceDisable ? false : active
const classes = useStyles()
const classNames = {
[classes.button]: true,
[classes.normalButton]: !active,
[classes.activeButton]: active
[classes.normalButton]: !isActive,
[classes.activeButton]: isActive
}
const normalButton = <Icon className={classes.buttonIcon} />
@ -42,7 +48,8 @@ const SubpageButton = memo(
)
const innerToggle = () => {
const newActiveState = !active
forceDisable = false
const newActiveState = !isActive
toggle(newActiveState)
setActive(newActiveState)
}
@ -51,7 +58,7 @@ const SubpageButton = memo(
<button
className={classnames(classNames, className)}
onClick={innerToggle}>
{active ? activeButton : normalButton}
{isActive ? activeButton : normalButton}
</button>
)
}

View file

@ -0,0 +1,28 @@
import FormControl from '@material-ui/core/FormControl'
import InputLabel from '@material-ui/core/InputLabel'
import MenuItem from '@material-ui/core/MenuItem'
import Select from '@material-ui/core/Select'
import classnames from 'classnames'
import React from 'react'
const Dropdown = ({ label, name, options, onChange, value, className }) => {
return (
<FormControl className={classnames(className)}>
<InputLabel>{label}</InputLabel>
<Select
autoWidth={true}
labelId={label}
id={name}
value={value}
onChange={onChange}>
{options.map((option, index) => (
<MenuItem key={index} value={option.value}>
{option.display}
</MenuItem>
))}
</Select>
</FormControl>
)
}
export default Dropdown

View file

@ -8,13 +8,18 @@ import classnames from 'classnames'
import React from 'react'
import { Label1 } from 'src/components/typography'
import { offColor } from 'src/styling/variables'
const styles = {
label: {
height: 16,
lineHeight: '16px',
margin: [[0, 0, 4, 0]],
paddingLeft: 3
},
subtitle: {
marginTop: -8,
marginLeft: 32,
color: offColor
}
}
@ -31,7 +36,6 @@ const RadioGroup = ({
radioClassName
}) => {
const classes = useStyles()
return (
<>
{label && <Label1 className={classes.label}>{label}</Label1>}
@ -41,13 +45,20 @@ const RadioGroup = ({
onChange={onChange}
className={classnames(className)}>
{options.map((option, idx) => (
<React.Fragment key={idx}>
<div>
<FormControlLabel
key={idx}
disabled={option.disabled}
value={option.code}
control={<Radio className={radioClassName} />}
label={option.display}
className={classnames(labelClassName)}
/>
{option.subtitle && (
<Label1 className={classes.subtitle}>{option.subtitle}</Label1>
)}
</div>
</React.Fragment>
))}
</MRadioGroup>
</>

View file

@ -0,0 +1,75 @@
import { makeStyles } from '@material-ui/core'
import { ToggleButtonGroup as MUIToggleButtonGroup } from '@material-ui/lab'
import ToggleButton from '@material-ui/lab/ToggleButton'
import React from 'react'
import { H4, P } from 'src/components/typography'
import { backgroundColor, comet } from 'src/styling/variables'
const styles = {
noTextTransform: {
textTransform: 'none'
},
flex: {
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
width: '90%',
overflow: 'hidden',
maxHeight: 80
},
buttonTextContent: {
marginLeft: 32,
textTransform: 'none',
textAlign: 'left'
},
button: {
backgroundColor: backgroundColor,
marginBottom: 16
},
paragraph: {
color: comet,
marginTop: -10
}
}
const useStyles = makeStyles(styles)
const ToggleButtonGroup = ({
name,
orientation = 'vertical',
value,
exclusive = true,
onChange,
size = 'small',
...props
}) => {
const classes = useStyles()
return (
<MUIToggleButtonGroup
size={size}
name={name}
orientation={orientation}
value={value}
exclusive={exclusive}
onChange={onChange}>
{props.options.map(option => {
return (
<ToggleButton
className={classes.button}
value={option.value}
aria-label={option.value}
key={option.value}>
<div className={classes.flex}>
<option.icon />
<div className={classes.buttonTextContent}>
<H4>{option.title}</H4>
<P className={classes.paragraph}> {option.description}</P>
</div>
</div>
</ToggleButton>
)
})}
</MUIToggleButtonGroup>
)
}
export default ToggleButtonGroup

View file

@ -1,12 +1,13 @@
import Autocomplete from './Autocomplete'
import Checkbox from './Checkbox'
import CodeInput from './CodeInput'
import Dropdown from './Dropdown'
import NumberInput from './NumberInput'
import RadioGroup from './RadioGroup'
import SecretInput from './SecretInput'
import Switch from './Switch'
import TextInput from './TextInput'
import ToggleButtonGroup from './ToggleButtonGroup'
export {
Checkbox,
CodeInput,
@ -15,5 +16,7 @@ export {
Switch,
SecretInput,
RadioGroup,
Autocomplete
Autocomplete,
ToggleButtonGroup,
Dropdown
}

View file

@ -0,0 +1,25 @@
import React, { memo } from 'react'
import { Dropdown } from '../base'
const RadioGroupFormik = memo(({ label, ...props }) => {
const { name, value } = props.field
const { setFieldValue } = props.form
return (
<Dropdown
name={name}
label={label}
value={value}
options={props.options}
ariaLabel={name}
onChange={e => {
setFieldValue(name, e.target.value)
props.resetError && props.resetError()
}}
className={props.className}
{...props}
/>
)
})
export default RadioGroupFormik

View file

@ -0,0 +1,27 @@
import React, { memo } from 'react'
import { ToggleButtonGroup } from '../base'
const ToggleButtonGroupFormik = memo(({ enforceValueSet = true, ...props }) => {
const { name, value } = props.field
const { setFieldValue } = props.form
return (
<ToggleButtonGroup
name={name}
value={value}
options={props.options}
ariaLabel={name}
onChange={(e, value) => {
// enforceValueSet prevents you from not having any button selected
// after selecting one the first time
if (enforceValueSet && !value) return null
setFieldValue(name, value)
props.resetError && props.resetError()
}}
className={props.className}
{...props}
/>
)
})
export default ToggleButtonGroupFormik

View file

@ -1,6 +1,7 @@
import Autocomplete from './Autocomplete'
import CashCassetteInput from './CashCassetteInput'
import Checkbox from './Checkbox'
import Dropdown from './Dropdown'
import NumberInput from './NumberInput'
import RadioGroup from './RadioGroup'
import SecretInput from './SecretInput'
@ -13,5 +14,6 @@ export {
NumberInput,
SecretInput,
RadioGroup,
CashCassetteInput
CashCassetteInput,
Dropdown
}

View file

@ -16,7 +16,7 @@ const TitleSection = ({
title,
error,
labels,
button,
buttons = [],
children,
appendix,
appendixClassName
@ -30,14 +30,20 @@ const TitleSection = ({
{error && (
<ErrorMessage className={classes.error}>Failed to save</ErrorMessage>
)}
{button && (
{buttons.length > 0 && (
<>
{buttons.map((button, idx) => (
<SubpageButton
key={idx}
className={classes.subpageButton}
Icon={button.icon}
InverseIcon={button.inverseIcon}
toggle={button.toggle}>
toggle={button.toggle}
forceDisable={button.forceDisable}>
<Info1 className={classes.buttonText}>{button.text}</Info1>
</SubpageButton>
))}
</>
)}
</div>
<Box display="flex" flexDirection="row">

View file

@ -109,12 +109,14 @@ const Commissions = ({ name: SCREEN_KEY }) => {
<TitleSection
title="Commissions"
labels={labels}
button={{
buttons={[
{
text: 'List view',
icon: ListingViewIcon,
inverseIcon: ReverseListingViewIcon,
toggle: setShowMachines
}}
}
]}
iconClassName={classes.listViewButton}
/>

View file

@ -61,7 +61,8 @@ const CustomerData = ({
customer,
updateCustomer,
editCustomer,
deleteEditedData
deleteEditedData,
updateCustomRequest
}) => {
const classes = useStyles()
const [listView, setListView] = useState(false)
@ -79,8 +80,15 @@ const CustomerData = ({
? 'Passed'
: 'Failed'
const sortByName = R.sortBy(
R.compose(R.toLower, R.path(['customInfoRequest', 'customRequest', 'name']))
)
const customEntries = null // get customer custom entries
const customRequirements = null // get customer custom requirements
const customInfoRequests = sortByName(
R.path(['customInfoRequests'])(customer) ?? []
)
const isEven = elem => elem % 2 === 0
@ -291,6 +299,36 @@ const CustomerData = ({
}
]
R.forEach(it => {
cards.push({
data: [
{
value: it.customerData.data,
component: TextInput
}
],
title: it.customInfoRequest.customRequest.name,
titleIcon: <CardIcon className={classes.cardIcon} />,
authorize: () =>
updateCustomRequest({
variables: {
customerId: it.customerId,
infoRequestId: it.customInfoRequest.id,
isAuthorized: true
}
}),
reject: () =>
updateCustomRequest({
variables: {
customerId: it.customerId,
infoRequestId: it.customInfoRequest.id,
isAuthorized: false
}
}),
save: () => {}
})
}, customInfoRequests)
const editableCard = (
{
title,

View file

@ -83,6 +83,16 @@ const GET_CUSTOMER = gql`
txCustomerPhotoAt
txCustomerPhotoPath
}
customInfoRequests {
customerId
approved
customerData
customInfoRequest {
id
enabled
customRequest
}
}
}
}
`
@ -156,6 +166,20 @@ const DELETE_EDITED_CUSTOMER = gql`
}
`
const SET_AUTHORIZED_REQUEST = gql`
mutation setAuthorizedCustomRequest(
$customerId: ID!
$infoRequestId: ID!
$isAuthorized: Boolean!
) {
setAuthorizedCustomRequest(
customerId: $customerId
infoRequestId: $infoRequestId
isAuthorized: $isAuthorized
)
}
`
const CustomerProfile = memo(() => {
const history = useHistory()
@ -187,6 +211,9 @@ const CustomerProfile = memo(() => {
const [setCustomer] = useMutation(SET_CUSTOMER, {
onCompleted: () => getCustomer()
})
const [updateCustomRequest] = useMutation(SET_AUTHORIZED_REQUEST, {
onCompleted: () => getCustomer()
})
const updateCustomer = it =>
setCustomer({
@ -375,7 +402,8 @@ const CustomerProfile = memo(() => {
updateCustomer={updateCustomer}
replacePhoto={replacePhoto}
editCustomer={editCustomer}
deleteEditedData={deleteEditedData}></CustomerData>
deleteEditedData={deleteEditedData}
updateCustomRequest={updateCustomRequest}></CustomerData>
</div>
)}
</div>

View file

@ -0,0 +1,195 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import { MainStatus } from 'src/components/Status'
import { ActionButton } from 'src/components/buttons'
import {
Table,
THead,
Th,
Tr,
Td,
TBody
} from 'src/components/fake-table/Table'
import { H3, Label1 } from 'src/components/typography'
import { ReactComponent as AuthorizeReversedIcon } from 'src/styling/icons/button/authorize/white.svg'
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
import { ReactComponent as RejectReversedIcon } from 'src/styling/icons/button/cancel/white.svg'
import { ReactComponent as RejectIcon } from 'src/styling/icons/button/cancel/zodiac.svg'
import { ReactComponent as LinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { white, disabledColor } from 'src/styling/variables'
import DetailsCard from '../../Triggers/CustomInfoRequests/DetailsCard'
const styles = {
white: {
color: white
},
actionButton: {
display: 'flex',
height: 28,
marginRight: 'auto'
},
flex: {
display: 'flex'
},
disabledBtn: {
backgroundColor: disabledColor,
'&:hover': {
backgroundColor: disabledColor
}
},
linkIcon: {
marginTop: 12,
marginLeft: 4,
cursor: 'pointer'
}
}
const SET_AUTHORIZED_REQUEST = gql`
mutation setAuthorizedCustomRequest(
$customerId: ID!
$infoRequestId: ID!
$isAuthorized: Boolean!
) {
setAuthorizedCustomRequest(
customerId: $customerId
infoRequestId: $infoRequestId
isAuthorized: $isAuthorized
)
}
`
const useStyles = makeStyles(styles)
const CustomInfoRequestsData = ({ data }) => {
const classes = useStyles()
const [toView, setToView] = useState(null)
const [setAuthorized] = useMutation(SET_AUTHORIZED_REQUEST, {
onError: () => console.error('Error while clearing notification'),
refetchQueries: () => ['customer']
})
const authorize = it =>
setAuthorized({
variables: {
customerId: it.customerId,
infoRequestId: it.customInfoRequest.id,
isAuthorized: true
}
})
const reject = it =>
setAuthorized({
variables: {
customerId: it.customerId,
infoRequestId: it.customInfoRequest.id,
isAuthorized: false
}
})
const getBtnClasses = (it, isAuthorize) => {
return {
[classes.actionButton]: true,
[classes.disabledBtn]:
(isAuthorize && it.approved === true) ||
(!isAuthorize && it.approved === false)
}
}
const AuthorizeButton = it => (
<ActionButton
className={classnames(getBtnClasses(it, true))}
color="secondary"
Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon}
onClick={() => authorize(it)}>
Authorize
</ActionButton>
)
const RejectButton = it => (
<ActionButton
className={classnames(getBtnClasses(it, false))}
color="secondary"
Icon={RejectIcon}
InverseIcon={RejectReversedIcon}
onClick={() => reject(it)}>
Reject
</ActionButton>
)
const getActionButtons = it => {
return (
<>
{AuthorizeButton(it)}
{RejectButton(it)}
</>
)
}
const getAuthorizedStatus = it => {
return it.approved === null
? { label: 'Pending', type: 'neutral' }
: it.approved === false
? { label: 'Rejected', type: 'error' }
: { label: 'Accepted', type: 'success' }
}
return (
<>
<H3>Custom Info Requests Data</H3>
<div>
<Table>
<THead>
<Th width={250}>Custom Request Name</Th>
<Th width={500}>Custom Request Data</Th>
<Th width={200}>Status</Th>
<Th width={250} textAlign="center">
Actions
</Th>
</THead>
<TBody>
{data.map((it, idx) => (
<React.Fragment key={idx}>
<Tr>
<Td size="sm" width={250}>
<div className={classes.flex}>
<Label1>{it.customInfoRequest.customRequest.name}</Label1>
<div onClick={() => setToView(it)}>
<LinkIcon className={classes.linkIcon} />
</div>
</div>
</Td>
<Td size="sm" width={500}>
<div>{JSON.stringify(it.customerData.data, null, 2)}</div>
</Td>
<Td size="sm" width={200}>
<MainStatus statuses={[getAuthorizedStatus(it)]} />
</Td>
<Td size="sm" width={250}>
<div className={classes.flex}>{getActionButtons(it)}</div>
</Td>
</Tr>
</React.Fragment>
))}
</TBody>
</Table>
{toView && (
<Modal
width={900}
height={400}
open={true}
handleClose={() => setToView(null)}>
<H3>Custom Information Request Details</H3>
<DetailsCard it={{ ...toView.customInfoRequest }} />
</Modal>
)}
</div>
</>
)
}
export default CustomInfoRequestsData

View file

@ -0,0 +1,249 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog'
import { IconButton, Button, Link } from 'src/components/buttons'
import DataTable from 'src/components/tables/DataTable'
import { Info1, Info3 } from 'src/components/typography'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import styles from './CustomInfoRequests.styles'
import DetailsRow from './DetailsCard'
import Wizard from './Wizard'
const useStyles = makeStyles(styles)
const inputTypeDisplay = {
numerical: 'Numerical',
text: 'Text',
choiceList: 'Choice list'
}
const constraintTypeDisplay = {
date: 'Date',
none: 'None',
email: 'Email',
length: 'Length',
selectOne: 'Select one',
selectMultiple: 'Select multiple',
spaceSeparation: 'Space separation'
}
const ADD_ROW = gql`
mutation insertCustomInfoRequest($customRequest: CustomRequestInput!) {
insertCustomInfoRequest(customRequest: $customRequest) {
id
}
}
`
const EDIT_ROW = gql`
mutation editCustomInfoRequest(
$id: ID!
$customRequest: CustomRequestInput!
) {
editCustomInfoRequest(id: $id, customRequest: $customRequest) {
id
}
}
`
const REMOVE_ROW = gql`
mutation removeCustomInfoRequest($id: ID!) {
removeCustomInfoRequest(id: $id) {
id
}
}
`
const CustomInfoRequests = ({
showWizard,
toggleWizard,
data: customRequests
}) => {
const classes = useStyles()
const [toBeDeleted, setToBeDeleted] = useState()
const [toBeEdited, setToBeEdited] = useState()
const [deleteDialog, setDeleteDialog] = useState(false)
const [hasError, setHasError] = useState(false)
const [addEntry] = useMutation(ADD_ROW, {
onError: () => {
console.log('Error while adding custom info request')
setHasError(true)
},
onCompleted: () => {
setHasError(false)
toggleWizard()
},
refetchQueries: () => ['customInfoRequests']
})
const [editEntry] = useMutation(EDIT_ROW, {
onError: () => {
console.log('Error while editing custom info request')
setHasError(true)
},
onCompleted: () => {
setHasError(false)
setToBeEdited(null)
toggleWizard()
},
refetchQueries: () => ['customInfoRequests']
})
const [removeEntry] = useMutation(REMOVE_ROW, {
onError: () => {
console.log('Error while removing custom info request')
setHasError(true)
},
onCompleted: () => {
setDeleteDialog(false)
setHasError(false)
},
refetchQueries: () => ['customInfoRequests']
})
const handleDelete = id => {
removeEntry({
variables: {
id
}
})
}
const handleSave = (values, isEditing) => {
if (isEditing) {
return editEntry({
variables: {
id: values.id,
customRequest: R.omit(['id'])(values)
}
})
}
return addEntry({
variables: {
customRequest: {
...values
}
}
})
}
return (
<>
{customRequests.length > 0 && (
<DataTable
emptyText="No custom info requests so far"
elements={[
{
header: 'Requirement name',
width: 300,
textAlign: 'left',
size: 'sm',
view: it => it.customRequest.name
},
{
header: 'Data entry type',
width: 300,
textAlign: 'left',
size: 'sm',
view: it => inputTypeDisplay[it.customRequest.input.type]
},
{
header: 'Constraints',
width: 300,
textAlign: 'left',
size: 'sm',
view: it =>
constraintTypeDisplay[it.customRequest.input.constraintType]
},
{
header: 'Edit',
width: 100,
textAlign: 'center',
size: 'sm',
view: it => {
return (
<IconButton
onClick={() => {
setToBeEdited(it)
return toggleWizard()
}}>
<EditIcon />
</IconButton>
)
}
},
{
header: 'Delete',
width: 100,
textAlign: 'center',
size: 'sm',
view: it => {
return (
<IconButton
onClick={() => {
setToBeDeleted(it.id)
return setDeleteDialog(true)
}}>
<DeleteIcon />
</IconButton>
)
}
}
]}
data={customRequests}
Details={DetailsRow}
expandable
rowSize="sm"
/>
)}
{!customRequests.length && (
<div className={classes.centerItems}>
<Info1 className={classnames(classes.m0, classes.mb10)}>
It seems you haven't added any custom information requests yet.
</Info1>
<Info3 className={classnames(classes.m0, classes.mb10)}>
Please read our{' '}
<a href="https://support.lamassu.is/hc/en-us/sections/115000817232-Compliance">
<Link>Support Article</Link>
</a>{' '}
on Compliance before adding new information requests.
</Info3>
<Button onClick={() => toggleWizard()}>
Add custom information request
</Button>
</div>
)}
{showWizard && (
<Wizard
hasError={hasError}
onClose={() => {
setToBeEdited(null)
setHasError(false)
toggleWizard()
}}
toBeEdited={toBeEdited}
onSave={(...args) => handleSave(...args)}
/>
)}
<DeleteDialog
errorMessage={hasError ? 'Failed to delete' : ''}
open={deleteDialog}
onDismissed={() => {
setDeleteDialog(false)
setHasError(false)
}}
onConfirmed={() => handleDelete(toBeDeleted)}
/>
</>
)
}
export default CustomInfoRequests

View file

@ -0,0 +1,19 @@
export default {
m0: {
margin: 0
},
mb10: {
marginBottom: 10
},
centerItems: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '50%',
justifyContent: 'center'
},
alignWithTitleSection: {
marginTop: -47,
display: 'flex'
}
}

View file

@ -0,0 +1,112 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import React from 'react'
import { Label1, Info2 } from 'src/components/typography'
const styles = {
flex: {
display: 'flex'
},
column: {
flexDirection: 'column'
},
halfWidth: {
width: '50%',
marginBottom: 15,
marginRight: 50
},
marginTop: {
marginTop: 20
},
marginBottom: {
marginBottom: 20
}
}
const useStyles = makeStyles(styles)
const DetailsCard = ({ it }) => {
const customRequest = it.customRequest
const classes = useStyles()
const getScreen2Data = () => {
const label1Display =
customRequest.input.constraintType === 'spaceSeparation'
? 'First word label'
: 'Text entry label'
switch (customRequest.input.type) {
case 'text':
return (
<>
<div className={classes.halfWidth}>
<Info2>{label1Display}</Info2>
<Label1>{customRequest.input.label1}</Label1>
</div>
{customRequest.input.constraintType === 'spaceSeparation' && (
<div className={classes.halfWidth}>
<Info2>Second word label</Info2>
<Label1>{customRequest.input.label2}</Label1>
</div>
)}
</>
)
default:
return (
<>
<div className={classes.halfWidth}>
<Info2>Screen 2 input title</Info2>
<Label1>{customRequest.screen2.title}</Label1>
</div>
<div className={classes.halfWidth}>
<Info2>Screen 2 input description</Info2>
<Label1>{customRequest.screen2.text}</Label1>
</div>
</>
)
}
}
const getInputData = () => {
return (
<>
{customRequest.input.choiceList && (
<>
<Info2>Choices</Info2>
{customRequest.input.choiceList.map((choice, idx) => {
return <Label1 key={idx}>{choice}</Label1>
})}
</>
)}
{customRequest.input.numDigits && (
<>
<Info2>Number of digits</Info2>
<Label1>{customRequest.input.numDigits}</Label1>
</>
)}
</>
)
}
return (
<div>
<div className={classnames(classes.flex, classes.row, classes.marginTop)}>
<div className={classes.halfWidth}>
<Info2>Screen 1 title</Info2>
<Label1>{customRequest.screen1.title}</Label1>
</div>
<div className={classnames(classes.halfWidth, classes.flex)}>
{getScreen2Data()}
</div>
</div>
<div
className={classnames(classes.flex, classes.row, classes.marginBottom)}>
<div className={classes.halfWidth}>
<Info2>Screen 1 text</Info2>
<Label1>{customRequest.screen1.text}</Label1>
</div>
<div className={classes.halfWidth}>{getInputData()}</div>
</div>
</div>
)
}
export default DetailsCard

View file

@ -0,0 +1,76 @@
import { Field } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import ToggleButtonGroup from 'src/components/inputs/formik/ToggleButtonGroup'
import { H4 } from 'src/components/typography'
import { ReactComponent as Keyboard } from 'src/styling/icons/compliance/keyboard.svg'
import { ReactComponent as Keypad } from 'src/styling/icons/compliance/keypad.svg'
import { ReactComponent as List } from 'src/styling/icons/compliance/list.svg'
import { zircon } from 'src/styling/variables'
const MakeIcon = IconSvg => (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: zircon,
borderRadius: 4,
maxWidth: 104,
maxHeight: 64,
minWidth: 104,
minHeight: 64
}}>
<IconSvg style={{ maxWidth: 80 }} />
</div>
)
const ChooseType = () => {
const options = [
{
value: 'numerical',
title: 'Numerical entry',
description:
'User will enter information with a keypad. Good for dates, ID numbers, etc.',
icon: () => MakeIcon(Keypad)
},
{
value: 'text',
title: 'Text entry',
description:
'User will entry information with a keyboard. Good for names, email, address, etc.',
icon: () => MakeIcon(Keyboard)
},
{
value: 'choiceList',
title: 'Choice list',
description: 'Gives user multiple options to choose from.',
icon: () => MakeIcon(List)
}
]
return (
<>
<H4>Choose the type of data entry</H4>
<Field
name="inputType"
component={ToggleButtonGroup}
orientation="vertical"
exclusive
options={options}
/>
</>
)
}
const validationSchema = Yup.object().shape({
inputType: Yup.string().required()
})
const defaultValues = {
inputType: ''
}
export default ChooseType
export { validationSchema, defaultValues }

View file

@ -0,0 +1,37 @@
import { Field } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import { H4, P } from 'src/components/typography'
const NameOfRequirement = () => {
return (
<>
<H4>Name of the requirement</H4> {/* TODO Add ? icon */}
<P>
The name of the requirement will only be visible to you on the dashboard
on the requirement list, as well as on the custom information request
list. The user won't see this name. Make sure to make it distinguishable
and short.
</P>
<Field
component={TextInputFormik}
label="Requirement name"
name="requirementName"
fullWidth
/>
</>
)
}
const validationSchema = Yup.object().shape({
requirementName: Yup.string().required()
})
const defaultValues = {
requirementName: ''
}
export default NameOfRequirement
export { validationSchema, defaultValues }

View file

@ -0,0 +1,45 @@
import { Field } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import { H4, P } from 'src/components/typography'
const Screen1Information = () => {
return (
<>
<H4>Screen 1 Information</H4> {/* TODO Add ? icon */}
<P>
On screen 1 you will request the user if he agrees on providing this
information, or if he wishes to terminate the transaction instead.
</P>
<Field
component={TextInputFormik}
label="Screen title"
name="screen1Title"
fullWidth
/>
<Field
component={TextInputFormik}
label="Screen text"
name="screen1Text"
multiline
fullWidth
rows={5}
/>
</>
)
}
const validationSchema = Yup.object().shape({
screen1Title: Yup.string().required(),
screen1Text: Yup.string().required()
})
const defaultValues = {
screen1Title: '',
screen1Text: ''
}
export default Screen1Information
export { validationSchema, defaultValues }

View file

@ -0,0 +1,43 @@
import { Field } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import TextInputFormik from 'src/components/inputs/formik/TextInput'
import { H4, P } from 'src/components/typography'
const ScreenInformation = () => {
return (
<>
<H4>Screen 2 Information</H4> {/* TODO Add ? icon */}
<P>
If the user agrees, on screen 2 is where the user will enter the custom
information.
</P>
<Field
component={TextInputFormik}
label="Screen 2 input title"
name="screen2Title"
fullWidth
/>
<Field
component={TextInputFormik}
label="Screen 2 input description"
name="screen2Text"
fullWidth
/>
</>
)
}
const validationSchema = Yup.object().shape({
screen2Title: Yup.string().required(),
screen2Text: Yup.string().required()
})
const defaultValues = {
screen2Title: '',
screen2Text: ''
}
export default ScreenInformation
export { validationSchema, defaultValues }

View file

@ -0,0 +1,102 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext, FieldArray } from 'formik'
import * as R from 'ramda'
import React, { useEffect, useRef } from 'react'
import Button from 'src/components/buttons/ActionButton'
import RadioGroup from 'src/components/inputs/formik/RadioGroup'
import TextInput from 'src/components/inputs/formik/TextInput'
import { H4 } from 'src/components/typography'
import { ReactComponent as AddIconInverse } from 'src/styling/icons/button/add/white.svg'
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
import styles from './formStyles.styles'
const useStyles = makeStyles(styles)
const nonEmptyStr = obj => obj.text && obj.text.length
const options = [
{ display: 'Select just one', code: 'selectOne' },
{ display: 'Select multiple', code: 'selectMultiple' }
]
const ChoiceList = () => {
const classes = useStyles()
const context = useFormikContext()
const choiceListRef = useRef(null)
const listChoices = R.path(['values', 'listChoices'])(context) ?? []
const choiceListError = R.path(['errors', 'listChoices'])(context) ?? false
const showErrorColor = {
[classes.radioSubtitle]: true,
[classes.error]:
!R.path(['values', 'constraintType'])(context) &&
R.path(['errors', 'constraintType'])(context)
}
const hasError = choice => {
return (
choiceListError &&
R.filter(nonEmptyStr)(listChoices).length < 2 &&
choice.text.length === 0
)
}
useEffect(() => {
scrollToBottom()
}, [listChoices.length])
const scrollToBottom = () => {
choiceListRef.current?.scrollIntoView()
}
return (
<>
<H4 className={classnames(showErrorColor)}>Choice list constraints</H4>
<Field
component={RadioGroup}
options={options}
className={classes.row}
name="constraintType"
/>
<FieldArray name="listChoices">
{({ push }) => {
return (
<div className={classnames(classes.flex, classes.column)}>
<H4 className={classes.subtitle}>Choices</H4>
<div className={classes.choiceList}>
{listChoices.map((choice, idx) => {
return (
<div ref={choiceListRef} key={idx}>
<Field
className={classes.textInput}
error={hasError(choice)}
component={TextInput}
name={`listChoices[${idx}].text`}
label={`Choice ${idx + 1}`}
/>
</div>
)
})}
</div>
<Button
Icon={AddIcon}
color="primary"
InverseIcon={AddIconInverse}
className={classes.button}
onClick={e => {
e.preventDefault()
return push({ text: '' })
}}>
Add choice
</Button>
</div>
)
}}
</FieldArray>
</>
)
}
export default ChoiceList

View file

@ -0,0 +1,62 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import * as R from 'ramda'
import React from 'react'
import NumberInput from 'src/components/inputs/formik/NumberInput'
import RadioGroup from 'src/components/inputs/formik/RadioGroup'
import { TL1, H4 } from 'src/components/typography'
import styles from './formStyles.styles'
const useStyles = makeStyles(styles)
const options = [
{ display: 'None', code: 'none' },
{ display: 'Date', code: 'date' },
{ display: 'Length', code: 'length' }
]
const NumericalEntry = () => {
const classes = useStyles()
const context = useFormikContext()
const isLength =
(R.path(['values', 'constraintType'])(useFormikContext()) ?? null) ===
'length'
const showErrorColor = {
[classes.radioSubtitle]: true,
[classes.error]:
!R.path(['values', 'constraintType'])(context) &&
R.path(['errors', 'constraintType'])(context)
}
return (
<>
<H4 className={classnames(showErrorColor)}>
Numerical entry constraints
</H4>
<Field
className={classes.row}
component={RadioGroup}
options={options}
name="constraintType"
/>
{isLength && (
<div className={classnames(classes.flex, classes.numberField)}>
<Field
component={NumberInput}
name={'inputLength'}
label={'Length'}
decimalPlaces={0}
allowNegative={false}
/>
<TL1 className={classes.tl1}>digits</TL1>
</div>
)}
</>
)
}
export default NumericalEntry

View file

@ -0,0 +1,79 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import * as R from 'ramda'
import React from 'react'
import RadioGroup from 'src/components/inputs/formik/RadioGroup'
import TextInput from 'src/components/inputs/formik/TextInput'
import { H4 } from 'src/components/typography'
import styles from './formStyles.styles'
const useStyles = makeStyles(styles)
const options = [
{ display: 'None', code: 'none' },
{ display: 'Email', code: 'email' },
{
display: 'Space separation',
subtitle: '(e.g. first and last name)',
code: 'spaceSeparation'
}
]
const TextEntry = () => {
const classes = useStyles()
const context = useFormikContext()
const showErrorColor = {
[classes.radioSubtitle]: true,
[classes.error]:
!R.path(['values', 'constraintType'])(context) &&
R.path(['errors', 'constraintType'])(context)
}
const getLabelInputs = () => {
switch (context.values.constraintType) {
case 'spaceSeparation':
return (
<div className={classes.flex}>
<Field
className={classes.label}
component={TextInput}
name={'inputLabel1'}
label={'First word label'}
/>
<Field
className={classes.label}
component={TextInput}
name={'inputLabel2'}
label={'Second word label'}
/>
</div>
)
default:
return (
<Field
className={classes.label}
component={TextInput}
name={'inputLabel1'}
label={'Text entry label'}
/>
)
}
}
return (
<>
<H4 className={classnames(showErrorColor)}>Text entry constraints</H4>
<Field
className={classes.row}
component={RadioGroup}
options={options}
name="constraintType"
/>
{getLabelInputs()}
</>
)
}
export default TextEntry

View file

@ -0,0 +1,50 @@
import { errorColor, spacer } from 'src/styling/variables'
const styles = {
flex: {
display: 'flex'
},
column: {
flexDirection: 'column'
},
choiceList: {
display: 'flex',
flexDirection: 'column',
maxHeight: 240,
overflowY: 'auto'
},
button: {
width: 120,
height: 28,
marginTop: 28
},
textInput: {
width: 420
},
row: {
flexDirection: 'row'
},
subtitle: {
marginBottom: 0
},
radioSubtitle: {
marginBottom: 0
},
error: {
color: errorColor
},
tl1: {
marginLeft: 8,
marginTop: 25
},
numberField: {
marginTop: 109,
maxWidth: 115
},
label: {
width: 200,
marginRight: spacer
}
}
export default styles

View file

@ -0,0 +1,79 @@
import { useFormikContext } from 'formik'
import * as R from 'ramda'
import React from 'react'
import * as Yup from 'yup'
import ChoiceList from './ChoiceList'
import NumericalEntry from './NumericalEntry'
import TextEntry from './TextEntry'
const nonEmptyStr = obj => obj.text && obj.text.length
const getForm = inputType => {
switch (inputType) {
case 'numerical':
return NumericalEntry
case 'text':
return TextEntry
case 'choiceList':
return ChoiceList
default:
return NumericalEntry
}
}
const TypeFields = () => {
const inputType = R.path(['values', 'inputType'])(useFormikContext()) ?? null
const Component = getForm(inputType)
return inputType && <Component />
}
const defaultValues = {
constraintType: '',
inputLength: '',
inputLabel1: '',
inputLabel2: '',
listChoices: [{ text: '' }, { text: '' }]
}
const validationSchema = Yup.lazy(values => {
switch (values.inputType) {
case 'numerical':
return Yup.object({
constraintType: Yup.string().required(),
inputLength: Yup.number().when('constraintType', {
is: 'length',
then: Yup.number()
.min(0)
.required(),
else: Yup.mixed().notRequired()
})
})
case 'text':
return Yup.object({
constraintType: Yup.string().required(),
inputLabel1: Yup.string().required(),
inputLabel2: Yup.string().when('constraintType', {
is: 'spaceSeparation',
then: Yup.string().required(),
else: Yup.mixed().notRequired()
})
})
case 'choiceList':
return Yup.object({
constraintType: Yup.string().required(),
listChoices: Yup.array().test(
'has-2-or-more',
'Choice list needs to have two or more non empty fields',
(values, ctx) => {
return R.filter(nonEmptyStr)(values).length > 1
}
)
})
default:
return Yup.mixed().notRequired()
}
})
export default TypeFields
export { defaultValues, validationSchema }

View file

@ -0,0 +1,244 @@
import { makeStyles } from '@material-ui/core'
import { Form, Formik } from 'formik'
import * as R from 'ramda'
import React, { useState } from 'react'
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 ChooseType, {
validationSchema as chooseTypeSchema,
defaultValues as chooseTypeDefaults
} from './Forms/ChooseType'
import NameOfRequirement, {
validationSchema as nameOfReqSchema,
defaultValues as nameOfReqDefaults
} from './Forms/NameOfRequirement'
import Screen1Information, {
validationSchema as screen1InfoSchema,
defaultValues as screen1InfoDefaults
} from './Forms/Screen1Information'
import Screen2Information, {
validationSchema as screen2InfoSchema,
defaultValues as screen2InfoDefaults
} from './Forms/Screen2Information'
import TypeFields, {
defaultValues as typeFieldsDefaults,
validationSchema as typeFieldsValidationSchema
} from './Forms/TypeFields'
import WizardSplash from './WizardSplash'
const LAST_STEP = 5
const styles = {
stepper: {
margin: [[16, 0, 14, 0]]
},
submit: {
display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, 24]]
},
button: {
marginLeft: 'auto'
},
form: {
height: '100%',
display: 'flex',
flexDirection: 'column'
}
}
const useStyles = makeStyles(styles)
const getStep = step => {
switch (step) {
case 1:
return {
schema: nameOfReqSchema,
Component: NameOfRequirement
}
case 2:
return {
schema: screen1InfoSchema,
Component: Screen1Information
}
case 3:
return { schema: chooseTypeSchema, Component: ChooseType }
case 4:
return {
schema: screen2InfoSchema,
Component: Screen2Information
}
case 5:
return {
schema: typeFieldsValidationSchema,
Component: TypeFields
}
default:
return {
schema: {},
Component: () => {
return <h1>Default component step</h1>
}
}
}
}
const nonEmptyStr = obj => obj.text && obj.text.length
const formatValues = (values, isEditing) => {
const isChoiceList = values.inputType === 'choiceList'
const choices = isChoiceList
? isEditing
? R.path(['listChoices'])(values)
: R.map(o => o.text)(R.filter(nonEmptyStr)(values.listChoices) ?? [])
: []
const hasInputLength = values.constraintType === 'length'
const inputLength = hasInputLength ? values.inputLength : ''
let resObj = {
name: values.requirementName,
screen1: {
text: values.screen1Text,
title: values.screen1Title
},
screen2: {
title: values.screen2Title,
text: values.screen2Text
},
input: {
type: values.inputType,
constraintType: values.constraintType
}
}
if (isChoiceList) {
resObj = R.assocPath(['input', 'choiceList'], choices, resObj)
}
if (hasInputLength) {
resObj = R.assocPath(['input', 'numDigits'], inputLength, resObj)
}
if (values.inputLabel1) {
resObj = R.assocPath(['input', 'label1'], values.inputLabel1, resObj)
}
if (values.inputLabel2) {
resObj = R.assocPath(['input', 'label2'], values.inputLabel2, resObj)
}
if (isEditing) {
resObj = R.assocPath(['id'], values.id, resObj)
}
return resObj
}
const makeEditingValues = it => {
const { customRequest } = it
return {
id: it.id,
requirementName: customRequest.name,
screen1Title: customRequest.screen1.title,
screen1Text: customRequest.screen1.text,
screen2Title: customRequest.screen2.title,
screen2Text: customRequest.screen2.text,
inputType: customRequest.input.type,
inputLabel1: customRequest.input.label1,
inputLabel2: customRequest.input.label2,
listChoices: customRequest.input.choiceList,
constraintType: customRequest.input.constraintType,
inputLength: customRequest.input.numDigits
}
}
const chooseNotNull = (a, b) => {
if (!R.isNil(b)) return b
return a
}
const Wizard = ({ onClose, error = false, toBeEdited, onSave, hasError }) => {
const classes = useStyles()
const isEditing = !R.isNil(toBeEdited)
const [step, setStep] = useState(isEditing ? 1 : 0)
const stepOptions = getStep(step)
const isLastStep = step === LAST_STEP
const onContinue = (values, actions) => {
const showScreen2 =
values.inputType === 'numerical' || values.inputType === 'choiceList'
if (isEditing && step === 2) {
return showScreen2
? setStep(4)
: onSave(formatValues(values, isEditing), isEditing)
}
if (isEditing && step === 4) {
return onSave(formatValues(values, isEditing), isEditing)
}
if (step === 3) {
return showScreen2 ? setStep(step + 1) : setStep(step + 2)
}
if (!isLastStep) {
return setStep(step + 1)
}
return onSave(formatValues(values, isEditing), isEditing)
}
const editingValues = isEditing ? makeEditingValues(toBeEdited) : {}
const wizardTitle = isEditing
? 'Editing custom requirement'
: 'New custom requirement'
return (
<Modal
title={step > 0 ? wizardTitle : ''}
handleClose={onClose}
width={520}
height={620}
open={true}>
{step > 0 && (
<Stepper
className={classes.stepper}
steps={LAST_STEP}
currentStep={step}
/>
)}
{step === 0 && !isEditing && <WizardSplash onContinue={onContinue} />}
{step > 0 && (
<Formik
validateOnBlur={false}
validateOnChange={false}
enableReinitialize={true}
onSubmit={onContinue}
initialValues={R.mergeWith(
chooseNotNull,
{
...nameOfReqDefaults,
...screen1InfoDefaults,
...screen2InfoDefaults,
...chooseTypeDefaults,
...typeFieldsDefaults
},
editingValues
)}
validationSchema={stepOptions.schema}>
<Form className={classes.form} id={'custom-requirement-form'}>
<stepOptions.Component />
<div className={classes.submit}>
{hasError && <ErrorMessage>Failed to save</ErrorMessage>}
<Button className={classes.button} type="submit">
{isLastStep ? 'Save' : 'Next'}
</Button>
</div>
</Form>
</Formik>
)}
</Modal>
)
}
export default Wizard

View file

@ -0,0 +1,57 @@
import { makeStyles } from '@material-ui/core'
import React from 'react'
import { Button } from 'src/components/buttons'
import { H1, P } from 'src/components/typography'
import { ReactComponent as CustomReqLogo } from 'src/styling/icons/compliance/custom-requirement.svg'
const styles = {
logo: {
maxHeight: 150,
maxWidth: 200
},
title: {
margin: [[24, 0, 32, 0]]
},
text: {
margin: 0
},
button: {
marginTop: 'auto',
marginBottom: 58
},
modalContent: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: [[0, 42]],
flex: 1
}
}
const useStyles = makeStyles(styles)
const WizardSplash = ({ onContinue }) => {
const classes = useStyles()
return (
<div className={classes.modalContent}>
<CustomReqLogo className={classes.logo} />
<H1 className={classes.title}>Custom information request</H1>
<P className={classes.text}>
A custom information request allows you to have an extra option to ask
specific information about your customers when adding a trigger that
isn't an option on the default requirements list.
</P>
<P>
Note that adding a custom information request isn't the same as adding
triggers. You will still need to add a trigger with the new requirement
to get this information from your customers.
</P>
<Button className={classes.button} onClick={onContinue}>
Get started
</Button>
</div>
)
}
export default WizardSplash

View file

@ -0,0 +1,2 @@
import CustomInfoRequests from './CustomInfoRequests'
export default CustomInfoRequests

View file

@ -0,0 +1,92 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
import { v4 } from 'uuid'
import { Button } from 'src/components/buttons'
import { Table as EditableTable } from 'src/components/editableTable'
import { H2 } from 'src/components/typography'
import { fromNamespace, namespaces } from 'src/utils/config'
import styles from './Triggers.styles'
import Wizard from './Wizard'
import { Schema, getElements, sortBy, toServer } from './helper'
const useStyles = makeStyles(styles)
const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) {
saveConfig(config: $config)
}
`
const TriggerView = ({
triggers,
showWizard,
config,
toggleWizard,
customInfoRequests
}) => {
const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(config)
)
const classes = useStyles()
const [error, setError] = useState(null)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => toggleWizard('off'),
refetchQueries: () => ['getData'],
onError: error => setError(error)
})
const save = config => {
setError(null)
return saveConfig({
variables: { config: { triggers: toServer(config.triggers) } }
})
}
const add = rawConfig => {
const toSave = R.concat([{ id: v4(), direction: 'both', ...rawConfig }])(
triggers
)
return saveConfig({ variables: { config: { triggers: toServer(toSave) } } })
}
return (
<>
<EditableTable
data={triggers}
name="triggers"
enableEdit
sortBy={sortBy}
groupBy="triggerType"
enableDelete
error={error?.message}
save={save}
validationSchema={Schema}
elements={getElements(currency, classes, customInfoRequests)}
/>
{showWizard && (
<Wizard
currency={currency}
error={error?.message}
save={add}
onClose={toggleWizard}
/>
)}
{R.isEmpty(triggers) && (
<Box display="flex" alignItems="center" flexDirection="column" mt={15}>
<H2>
It seems there are no active compliance triggers on your network
</H2>
<Button onClick={() => toggleWizard()}>Add first trigger</Button>
</Box>
)}
</>
)
}
export default TriggerView

View file

@ -1,25 +1,26 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import classnames from 'classnames'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
import { v4 } from 'uuid'
import { Tooltip } from 'src/components/Tooltip'
import { Link, Button } from 'src/components/buttons'
import { Table as EditableTable } from 'src/components/editableTable'
import { Link } from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import TitleSection from 'src/components/layout/TitleSection'
import { P, Label2, H2 } from 'src/components/typography'
import { P, Label2 } from 'src/components/typography'
import { ReactComponent as ReverseCustomInfoIcon } from 'src/styling/icons/circle buttons/filter/white.svg'
import { ReactComponent as CustomInfoIcon } from 'src/styling/icons/circle buttons/filter/zodiac.svg'
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
import { fromNamespace, toNamespace } from 'src/utils/config'
import CustomInfoRequests from './CustomInfoRequests'
import TriggerView from './TriggerView'
import styles from './Triggers.styles'
import Wizard from './Wizard'
import AdvancedTriggers from './components/AdvancedTriggers'
import { Schema, getElements, sortBy, fromServer, toServer } from './helper'
import { fromServer } from './helper'
const useStyles = makeStyles(styles)
const SAVE_CONFIG = gql`
@ -28,24 +29,40 @@ const SAVE_CONFIG = gql`
}
`
const GET_INFO = gql`
const GET_CONFIG = gql`
query getData {
config
}
`
const GET_CUSTOM_REQUESTS = gql`
query customInfoRequests {
customInfoRequests {
id
customRequest
enabled
}
}
`
const Triggers = () => {
const classes = useStyles()
const [wizard, setWizard] = useState(false)
const [advancedSettings, setAdvancedSettings] = useState(false)
const [wizardType, setWizard] = useState(false)
const { data, loading } = useQuery(GET_CONFIG)
const { data: customInfoReqData } = useQuery(GET_CUSTOM_REQUESTS)
const [error, setError] = useState(null)
const [subMenu, setSubMenu] = useState(false)
const customInfoRequests =
R.path(['customInfoRequests'])(customInfoReqData) ?? []
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
customInfoRequests
)
const { data, loading } = useQuery(GET_INFO)
const triggers = fromServer(data?.config?.triggers ?? [])
const complianceConfig =
data?.config && fromNamespace('compliance')(data.config)
const rejectAddressReuse = complianceConfig?.rejectAddressReuse ?? false
const [error, setError] = useState(null)
const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => setWizard(false),
@ -53,41 +70,56 @@ const Triggers = () => {
onError: error => setError(error)
})
const add = rawConfig => {
const toSave = R.concat([{ id: v4(), direction: 'both', ...rawConfig }])(
triggers
)
return saveConfig({ variables: { config: { triggers: toServer(toSave) } } })
}
const addressReuseSave = rawConfig => {
const config = toNamespace('compliance')(rawConfig)
return saveConfig({ variables: { config } })
}
const save = config => {
setError(null)
return saveConfig({
variables: { config: { triggers: toServer(config.triggers) } }
})
const titleSectionWidth = {
[classes.tableWidth]: !subMenu === 'customInfoRequests'
}
const currency = R.path(['fiatCurrency'])(
fromNamespace(namespaces.LOCALE)(data?.config)
)
const setBlur = shouldBlur => {
return shouldBlur
? document.querySelector('#root').classList.add('root-blur')
: document.querySelector('#root').classList.remove('root-blur')
}
const toggleWizard = wizardName => forceDisable => {
if (wizardType === wizardName || forceDisable) {
setBlur(false)
return setWizard(null)
}
setBlur(true)
return setWizard(wizardName)
}
return (
<>
<TitleSection
title="Compliance Triggers"
button={{
buttons={[
{
text: 'Advanced settings',
icon: SettingsIcon,
inverseIcon: ReverseSettingsIcon,
toggle: setAdvancedSettings
}}
className={classes.tableWidth}>
{!advancedSettings && (
forceDisable: !(subMenu === 'advancedSettings'),
toggle: show => {
setSubMenu(show ? 'advancedSettings' : false)
}
},
{
text: 'Custom info requests',
icon: CustomInfoIcon,
inverseIcon: ReverseCustomInfoIcon,
forceDisable: !(subMenu === 'customInfoRequests'),
toggle: show => {
setSubMenu(show ? 'customInfoRequests' : false)
}
}
]}
className={classnames(titleSectionWidth)}>
{!subMenu && (
<Box display="flex" alignItems="center">
<Box
display="flex"
@ -115,55 +147,41 @@ const Triggers = () => {
</Box>
</Box>
)}
</TitleSection>
{!advancedSettings && (
<>
<Box
marginBottom={2}
className={classes.tableWidth}
display="flex"
justifyContent="flex-end">
{!loading && !R.isEmpty(triggers) && (
<Link color="primary" onClick={() => setWizard(true)}>
{subMenu === 'customInfoRequests' &&
!R.isEmpty(enabledCustomInfoRequests) && (
<Box display="flex" justifyContent="flex-end">
<Link
color="primary"
onClick={() => toggleWizard('newCustomRequest')()}>
+ Add new custom info request
</Link>
</Box>
)}
{!loading && !subMenu && !R.isEmpty(triggers) && (
<Box display="flex" justifyContent="flex-end">
<Link color="primary" onClick={() => toggleWizard('newTrigger')()}>
+ Add new trigger
</Link>
)}
</Box>
<EditableTable
data={triggers}
name="triggers"
enableEdit
sortBy={sortBy}
groupBy="triggerType"
enableDelete
error={error?.message}
save={save}
validationSchema={Schema}
elements={getElements(currency, classes)}
/>
{wizard && (
<Wizard
currency={currency}
error={error?.message}
save={add}
onClose={() => setWizard(null)}
/>
)}
{!loading && R.isEmpty(triggers) && (
<Box
display="flex"
alignItems="center"
flexDirection="column"
mt={15}>
<H2>
It seems there are no active compliance triggers on your network
</H2>
<Button onClick={() => setWizard(true)}>Add first trigger</Button>
</Box>
)}
</>
</TitleSection>
{!loading && subMenu === 'customInfoRequests' && (
<CustomInfoRequests
data={enabledCustomInfoRequests}
showWizard={wizardType === 'newCustomRequest'}
toggleWizard={toggleWizard('newCustomRequest')}
/>
)}
{advancedSettings && (
{!loading && !subMenu && (
<TriggerView
triggers={triggers}
showWizard={wizardType === 'newTrigger'}
config={data?.config ?? {}}
toggleWizard={toggleWizard('newTrigger')}
customInfoRequests={customInfoRequests}
/>
)}
{!loading && subMenu === 'advancedSettings' && (
<AdvancedTriggers
error={error}
save={saveConfig}

View file

@ -162,6 +162,8 @@ const getRequirementText = (config, classes) => {
)
case 'block':
return <>blocked</>
case 'custom':
return <>asked to fulfill a custom requirement</>
default:
return orUnderline(null, classes)
}
@ -170,9 +172,11 @@ const getRequirementText = (config, classes) => {
const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => {
const classes = useStyles()
const oldText = R.range(1, step).map(it =>
getText(it, config, currency, classes)
)
const oldText = R.range(1, step).map((it, idx) => (
<React.Fragment key={idx}>
{getText(it, config, currency, classes)}
</React.Fragment>
))
const newText = getText(step, liveValues, currency, classes)
const isLastStep = step === LAST_STEP
@ -262,7 +266,7 @@ const Wizard = ({ onClose, save, error, currency }) => {
<Modal
title="New compliance trigger"
handleClose={onClose}
width={520}
width={560}
height={520}
infoPanel={
<InfoPanel

View file

@ -1,14 +1,17 @@
import { useQuery } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { memo } from 'react'
import * as Yup from 'yup'
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
import { NumberInput, RadioGroup, Dropdown } from 'src/components/inputs/formik'
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
import { errorColor } from 'src/styling/variables'
import { transformNumber } from 'src/utils/number'
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
// import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
@ -33,7 +36,7 @@ const useStyles = makeStyles({
},
specialGrid: {
display: 'grid',
gridTemplateColumns: [[182, 162, 141]]
gridTemplateColumns: [[182, 162, 181]]
},
directionIcon: {
marginRight: 2
@ -77,6 +80,10 @@ const useStyles = makeStyles({
},
daysInput: {
width: 60
},
dropdownField: {
marginTop: 16,
minWidth: 155
}
})
@ -501,6 +508,15 @@ const requirementOptions = [
{ display: 'Block', code: 'block' }
]
const GET_ACTIVE_CUSTOM_REQUESTS = gql`
query customInfoRequests($onlyEnabled: Boolean) {
customInfoRequests(onlyEnabled: $onlyEnabled) {
id
customRequest
}
}
`
const Requirement = () => {
const classes = useStyles()
const {
@ -510,6 +526,19 @@ const Requirement = () => {
handleChange,
setTouched
} = useFormikContext()
const { data } = useQuery(GET_ACTIVE_CUSTOM_REQUESTS, {
variables: {
onlyEnabled: true
}
})
const isSuspend = values?.requirement?.requirement === 'suspend'
const isCustom = values?.requirement?.requirement === 'custom'
const makeCustomReqOptions = () =>
customInfoRequests.map(it => ({
value: it.id,
display: it.customRequest.name
}))
const hasRequirementError =
!!errors.requirement &&
@ -517,8 +546,15 @@ const Requirement = () => {
(!values.requirement?.suspensionDays ||
values.requirement?.suspensionDays < 0)
const isSuspend = values?.requirement?.requirement === 'suspend'
const customInfoRequests = R.path(['customInfoRequests'])(data) ?? []
const enableCustomRequirement = customInfoRequests.length > 0
const customInfoOption = {
display: 'Custom information requirement',
code: 'custom'
}
const options = enableCustomRequirement
? [...requirementOptions, customInfoOption]
: [...requirementOptions, { ...customInfoOption, disabled: true }]
const titleClass = {
[classes.error]:
(!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError)
@ -532,7 +568,7 @@ const Requirement = () => {
<Field
component={RadioGroup}
name="requirement.requirement"
options={requirementOptions}
options={options}
labelClassName={classes.specialLabel}
radioClassName={classes.radio}
className={classnames(classes.radioGroup, classes.specialGrid)}
@ -543,7 +579,6 @@ const Requirement = () => {
})
}}
/>
{isSuspend && (
<Field
className={classes.thresholdField}
@ -554,6 +589,17 @@ const Requirement = () => {
error={hasRequirementError}
/>
)}
{isCustom && (
<div>
<Field
className={classes.dropdownField}
component={Dropdown}
label="Available requests"
name="requirement.customInfoRequestId"
options={makeCustomReqOptions()}
/>
</div>
)}
</>
)
}
@ -562,7 +608,13 @@ const requirements = {
schema: requirementSchema,
options: requirementOptions,
Component: Requirement,
initialValues: { requirement: { requirement: '', suspensionDays: '' } }
initialValues: {
requirement: {
requirement: '',
suspensionDays: '',
customInfoRequestId: ''
}
}
}
const getView = (data, code, compare) => it => {
@ -586,14 +638,23 @@ const getView = (data, code, compare) => it => {
// )
// }
const RequirementInput = () => {
const customReqIdMatches = customReqId => it => {
return it.id === customReqId
}
const RequirementInput = ({ customInfoRequests }) => {
const { values } = useFormikContext()
const classes = useStyles()
const requirement = values?.requirement?.requirement
const customRequestId =
R.path(['requirement', 'customInfoRequestId'])(values) ?? ''
const isSuspend = requirement === 'suspend'
const display = getView(requirementOptions, 'display')(requirement)
const display = customRequestId
? R.path(['customRequest', 'name'])(
R.find(customReqIdMatches(customRequestId))(customInfoRequests)
) ?? ''
: getView(requirementOptions, 'display')(requirement)
return (
<Box display="flex" alignItems="baseline">
@ -612,11 +673,20 @@ const RequirementInput = () => {
)
}
const RequirementView = ({ requirement, suspensionDays }) => {
const RequirementView = ({
requirement,
suspensionDays,
customInfoRequestId,
customInfoRequests
}) => {
const classes = useStyles()
const display = getView(requirementOptions, 'display')(requirement)
const display =
requirement === 'custom'
? R.path(['customRequest', 'name'])(
R.find(customReqIdMatches(customInfoRequestId))(customInfoRequests)
) ?? ''
: getView(requirementOptions, 'display')(requirement)
const isSuspend = requirement === 'suspend'
return (
<Box display="flex" alignItems="baseline">
{`${display} ${isSuspend ? 'for' : ''}`}
@ -728,7 +798,7 @@ const ThresholdView = ({ config, currency }) => {
return <DisplayThreshold config={config} currency={currency} />
}
const getElements = (currency, classes) => [
const getElements = (currency, classes, customInfoRequests) => [
{
name: 'triggerType',
size: 'sm',
@ -749,8 +819,10 @@ const getElements = (currency, classes) => [
size: 'sm',
width: 230,
bypassField: true,
input: RequirementInput,
view: it => <RequirementView {...it} />
input: () => <RequirementInput customInfoRequests={customInfoRequests} />,
view: it => (
<RequirementView {...it} customInfoRequests={customInfoRequests} />
)
},
{
name: 'threshold',
@ -782,12 +854,20 @@ const sortBy = [
)
]
const fromServer = triggers =>
R.map(
({ requirement, suspensionDays, threshold, thresholdDays, ...rest }) => ({
const fromServer = (triggers, customInfoRequests) => {
return R.map(
({
requirement,
suspensionDays,
threshold,
thresholdDays,
customInfoRequestId,
...rest
}) => ({
requirement: {
requirement,
suspensionDays
suspensionDays,
customInfoRequestId
},
threshold: {
threshold,
@ -796,6 +876,7 @@ const fromServer = triggers =>
...rest
})
)(triggers)
}
const toServer = triggers =>
R.map(({ requirement, threshold, ...rest }) => ({
@ -803,6 +884,7 @@ const toServer = triggers =>
suspensionDays: requirement.suspensionDays,
threshold: threshold.threshold,
thresholdDays: threshold.thresholdDays,
customInfoRequestId: requirement.customInfoRequestId,
...rest
}))(triggers)

View file

@ -23,6 +23,10 @@ export default {
// for when notification center is open
overflow: 'hidden'
},
'.root-blur': {
filter: 'blur(1px)',
pointerEvents: 'none'
},
html: {
height: fill
},

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="44px" viewBox="0 0 72 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>entry-icon/keyboard</title>
<g id="entry-icon/keyboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2-Copy-2" transform="translate(0.000000, 6.000000)" fill="#1B2559">
<rect id="Rectangle" x="0" y="0" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-5" x="0" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-8" x="0" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-3" x="12" y="1.13686838e-13" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-6" x="12" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-9" x="12" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-11" x="64" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-32" x="52" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-4" x="24" y="1.13686838e-13" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-26" x="36" y="1.13686838e-13" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-27" x="48" y="1.13686838e-13" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-28" x="60" y="1.13686838e-13" width="12" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-7" x="24" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-29" x="36" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-30" x="48" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-31" x="60" y="12" width="12" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-10" x="24" y="24" width="24" height="8" rx="2"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="44px" viewBox="0 0 72 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>entry-icon/keypad</title>
<g id="entry-icon/keypad" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(20.000000, 0.000000)" fill="#1B2559">
<rect id="Rectangle" x="0" y="0" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-5" x="0" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-8" x="0" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-3" x="12" y="0" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-6" x="12" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-9" x="12" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-11" x="12" y="36" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-4" x="24" y="0" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-7" x="24" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-10" x="24" y="24" width="8" height="8" rx="2"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="44px" viewBox="0 0 72 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>entry-icon/list</title>
<g id="entry-icon/list" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-4" transform="translate(8.000000, 0.000000)" fill="#1B2559">
<rect id="Rectangle-Copy-33" x="0" y="0" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-35" x="0" y="12" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-37" x="0" y="24" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-39" x="0" y="36" width="8" height="8" rx="2"></rect>
<rect id="Rectangle-Copy-34" x="12" y="2" width="44" height="4" rx="1"></rect>
<rect id="Rectangle-Copy-36" x="12" y="14" width="44" height="4" rx="1"></rect>
<rect id="Rectangle-Copy-38" x="12" y="26" width="44" height="4" rx="1"></rect>
<rect id="Rectangle-Copy-40" x="12" y="38" width="44" height="4" rx="1"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -10,7 +10,10 @@ import {
offColor,
subheaderColor,
fontSize3,
fontSize5
fontSize5,
zircon,
zircon2,
primaryColor
} from './variables'
const { p } = typographyStyles
@ -115,6 +118,44 @@ export default createMuiTheme({
backgroundColor: backgroundColor
}
}
},
MuiToggleButton: {
root: {
'&$selected': {
backgroundColor: zircon,
borderColor: primaryColor,
borderTopColor: [primaryColor, '!important'],
'&:hover': {
backgroundColor: zircon2
}
},
'&:hover': {
backgroundColor: zircon2
}
}
},
MuiToggleButtonGroup: {
groupedVertical: {
borderRadius: 8,
border: '1px solid',
borderColor: zircon,
'&:not(:first-child)': {
borderTop: '1px solid',
borderTopColor: zircon,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
borderBottomRightRadius: 8,
borderBottomLeftRadius: 8
},
'&:not(:last-child)': {
borderTop: '1px solid',
borderTopColor: zircon,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
borderBottomRightRadius: 8,
borderBottomLeftRadius: 8
}
}
}
}
})