feat: added the compliance/customers route

feat: added customers list page

feat: created the Customer type on the gql server and consume it

Currently only with the 'name' property

feat: added query on gql to get the customers list with the needed props

feat: added the currently available props to the front end table

fix: consider only sent txs for the aggregations on the customers list

fix: replace ExpTable with a non-expandable one

fix: remove unused properties from gql and front-end

fix: fixed the customers list columns width

fix: the last active table column was reading the wrong property

chore: remove debug logging

fix: use the correct table columns to check for txs that should be
considered on the customers list page

fix: use the international format for phone numbers

feat: added the search box

fix: remove ordering from the gql customers list query and moved it to
the front-end)

fix: removed the search box

chore: refactor the customers list table into a new component

chore: cleanup code

fix: fixed styles from customer list page header

feat: created customer profile page and started a transition feature
from the customer list

refactor: make components out of customers list table and profile page

feat: selecting a customer now transitions to its profile page

feat: added the customer transactions list table

fix: fix tx class button margins

fix: fix tx class icon margins on the customer list

fix: fixed crypto value

style: fixed the table column widths

feat: added the requirements column (no data yet, though)

feat: added the header with the customer details (no image yet, though)

feat: created the skeleton for the properties cards

feat: create the breadcrumb on the customer profile page (no link yet)

feat: added the children container in the property card

feat: added block customer action button

feat: added action buttons to the property cards

feat: added a children prop to the property card component

feat: added extra properties to the customer gql query

feat: added override fields to the customers gql query

style: added conditional styles to the property card component

feat: added children to the customer property cards

feat: create the edit button function on the property card

feat: add error properties to the txs (from gql)

style: fix action left editing action button and right property card
margins

feat: created a mutation to update a customer

feat: added the customer auth override state to the gql query

feat: fix the routing to the individual customer profile pages

feat: made the 'Customers' label on the breadcrumb work as a link

style: fixed the breadcrumb separator

style: fixed the customer name style

feat: made the action to block and authorize a customer as a toggle

feat: removed the 'Super user' switch (left for v2)

style: added the crossed camera icon on the photo

style: fixed the rejected icons

refactor: refactored some styles that were repetitive

refactor: created constants for the override possible states

feat: created functions for the authorization and blocking of overrides

refactor: renamed setOverride to updateCustomer

fix: remove current unused features

feat: make the property cards fields read-only

feat: setup id card photo and front camera photo image servers

feat: add id card photo on the corresponding property card

feat: add front camera photo on the customer profile header

feat: added gql cache to update the front-end after any mutation

style: added the crossed camera icon when there's no id card photo

refactor: extracted the PropertyCard component to another file

fix: deactivated the cache for the transactions (no need for it)

refactor: removed unused styles

fix: fixed front-camera-photo img path

fix: changed gql local data updates from cache to query refetch

refactor: move override status constants to the property card class

refactor: make the image servers URI a const dependent on the build

fix: remove requirements column from customer tx table (left for future
version)

fix: add aliases to gql query to correctly show errors on tx table

style: fix the transaction errors styles

feat: add terms and conditions page

feat: add modal preview

feat: remove preview

fix: increase space between switch and fields

feat: added the compliance/customers route

feat: added customers list page

feat: created the Customer type on the gql server and consume it

Currently only with the 'name' property

feat: added query on gql to get the customers list with the needed props

feat: added the currently available props to the front end table

fix: consider only sent txs for the aggregations on the customers list

fix: replace ExpTable with a non-expandable one

fix: remove unused properties from gql and front-end

fix: fixed the customers list columns width

fix: the last active table column was reading the wrong property

chore: remove debug logging

fix: use the correct table columns to check for txs that should be
considered on the customers list page

fix: use the international format for phone numbers

feat: added the search box

fix: remove ordering from the gql customers list query and moved it to
the front-end)

fix: removed the search box

chore: refactor the customers list table into a new component

chore: cleanup code

fix: fixed styles from customer list page header

fix: removed unused code

refactor: move transactions to a custom resolver in the customer's query

refactor: break the CustomerProfile component into several smaller ones

style: changed the table row error color from red to no change and the
error text from tomato to comet

fix: removed repeated function (wrong merge)

fix: make the updateCustomer function updates only what's explicitly
told so

style: return with the table row error style

refactor: create a function to test if a value is null prior to passing
it through another function

fix: make t&c changes backwards compatible

chore: bump eslint import library to activate rule

fix: stop showing object on empty column

fix: get machine logs page up-to-date

fix: small admin fixes

feat: add terms and conditions page

feat: add modal preview

feat: remove preview

fix: increase space between switch and fields

feat: added the compliance/customers route

feat: added customers list page

feat: created the Customer type on the gql server and consume it

Currently only with the 'name' property

feat: added query on gql to get the customers list with the needed props

feat: added the currently available props to the front end table

fix: consider only sent txs for the aggregations on the customers list

fix: replace ExpTable with a non-expandable one

fix: remove unused properties from gql and front-end

fix: fixed the customers list columns width

fix: the last active table column was reading the wrong property

chore: remove debug logging

fix: use the correct table columns to check for txs that should be
considered on the customers list page

fix: use the international format for phone numbers

feat: added the search box

fix: remove ordering from the gql customers list query and moved it to
the front-end)

fix: removed the search box

chore: refactor the customers list table into a new component

chore: cleanup code

fix: fixed styles from customer list page header

fix: make t&c changes backwards compatible

fix: stop showing object on empty column

fix: get machine logs page up-to-date

feat: add terms and conditions page

feat: add modal preview

feat: remove preview

fix: increase space between switch and fields

feat: added the compliance/customers route

feat: added customers list page

feat: created the Customer type on the gql server and consume it

Currently only with the 'name' property

feat: added query on gql to get the customers list with the needed props

feat: added the currently available props to the front end table

fix: consider only sent txs for the aggregations on the customers list

fix: replace ExpTable with a non-expandable one

fix: remove unused properties from gql and front-end

fix: fixed the customers list columns width

fix: the last active table column was reading the wrong property

chore: remove debug logging

fix: use the correct table columns to check for txs that should be
considered on the customers list page

fix: use the international format for phone numbers

feat: added the search box

fix: remove ordering from the gql customers list query and moved it to
the front-end)

fix: removed the search box

chore: refactor the customers list table into a new component

chore: cleanup code

fix: fixed styles from customer list page header

fix: make t&c changes backwards compatible

fix: stop showing object on empty column

fix: get machine logs page up-to-date

fix: small admin fixes

feat: create add machine page

feat: add terms and conditions page

feat: add modal preview

feat: remove preview

fix: increase space between switch and fields

feat: added the compliance/customers route

feat: added customers list page

feat: created the Customer type on the gql server and consume it

Currently only with the 'name' property

feat: added query on gql to get the customers list with the needed props

feat: added the currently available props to the front end table

fix: consider only sent txs for the aggregations on the customers list

fix: replace ExpTable with a non-expandable one

fix: remove unused properties from gql and front-end

fix: fixed the customers list columns width

fix: the last active table column was reading the wrong property

chore: remove debug logging

fix: use the correct table columns to check for txs that should be
considered on the customers list page

fix: use the international format for phone numbers

feat: added the search box

fix: remove ordering from the gql customers list query and moved it to
the front-end)

fix: removed the search box

chore: refactor the customers list table into a new component

chore: cleanup code

fix: fixed styles from customer list page header

fix: make t&c changes backwards compatible

fix: stop showing object on empty column

fix: get machine logs page up-to-date

feat: create add machine page

fix: fixed wrong merging

fix: more fixes from last merge

fix: export needed functions that wasn't exported from the customers
module

fix: removed the customer profile route from the header

fix: replaced old dataTable with new component

feat: added onClick event to new DataTable
This commit is contained in:
Liordino Neto 2020-02-06 20:36:56 -03:00 committed by Taranto
parent 840788e044
commit c808ca3be9
29 changed files with 1215 additions and 106 deletions

View file

@ -85,6 +85,30 @@ function update (id, data, userToken, txId) {
.then(camelize)
}
/**
* Update customer record
*
* @name updateCustomer
* @function
*
* @param {string} id Customer's id
* @param {object} data Fields to update
*
* @returns {Promise} Newly updated Customer
*/
async function updateCustomer (id, data) {
const formattedData = _.pick(
['authorized_override', 'id_card_photo_override', 'id_card_data_override', 'sms_override'],
_.mapKeys(_.snakeCase, data))
const sql = Pgp.helpers.update(formattedData, _.keys(formattedData), 'customers') +
' where id=$1'
await db.none(sql, [id])
return getCustomerById(id)
}
/**
* Get customer by id
*
@ -377,21 +401,24 @@ function batch () {
}, customers)))
}
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
/**
* Query all customers, ordered by last activity
* and with aggregate columns based on their
* transactions
*
* @returns {array} Array of customers with it's
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomersList () {
const sql = `select name, phone, total_txs, total_spent,
coalesce(tx_created, customer_created) as last_active,
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code,
tx_class as last_tx_class
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
id_card_photo_override, total_txs, total_spent, created as last_active,
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class
from (
select c.name, c.phone, c.created as customer_created,
t.tx_class, t.fiat, t.fiat_code, t.created as tx_created,
select c.id, c.name, c.authorized_override, c.front_camera_path, c.phone, c.sms_override,
c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path,
c.id_card_photo_override, t.tx_class, t.fiat, t.fiat_code, t.created,
row_number() over (partition by c.id order by t.created desc) as rn,
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
coalesce(sum(t.fiat) over (partition by c.id), 0) as total_spent
@ -410,6 +437,37 @@ function getCustomersList () {
}, customers)))
}
/**
* Query all customers, ordered by last activity
* and with aggregate columns based on their
* transactions
*
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomerById (id) {
const sql = `select id, name, authorized_override, front_camera_path, phone, sms_override,
id_card_data, id_card_data_override, id_card_data_expiration, id_card_photo_path,
id_card_photo_override, total_txs, total_spent, created as last_active,
fiat as last_tx_fiat, fiat_code as last_tx_fiat_code, tx_class as last_tx_class
from (
select c.id, c.name, c.authorized_override, c.front_camera_path, c.phone, c.sms_override,
c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.id_card_photo_path,
c.id_card_photo_override, t.tx_class, t.fiat, t.fiat_code, t.created,
row_number() over (partition by c.id order by t.created desc) as rn,
count(0) over (partition by c.id) as total_txs,
sum(t.fiat) over (partition by c.id) as total_spent
from customers c inner join (
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id
from cash_in_txs where send_confirmed = true union
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
where c.id = $1
) as cl where rn = 1`
return db.oneOrNone(sql, [id])
.then(populateOverrideUsernames)
.then(camelize)
}
/**
* @param {String} id customer id
* @param {Object} patch customer update record
@ -515,4 +573,4 @@ function updateFrontCamera (id, patch) {
})
}
module.exports = { add, get, batch, getCustomersList, getById, update, updatePhotoCard, updateFrontCamera }
module.exports = { add, get, batch, getCustomersList, getCustomerById, getById, update, updateCustomer, updatePhotoCard, updateFrontCamera }

View file

@ -2,10 +2,12 @@ const fs = require('fs')
const path = require('path')
const express = require('express')
const https = require('https')
const serveStatic = require('serve-static')
const cors = require('cors')
const helmet = require('helmet')
const cookieParser = require('cookie-parser')
const { ApolloServer, AuthenticationError } = require('apollo-server-express')
const _ = require('lodash/fp')
const T = require('../time')
const options = require('../options')
@ -15,6 +17,8 @@ const { typeDefs, resolvers } = require('./graphql/schema')
const devMode = require('minimist')(process.argv.slice(2)).dev
const NEVER = new Date(Date.now() + 100 * T.years)
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
const frontCameraBasedir = _.get('frontCameraDir', options)
const hostname = options.hostname
if (!hostname) {
@ -55,6 +59,9 @@ apolloServer.applyMiddleware({
// cors on app for /api/register endpoint.
app.use(cors({ credentials: true, origin: devMode && 'https://localhost:3000' }))
app.use('/id-card-photo', serveStatic(idPhotoCardBasedir, {index: false}))
app.use('/front-camera-photo', serveStatic(frontCameraBasedir, {index: false}))
app.get('/api/register', (req, res, next) => {
const otp = req.query.otp

View file

@ -61,8 +61,37 @@ const typeDefs = gql`
}
type Customer {
name: String
id: ID!
name: String!
authorizedOverride: String
frontCameraPath: String
phone: String
smsOverride: String
idCardData: JSONObject
idCardDataOverride: String
idCardDataExpiration: Date
idCardPhotoPath: String
idCardPhotoOverride: String
totalTxs: Int
totalSpent: String
lastActive: Date
lastTxFiat: String
lastTxFiatCode: String
lastTxClass: String
transactions: [Transaction]
}
input CustomerInput {
name: String
authorizedOverride: String
frontCameraPath: String
phone: String
smsOverride: String
idCardData: JSONObject
idCardDataOverride: String
idCardDataExpiration: Date
idCardPhotoPath: String
idCardPhotoOverride: String
totalTxs: Int
totalSpent: String
lastActive: Date
@ -160,6 +189,7 @@ const typeDefs = gql`
cryptoCurrencies: [CryptoCurrency]
machines: [Machine]
customers: [Customer]
customer(customerId: ID!): Customer
machineLogs(deviceId: ID!): [MachineLog]
funding: [CoinFunds]
serverVersion: String!
@ -187,6 +217,7 @@ const typeDefs = gql`
machineAction(deviceId:ID!, action: MachineAction!): Machine
machineSupportLogs(deviceId: ID!): SupportLogsResponse
serverSupportLogs: SupportLogsResponse
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer
saveConfig(config: JSONObject): JSONObject
createPairingTotem(name: String!): String
saveAccount(account: JSONObject): [JSONObject]
@ -201,6 +232,9 @@ const resolvers = {
JSON: GraphQLJSON,
JSONObject: GraphQLJSONObject,
Date: GraphQLDateTime,
Customer: {
transactions: parent => transactions.getCustomerTransactions(parent.id)
},
Query: {
countries: () => countries,
currencies: () => currencies,
@ -209,6 +243,7 @@ const resolvers = {
cryptoCurrencies: () => coins,
machines: () => machineLoader.getMachineNames(),
customers: () => customers.getCustomersList(),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
funding: () => funding.getFunding(),
machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId),
serverVersion: () => serverVersion,
@ -225,6 +260,7 @@ const resolvers = {
serverSupportLogs: () => serverLogs.insert(),
saveAccount: (...[, { account }]) => settingsLoader.saveAccounts([account]),
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
.then(it => {
notify()

View file

@ -61,6 +61,49 @@ function batch () {
.then(packager)
}
function getCustomerTransactions (customerId) {
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']),
_.take(NUM_RESULTS), _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class, txs.*,
c.phone as customer_phone,
c.id_card_data_number as customer_id_card_data_number,
c.id_card_data_expiration as customer_id_card_data_expiration,
c.id_card_data as customer_id_card_data,
c.name as customer_name,
c.front_camera_path as customer_front_camera_path,
c.id_card_photo_path as customer_id_card_photo_path,
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
from cash_in_txs as txs
left outer join customers c on txs.customer_id = c.id
where c.id = $1
order by created desc limit $3`
const cashOutSql = `select 'cashOut' as tx_class,
txs.*,
actions.tx_hash,
c.phone as customer_phone,
c.id_card_data_number as customer_id_card_data_number,
c.id_card_data_expiration as customer_id_card_data_expiration,
c.id_card_data as customer_id_card_data,
c.name as customer_name,
c.front_camera_path as customer_front_camera_path,
c.id_card_photo_path as customer_id_card_photo_path,
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $3 as expired
from cash_out_txs txs
inner join cash_out_actions actions on txs.id = actions.tx_id
and actions.action = 'provisionAddress'
left outer join customers c on txs.customer_id = c.id
where c.id = $1
order by created desc limit $2`
return Promise.all([
db.any(cashInSql, [customerId, cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
db.any(cashOutSql, [customerId, NUM_RESULTS, REDEEMABLE_AGE])
])
.then(packager)
}
function single (txId) {
const packager = _.flow(_.compact, _.map(camelize), addNames)
@ -107,4 +150,4 @@ function cancel (txId) {
.then(() => single(txId))
}
module.exports = { batch, single, cancel }
module.exports = { batch, getCustomerTransactions, single, cancel }

View file

@ -15049,9 +15049,9 @@
}
},
"libphonenumber-js": {
"version": "1.7.49",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.49.tgz",
"integrity": "sha512-AthHsii6+s+TBNMCUvKRzjscxMJAUD9rjDYZNj8rCVKBX9w1TzRbsmv+f4/pSuoHeKoNI64rcOV0Xb+7hoHudw==",
"version": "1.7.50",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.50.tgz",
"integrity": "sha512-FmdA2WvwdTgu1X05zBnAE+3UAA09o3hFxEaqR0J+x7tGPAt1AD7Dj54L58PTJodrFBve/AIThFtC/UGqfSLbBw==",
"requires": {
"minimist": "^1.2.5",
"xml2js": "^0.4.17"

View file

@ -18,7 +18,7 @@
"fuse.js": "^3.4.6",
"graphql": "^14.5.8",
"jss-plugin-extend": "^10.0.0",
"libphonenumber-js": "^1.7.49",
"libphonenumber-js": "^1.7.50",
"moment": "2.24.0",
"qrcode.react": "0.9.3",
"ramda": "^0.26.1",

View file

@ -91,12 +91,14 @@ const Tr = ({ error, errorMessage, children, className }) => {
const cardClasses = { root: classes.cardContentRoot }
const classNames = {
[classes.tr]: true,
[classes.trError]: error
[classes.trError]: error,
[classes.card]: true,
className
}
return (
<>
<Card className={classnames(classNames, classes.card, className)}>
<Card className={classnames(classNames, className)}>
<CardContent classes={cardClasses}>
<div className={classes.mainContent}>{children}</div>
{error && <div className={classes.errorContent}>{errorMessage}</div>}

View file

@ -33,12 +33,15 @@ const Row = ({
expanded,
expandRow,
expWidth,
expandable
expandable,
onClick
}) => {
const classes = useStyles()
return (
<>
<div
style={onClick && { cursor: 'pointer' }}
onClick={() => onClick && onClick(data)}>
<Tr
className={classnames(classes.row)}
error={data.error}
@ -66,7 +69,7 @@ const Row = ({
</Td>
</Tr>
)}
</>
</div>
)
}
@ -76,6 +79,7 @@ const DataTable = ({
Details,
className,
expandable,
onClick,
...props
}) => {
const [expanded, setExpanded] = useState(null)
@ -114,6 +118,7 @@ const DataTable = ({
expanded={index === expanded}
expandRow={expandRow}
expandable={expandable}
onClick={onClick}
/>
</div>
</CellMeasurer>

View file

@ -0,0 +1,180 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { memo } from 'react'
import { useQuery, useMutation } from '@apollo/react-hooks'
import { gql } from 'apollo-boost'
import { useHistory, useParams } from 'react-router-dom'
import Breadcrumbs from '@material-ui/core/Breadcrumbs'
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import {
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { ActionButton } from 'src/components/buttons'
import { Label1 } from 'src/components/typography'
import { ReactComponent as BlockReversedIcon } from 'src/styling/icons/button/block/white.svg'
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
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 {
CustomerDetails,
IdDataCard,
PhoneCard,
IdCardPhotoCard,
TransactionsList
} from './components'
import { mainStyles } from './Customers.styles'
const useStyles = makeStyles(mainStyles)
const GET_CUSTOMER = gql`
query customer($customerId: ID!) {
customer(customerId: $customerId) {
id
name
authorizedOverride
frontCameraPath
phone
smsOverride
idCardData
idCardDataOverride
idCardDataExpiration
idCardPhotoPath
idCardPhotoOverride
totalTxs
totalSpent
lastActive
lastTxFiat
lastTxFiatCode
lastTxClass
transactions {
txClass
id
fiat
fiatCode
cryptoAtoms
cryptoCode
created
errorMessage: error
error: errorCode
}
}
}
`
const SET_CUSTOMER = gql`
mutation setCustomer($customerId: ID!, $customerInput: CustomerInput) {
setCustomer(customerId: $customerId, customerInput: $customerInput) {
id
name
authorizedOverride
frontCameraPath
phone
smsOverride
idCardData
idCardDataOverride
idCardDataExpiration
idCardPhotoPath
idCardPhotoOverride
totalTxs
totalSpent
lastActive
lastTxFiat
lastTxFiatCode
lastTxClass
}
}
`
const CustomerProfile = memo(() => {
const classes = useStyles()
const history = useHistory()
const { id: customerId } = useParams()
const { data: customerResponse, refetch: getCustomer } = useQuery(
GET_CUSTOMER,
{
variables: { customerId },
fetchPolicy: 'no-cache'
}
)
const [setCustomer] = useMutation(SET_CUSTOMER, {
onCompleted: () => getCustomer()
})
const updateCustomer = it =>
setCustomer({
variables: {
customerId,
customerInput: it
}
})
const customerData = R.path(['customer'])(customerResponse) ?? []
const transactionsData = R.sortWith([R.descend('created')])(
R.path(['transactions'])(customerData) ?? []
)
const blocked =
R.path(['authorizedOverride'])(customerData) === OVERRIDE_REJECTED
return (
<>
<Breadcrumbs
separator={<NavigateNextIcon fontSize="small" />}
aria-label="breadcrumb">
<Label1
className={classes.labelLink}
onClick={() => history.push('/compliance/customers')}>
Customers
</Label1>
<Label1 className={classes.bold}>
{R.path(['name'])(customerData)}
</Label1>
</Breadcrumbs>
<div>
<div className={classes.header}>
<CustomerDetails customer={customerData} />
<div className={classes.rightAligned}>
<Label1 className={classes.label1}>Actions</Label1>
<ActionButton
className={classes.actionButton}
color="primary"
Icon={blocked ? AuthorizeIcon : BlockIcon}
InverseIcon={blocked ? AuthorizeReversedIcon : BlockReversedIcon}
onClick={() =>
updateCustomer({
authorizedOverride: blocked
? OVERRIDE_AUTHORIZED
: OVERRIDE_REJECTED
})
}>
{`${blocked ? 'Authorize' : 'Block'} customer`}
</ActionButton>
</div>
</div>
<div className={classes.rowCenterAligned}>
<IdDataCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
<PhoneCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
<IdCardPhotoCard
customerData={customerData}
updateCustomer={updateCustomer}
/>
</div>
</div>
<TransactionsList data={transactionsData} />
</>
)
})
export default CustomerProfile

View file

@ -1,23 +1,15 @@
import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import { gql } from 'apollo-boost'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import moment from 'moment'
import { useHistory } from 'react-router-dom'
import * as R from 'ramda'
import React from 'react'
import Title from 'src/components/Title'
import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { mainStyles } from './Customers.styles'
const useStyles = makeStyles(mainStyles)
import CustomersList from './CustomersList'
const GET_CUSTOMERS = gql`
{
customers {
id
name
phone
totalTxs
@ -31,82 +23,17 @@ const GET_CUSTOMERS = gql`
`
const Customers = () => {
const classes = useStyles()
const history = useHistory()
const { data: customersResponse } = useQuery(GET_CUSTOMERS)
const elements = [
{
header: 'Name',
width: 277,
view: R.path(['name'])
},
{
header: 'Phone',
width: 166,
view: it => parsePhoneNumberFromString(it.phone)?.formatInternational()
},
{
header: 'Total TXs',
width: 174,
textAlign: 'right',
view: it => `${Number.parseInt(it.totalTxs)}`
},
{
header: 'Total spent',
width: 188,
textAlign: 'right',
view: it =>
it.lastTxFiatCode
? `${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode}`
: null
},
{
header: 'Last active',
width: 197,
view: it =>
it.lastActive ? moment.utc(it.lastActive).format('YYYY-MM-D') : null
},
{
header: 'Last transaction',
width: 198,
textAlign: 'right',
view: it =>
it.lastTxFiatCode ? (
<div>
{`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode} `}
{it.lastTxClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}
</div>
) : null
}
]
const handleCustomerClicked = customer =>
history.push(`/compliance/customer/${customer.id}`)
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Customers</Title>
</div>
<div className={classes.headerLabels}>
<div>
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
<TxInIcon />
<span>Cash-in</span>
</div>
</div>
</div>
<DataTable
elements={elements}
data={R.sortWith([
R.ascend(R.prop('name')),
R.descend(R.prop('lastActive'))
])(R.path(['customers'])(customersResponse) ?? [])}
/>
</>
const customersData = R.sortWith([R.descend('lastActive')])(
R.path(['customers'])(customersResponse) ?? []
)
return <CustomersList data={customersData} onClick={handleCustomerClicked} />
}
export default Customers

View file

@ -1,13 +1,91 @@
import typographyStyles from 'src/components/typography/styles'
import baseStyles from 'src/pages/Logs.styles'
import { zircon, primaryColor, comet } from 'src/styling/variables'
const { label1 } = typographyStyles
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
const { titleWrapper, titleAndButtonsContainer } = baseStyles
const mainStyles = {
rightAligned: {
display: 'flex',
flexFlow: 'column nowrap',
right: 0
},
actionButton: {
height: 28
},
titleWrapper,
titleAndButtonsContainer,
buttonsWrapper,
header: {
display: 'flex',
flex: 1,
justifyContent: 'space-between'
},
customerDetails: {
display: 'flex',
flex: 1
},
row: {
display: 'flex',
flexFlow: 'row nowrap'
},
rowCenterAligned: {
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center'
},
rowSpaceBetween: {
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
justifyContent: 'space-between'
},
column: {
display: 'flex',
flexFlow: 'column nowrap',
width: '100%',
height: '100%',
justifyContent: 'space-between'
},
textInput: {
width: 144
},
label1: {
fontFamily: 'MuseoSans',
fontSize: 12,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1.33,
letterSpacing: 'normal',
color: comet,
margin: [[4, 0]]
},
p: {
fontFamily: 'MuseoSans',
fontSize: 14,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1.14,
letterSpacing: 'normal',
color: primaryColor
},
bold: {
fontWeight: 'bold'
},
txId: {
fontFamily: 'MuseoSans',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
txClassIconLeft: {
marginRight: 11
},
txClassIconRight: {
marginLeft: 11
},
headerLabels: {
display: 'flex',
flexDirection: 'row',
@ -22,6 +100,62 @@ const mainStyles = {
extend: label1,
marginLeft: 6
}
},
photo: {
width: 92,
height: 92,
borderRadius: 8,
backgroundColor: zircon,
margin: [[15, 28, 0, 0]],
padding: [[30]],
alignItems: 'center',
justifyContent: 'space-between'
},
idDataCard: {
width: 544,
height: 240
},
phoneCard: {
width: 253,
height: 240
},
idCardPhotoCard: {
width: 378,
height: 240,
margin: [[32, 0, 0, 0]]
},
labelLink: {
cursor: 'pointer'
},
field: {
position: 'relative',
width: 144,
height: 46,
padding: [[0, 4, 4, 0]],
display: 'flex',
flexDirection: 'column',
'& > p:first-child': {
height: 16,
lineHeight: '16px',
paddingLeft: 3,
margin: [[0, 0, 5, 0]]
},
'& > p:last-child': {
margin: 0,
paddingLeft: 4
}
},
customerName: {
marginBottom: 32
},
fieldDisplay: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
idCardPhoto: {
maxWidth: 171,
maxHeight: 97
}
}

View file

@ -0,0 +1,86 @@
import { makeStyles } from '@material-ui/core/styles'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import moment from 'moment'
import * as R from 'ramda'
import React from 'react'
import Title from 'src/components/Title'
import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { mainStyles } from './Customers.styles'
const useStyles = makeStyles(mainStyles)
const CustomersList = ({ data, onClick }) => {
const classes = useStyles()
const elements = [
{
header: 'Name',
width: 277,
view: R.path(['name'])
},
{
header: 'Phone',
width: 186,
view: it => parsePhoneNumberFromString(it.phone).formatInternational()
},
{
header: 'Total TXs',
width: 154,
textAlign: 'right',
view: it => `${Number.parseInt(it.totalTxs)}`
},
{
header: 'Total spent',
width: 188,
textAlign: 'right',
view: it => `${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode}`
},
{
header: 'Last active',
width: 197,
view: it => moment.utc(it.lastActive).format('YYYY-MM-D')
},
{
header: 'Last transaction',
width: 198,
textAlign: 'right',
view: it => (
<>
{`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode}`}
{it.lastTxClass === 'cashOut' ? (
<TxOutIcon className={classes.txClassIconRight} />
) : (
<TxInIcon className={classes.txClassIconRight} />
)}
</>
)
}
]
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Customers</Title>
</div>
<div className={classes.headerLabels}>
<div>
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
<TxInIcon />
<span>Cash-in</span>
</div>
</div>
</div>
<DataTable elements={elements} data={data} onClick={onClick} />
</>
)
}
export default CustomersList

View file

@ -0,0 +1,91 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import moment from 'moment'
import React, { memo } from 'react'
import { H2 } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { mainStyles } from '../Customers.styles'
import { ifNotNull } from '../../../utils/nullCheck'
import FrontCameraPhoto from './FrontCameraPhoto'
const useStyles = makeStyles(mainStyles)
const CustomerDetails = memo(({ customer }) => {
const classes = useStyles()
const elements = [
{
header: 'Transactions',
size: 127,
value: ifNotNull(
customer.totalTxs,
`${Number.parseInt(customer.totalTxs)}`
)
},
{
header: 'Transaction volume',
size: 167,
value: ifNotNull(
customer.totalSpent,
`${Number.parseFloat(customer.totalSpent)} ${customer.lastTxFiatCode}`
)
},
{
header: 'Last active',
size: 142,
value: ifNotNull(
customer.lastActive,
moment.utc(customer.lastActive).format('YYYY-MM-D')
)
},
{
header: 'Last transaction',
size: 198,
value: ifNotNull(
customer.lastTxFiat,
<>
{`${Number.parseFloat(customer.lastTxFiat)}
${customer.lastTxFiatCode}`}
{customer.lastTxClass === 'cashOut' ? (
<TxOutIcon className={classes.txClassIconRight} />
) : (
<TxInIcon className={classes.txClassIconRight} />
)}
</>
)
}
]
return (
<div className={classes.customerDetails}>
<FrontCameraPhoto
frontCameraPath={R.path(['frontCameraPath'])(customer)}
/>
<div>
<div className={classes.rowCenterAligned}>
<H2 className={classes.customerName}>{R.path(['name'])(customer)}</H2>
</div>
<div className={classes.rowCenterAligned}>
{elements.map(({ size, header }, idx) => (
<div key={idx} className={classes.label1} style={{ width: size }}>
{header}
</div>
))}
</div>
<div className={classes.rowCenterAligned}>
{elements.map(({ size, value }, idx) => (
<div key={idx} className={classes.p} style={{ width: size }}>
{value}
</div>
))}
</div>
</div>
</div>
)
})
export default CustomerDetails

View file

@ -0,0 +1,21 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { memo } from 'react'
import { Info3, Label1 } from 'src/components/typography'
import { mainStyles } from '../Customers.styles'
const useStyles = makeStyles(mainStyles)
const Field = memo(({ label, display }) => {
const classes = useStyles()
return (
<div className={classes.field}>
<Label1>{label}</Label1>
<Info3 className={classes.fieldDisplay}>{display}</Info3>
</div>
)
})
export default Field

View file

@ -0,0 +1,30 @@
import { makeStyles } from '@material-ui/core/styles'
import React, { memo } from 'react'
import { Paper } from '@material-ui/core'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import { mainStyles } from '../Customers.styles'
import { IMAGES_URI } from './variables'
const useStyles = makeStyles(mainStyles)
const FrontCameraPhoto = memo(({ frontCameraPath }) => {
const classes = useStyles()
return (
<Paper className={classes.photo} elevation={0}>
{frontCameraPath ? (
<img
src={`${IMAGES_URI}/front-camera-photo/${frontCameraPath}`}
alt=""
/>
) : (
<CrossedCameraIcon />
)}
</Paper>
)
})
export default FrontCameraPhoto

View file

@ -0,0 +1,55 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import moment from 'moment'
import React, { memo } from 'react'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import {
PropertyCard,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { mainStyles } from '../Customers.styles'
import Field from './Field'
import { IMAGES_URI } from './variables'
const useStyles = makeStyles(mainStyles)
const IdCardPhotoCard = memo(({ customerData, updateCustomer }) => {
const classes = useStyles()
return (
<PropertyCard
className={classes.idCardPhotoCard}
title={'ID card photo'}
state={R.path(['idCardPhotoOverride'])(customerData)}
authorize={() =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED })
}
reject={() => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED })}>
<div className={classes.row}>
{customerData.idCardPhotoPath ? (
<img
className={classes.idCardPhoto}
src={`${IMAGES_URI}/id-card-photo/${R.path(['idCardPhotoPath'])(
customerData
)}`}
alt=""
/>
) : (
<CrossedCameraIcon />
)}
<Field
label={'Expiration date'}
display={moment
.utc(R.path(['idCardDataExpiration'])(customerData))
.format('YYYY-MM-D')}
/>
</div>
</PropertyCard>
)
})
export default IdCardPhotoCard

View file

@ -0,0 +1,77 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import moment from 'moment'
import React, { memo } from 'react'
import {
PropertyCard,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { mainStyles } from '../Customers.styles'
import Field from './Field'
const useStyles = makeStyles(mainStyles)
const IdDataCard = memo(({ customerData, updateCustomer }) => {
const classes = useStyles()
return (
<PropertyCard
className={classes.idDataCard}
title={'ID data'}
state={R.path(['idCardDataOverride'])(customerData)}
authorize={() =>
updateCustomer({ idCardDataOverride: OVERRIDE_AUTHORIZED })
}
reject={() => updateCustomer({ idCardDataOverride: OVERRIDE_REJECTED })}>
<div className={classes.rowSpaceBetween}>
<div className={classes.column}>
<Field
label={'Name'}
display={`${R.path(['idCardData', 'firstName'])(
customerData
)} ${R.path(['idCardData', 'lastName'])(customerData)}`}
/>
<Field
label={'Gender'}
display={R.path(['idCardData', 'gender'])(customerData)}
/>
</div>
<div className={classes.column}>
<Field
label={'ID number'}
display={R.path(['idCardData', 'documentNumber'])(customerData)}
/>
<Field
label={'Country'}
display={R.path(['idCardData', 'country'])(customerData)}
/>
</div>
<div className={classes.column}>
<Field
label={'Age'}
display={moment
.utc()
.diff(
moment
.utc(R.path(['idCardData', 'dateOfBirth'])(customerData))
.format('YYYY-MM-D'),
'years'
)}
/>
<Field
label={'Expiration date'}
display={moment
.utc(R.path(['idCardData', 'expirationDate'])(customerData))
.format('YYYY-MM-D')}
/>
</div>
</div>
</PropertyCard>
)
})
export default IdDataCard

View file

@ -0,0 +1,42 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { memo } from 'react'
import { parsePhoneNumberFromString } from 'libphonenumber-js'
import {
PropertyCard,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { mainStyles } from '../Customers.styles'
import Field from './Field'
const useStyles = makeStyles(mainStyles)
const PhoneCard = memo(({ customerData, updateCustomer }) => {
const classes = useStyles()
return (
<PropertyCard
className={classes.phoneCard}
title={'Phone'}
state={R.path(['smsOverride'])(customerData)}
authorize={() => updateCustomer({ smsOverride: OVERRIDE_AUTHORIZED })}
reject={() => updateCustomer({ smsOverride: OVERRIDE_REJECTED })}>
<Field
label={'Phone'}
display={
R.path(['phone'])(customerData)
? parsePhoneNumberFromString(
R.path(['phone'])(customerData)
).formatInternational()
: []
}
/>
</PropertyCard>
)
})
export default PhoneCard

View file

@ -0,0 +1,88 @@
import { makeStyles } from '@material-ui/core/styles'
import BigNumber from 'bignumber.js'
import moment from 'moment'
import React from 'react'
import DataTable from 'src/components/tables/DataTable'
import { H4, Label2 } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { toUnit } from 'src/utils/coin'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import { mainStyles } from '../Customers.styles'
const useStyles = makeStyles(mainStyles)
const TransactionsList = ({ data }) => {
const classes = useStyles()
const elements = [
{
header: 'Direction',
width: 207,
view: it => (
<>
{it.txClass === 'cashOut' ? (
<TxOutIcon className={classes.txClassIconLeft} />
) : (
<TxInIcon className={classes.txClassIconLeft} />
)}
{it.txClass === 'cashOut' ? 'Cach-out' : 'Cash-in'}
</>
)
},
{
header: 'Transaction ID',
width: 414,
view: it => (
<CopyToClipboard className={classes.txId}>{it.id}</CopyToClipboard>
)
},
{
header: 'Cash',
width: 146,
view: it => (
<>
{`${Number.parseFloat(it.fiat)} `}
<Label2 inline>{it.fiatCode}</Label2>
</>
)
},
{
header: 'Crypto',
width: 142,
view: it => (
<>
{`${toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode).toFormat(
5
)} `}
<Label2 inline>{it.cryptoCode}</Label2>
</>
)
},
{
header: 'Date',
width: 157,
view: it => moment.utc(it.created).format('YYYY-MM-D')
},
{
header: 'Time (h:m:s)',
width: 134,
view: it => moment.utc(it.created).format('hh:mm:ss')
}
]
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<H4>All transactions from this customer</H4>
</div>
</div>
<DataTable elements={elements} data={data} />
</>
)
}
export default TransactionsList

View file

@ -0,0 +1,13 @@
import CustomerDetails from './CustomerDetails'
import IdDataCard from './IdDataCard'
import IdCardPhotoCard from './IdCardPhotoCard'
import PhoneCard from './PhoneCard'
import TransactionsList from './TransactionsList'
export {
CustomerDetails,
IdDataCard,
IdCardPhotoCard,
PhoneCard,
TransactionsList
}

View file

@ -0,0 +1,95 @@
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import React, { memo } from 'react'
import { Paper } from '@material-ui/core'
import { ActionButton } from 'src/components/buttons'
import { H3 } 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 { propertyCardStyles } from './PropertyCard.styles'
const useStyles = makeStyles(propertyCardStyles)
const OVERRIDE_PENDING = 'automatic'
const OVERRIDE_AUTHORIZED = 'verified'
const OVERRIDE_REJECTED = 'blocked'
const PropertyCard = memo(
({ className, title, state, authorize, reject, children }) => {
const classes = useStyles()
const propertyCardClassNames = {
[classes.propertyCard]: true,
[classes.propertyCardPending]: state === OVERRIDE_PENDING,
[classes.propertyCardRejected]: state === OVERRIDE_REJECTED,
[classes.propertyCardAccepted]: state === OVERRIDE_AUTHORIZED
}
const label1ClassNames = {
[classes.label1]: true,
[classes.label1Pending]: state === OVERRIDE_PENDING,
[classes.label1Rejected]: state === OVERRIDE_REJECTED,
[classes.label1Accepted]: state === OVERRIDE_AUTHORIZED
}
const AuthorizeButton = () => (
<ActionButton
className={classes.cardActionButton}
color="secondary"
Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon}
onClick={() => authorize()}>
Authorize
</ActionButton>
)
const RejectButton = () => (
<ActionButton
className={classes.cardActionButton}
color="secondary"
Icon={RejectIcon}
InverseIcon={RejectReversedIcon}
onClick={() => reject()}>
Reject
</ActionButton>
)
const authorizedAsString =
state === OVERRIDE_PENDING
? 'Pending'
: state === OVERRIDE_REJECTED
? 'Rejected'
: 'Accepted'
return (
<Paper
className={classnames(propertyCardClassNames, className)}
elevation={0}>
<div className={classes.rowSpaceBetween}>
<H3>{title}</H3>
<div className={classnames(label1ClassNames)}>
{authorizedAsString}
</div>
</div>
<Paper className={classes.cardProperties} elevation={0}>
{children}
</Paper>
<div className={classes.buttonsWrapper}>
{state !== OVERRIDE_AUTHORIZED && AuthorizeButton()}
{state !== OVERRIDE_REJECTED && RejectButton()}
</div>
</Paper>
)
}
)
export {
PropertyCard,
OVERRIDE_PENDING,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
}

View file

@ -0,0 +1,74 @@
import {
white,
zircon,
mistyRose,
tomato,
spring3,
spring4,
comet
} from 'src/styling/variables'
const propertyCardStyles = {
propertyCard: {
margin: [[32, 12, 0, 0]],
padding: [[0, 16]],
borderRadius: 8
},
propertyCardPending: {
backgroundColor: zircon
},
propertyCardRejected: {
backgroundColor: mistyRose
},
propertyCardAccepted: {
backgroundColor: spring3
},
label1: {
fontFamily: 'MuseoSans',
fontSize: 12,
fontWeight: 500,
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 1.33,
letterSpacing: 'normal',
color: comet,
margin: [[4, 0]]
},
label1Pending: {
color: comet
},
label1Rejected: {
color: tomato
},
label1Accepted: {
color: spring4
},
cardActionButton: {
height: 28,
marginLeft: 12
},
cardProperties: {
borderRadius: 8,
width: '100%',
height: 'calc(100% - 100px)',
padding: [[20]],
boxSizing: 'border-box',
boxShadow: '0 0 8px 0 rgba(0, 0, 0, 0.04)',
border: 'solid 0',
backgroundColor: white
},
rowSpaceBetween: {
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
justifyContent: 'space-between'
},
buttonsWrapper: {
display: 'flex',
justifyContent: 'flex-end',
marginTop: 16,
marginBottom: 16
}
}
export { propertyCardStyles }

View file

@ -0,0 +1,13 @@
import {
PropertyCard,
OVERRIDE_PENDING,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from './PropertyCard'
export {
PropertyCard,
OVERRIDE_PENDING,
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
}

View file

@ -0,0 +1,4 @@
const IMAGES_URI =
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
export { IMAGES_URI }

View file

@ -1,3 +1,4 @@
import Customers from './Customers'
import CustomerProfile from './CustomerProfile'
export default Customers
export { Customers, CustomerProfile }

View file

@ -4,7 +4,7 @@ import { Route, Redirect, Switch } from 'react-router-dom'
import AuthRegister from 'src/pages/AuthRegister'
import Commissions from 'src/pages/Commissions'
import Customers from 'src/pages/Customers'
import { Customers, CustomerProfile } from 'src/pages/Customers'
import Funding from 'src/pages/Funding'
import Locales from 'src/pages/Locales'
import MachineLogs from 'src/pages/MachineLogs'
@ -124,6 +124,11 @@ const tree = [
label: 'Customers',
route: '/compliance/customers',
component: Customers
},
{
key: 'customer',
route: '/compliance/customer/:id',
component: CustomerProfile
}
]
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
<title>icon/crossed-camera</title>
<desc>Created with Sketch.</desc>
<g id="icon/crossed-camera" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="crossed-camera">
<g id="Group-2">
<g id="icon/ID/cam/zodiac" transform="translate(0.000000, 3.657143)" fill="#1B2559" fill-rule="nonzero">
<path d="M16,19.1876029 C12.4482116,19.1876029 9.56809571,16.389776 9.56809571,12.9394673 C9.56809571,9.48915858 12.4482116,6.69133172 16,6.69133172 C19.5517884,6.69133172 22.4319043,9.48915858 22.4319043,12.9394673 C22.4319043,16.389776 19.5517884,19.1876029 16,19.1876029 Z M16,17.104891 C18.3677075,17.104891 20.2879362,15.239526 20.2879362,12.9394673 C20.2879362,10.6394086 18.3677075,8.77404358 16,8.77404358 C13.6322925,8.77404358 11.7120638,10.6394086 11.7120638,12.9394673 C11.7120638,15.239526 13.6322925,17.104891 16,17.104891 Z M22.7667469,3.30692494 L30.7397807,3.30692494 C31.3318211,3.30692494 31.8117647,3.77315587 31.8117647,4.34828087 L31.8117647,22.0513317 C31.8117647,22.6264567 31.3318211,23.0926877 30.7397807,23.0926877 L1.26021934,23.0926877 C0.6681789,23.0926877 0.188235294,22.6264567 0.188235294,22.0513317 L0.188235294,4.34828087 C0.188235294,3.77315587 0.6681789,3.30692494 1.26021934,3.30692494 L9.23325311,3.30692494 L12.0766705,0.494526627 C12.2782333,0.295162767 12.5538198,0.182857143 12.8414756,0.182857143 L19.1585244,0.182857143 C19.4461802,0.182857143 19.7217667,0.295162767 19.9233295,0.494526627 L22.7667469,3.30692494 Z M29.6677966,5.3896368 L22.3170489,5.3896368 C22.0293931,5.3896368 21.7538065,5.27733118 21.5522438,5.07796732 L18.7088264,2.26556901 L13.2911736,2.26556901 L10.4477562,5.07796732 C10.2461935,5.27733118 9.97060695,5.3896368 9.68295115,5.3896368 L2.33220339,5.3896368 L2.33220339,21.0099758 L29.6677966,21.0099758 L29.6677966,5.3896368 Z" id="Stroke-1"></path>
</g>
<line x1="32" y1="0" x2="0" y2="32" id="Line" stroke="#FF584A" stroke-width="2" stroke-linecap="square"></line>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -122,6 +122,7 @@ export {
comet,
spring2,
spring3,
spring4,
tomato,
pumpkin,
mistyRose,

View file

@ -0,0 +1,5 @@
const ifNotNull = (value, valueIfNotNull) => {
return value === null ? '' : valueIfNotNull
}
export { ifNotNull }