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:
parent
3de2bb3d86
commit
ba8cac60f8
48 changed files with 2424 additions and 146 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
54
lib/new-admin/graphql/types/customInfoRequests.type.js
Normal file
54
lib/new-admin/graphql/types/customInfoRequests.type.js
Normal 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
|
||||
|
|
@ -40,6 +40,7 @@ const typeDef = gql`
|
|||
transactions: [Transaction]
|
||||
subscriberInfo: JSONObject
|
||||
customFields: [CustomerCustomField]
|
||||
customInfoRequests: [CustomRequestData]
|
||||
}
|
||||
|
||||
input CustomerInput {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
122
lib/new-admin/services/customInfoRequests.js
Normal file
122
lib/new-admin/services/customInfoRequests.js
Normal 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
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
migrations/1620165712260-custom-info-requests.js
Normal file
24
migrations/1620165712260-custom-info-requests.js
Normal 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()
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
28
new-lamassu-admin/src/components/inputs/base/Dropdown.js
Normal file
28
new-lamassu-admin/src/components/inputs/base/Dropdown.js
Normal 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
|
||||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
25
new-lamassu-admin/src/components/inputs/formik/Dropdown.js
Normal file
25
new-lamassu-admin/src/components/inputs/formik/Dropdown.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
import CustomInfoRequests from './CustomInfoRequests'
|
||||
export default CustomInfoRequests
|
||||
92
new-lamassu-admin/src/pages/Triggers/TriggerView.js
Normal file
92
new-lamassu-admin/src/pages/Triggers/TriggerView.js
Normal 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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
25
new-lamassu-admin/src/styling/icons/compliance/keyboard.svg
Normal file
25
new-lamassu-admin/src/styling/icons/compliance/keyboard.svg
Normal 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 |
18
new-lamassu-admin/src/styling/icons/compliance/keypad.svg
Normal file
18
new-lamassu-admin/src/styling/icons/compliance/keypad.svg
Normal 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 |
16
new-lamassu-admin/src/styling/icons/compliance/list.svg
Normal file
16
new-lamassu-admin/src/styling/icons/compliance/list.svg
Normal 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 |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue