684 lines
18 KiB
JavaScript
684 lines
18 KiB
JavaScript
import { useQuery, useMutation } from '@apollo/react-hooks'
|
|
import { makeStyles, Breadcrumbs, Box } from '@material-ui/core'
|
|
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
|
import gql from 'graphql-tag'
|
|
import * as R from 'ramda'
|
|
import React, { memo, useState } from 'react'
|
|
import { useHistory, useParams } from 'react-router-dom'
|
|
|
|
import { ActionButton } from 'src/components/buttons'
|
|
import { Switch } from 'src/components/inputs'
|
|
import { Label1, Label2 } from 'src/components/typography'
|
|
import {
|
|
OVERRIDE_AUTHORIZED,
|
|
OVERRIDE_REJECTED
|
|
} from 'src/pages/Customers/components/propertyCard'
|
|
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 BlockReversedIcon } from 'src/styling/icons/button/block/white.svg'
|
|
import { ReactComponent as BlockIcon } from 'src/styling/icons/button/block/zodiac.svg'
|
|
import { ReactComponent as DataReversedIcon } from 'src/styling/icons/button/data/white.svg'
|
|
import { ReactComponent as DataIcon } from 'src/styling/icons/button/data/zodiac.svg'
|
|
// TODO: Enable for next release
|
|
// import { ReactComponent as DiscountReversedIcon } from 'src/styling/icons/button/discount/white.svg'
|
|
// import { ReactComponent as Discount } from 'src/styling/icons/button/discount/zodiac.svg'
|
|
import { fromNamespace, namespaces } from 'src/utils/config'
|
|
|
|
import CustomerData from './CustomerData'
|
|
import CustomerNotes from './CustomerNotes'
|
|
import CustomerPhotos from './CustomerPhotos'
|
|
import styles from './CustomerProfile.styles'
|
|
import {
|
|
CustomerDetails,
|
|
TransactionsList,
|
|
CustomerSidebar,
|
|
Wizard
|
|
} from './components'
|
|
import { getFormattedPhone, getName, formatPhotosData } from './helper'
|
|
|
|
const useStyles = makeStyles(styles)
|
|
|
|
const GET_CUSTOMER = gql`
|
|
query customer($customerId: ID!) {
|
|
config
|
|
customer(customerId: $customerId) {
|
|
id
|
|
authorizedOverride
|
|
frontCameraPath
|
|
frontCameraAt
|
|
frontCameraOverride
|
|
phone
|
|
isAnonymous
|
|
smsOverride
|
|
idCardData
|
|
idCardDataOverride
|
|
idCardDataExpiration
|
|
idCardPhotoPath
|
|
idCardPhotoOverride
|
|
usSsn
|
|
usSsnOverride
|
|
sanctions
|
|
sanctionsAt
|
|
sanctionsOverride
|
|
totalTxs
|
|
totalSpent
|
|
lastActive
|
|
lastTxFiat
|
|
lastTxFiatCode
|
|
lastTxClass
|
|
daysSuspended
|
|
isSuspended
|
|
isTestCustomer
|
|
subscriberInfo
|
|
phoneOverride
|
|
customFields {
|
|
id
|
|
label
|
|
value
|
|
}
|
|
notes {
|
|
id
|
|
customerId
|
|
title
|
|
content
|
|
created
|
|
lastEditedAt
|
|
}
|
|
transactions {
|
|
txClass
|
|
id
|
|
fiat
|
|
fiatCode
|
|
cryptoAtoms
|
|
cryptoCode
|
|
created
|
|
machineName
|
|
errorMessage: error
|
|
error: errorCode
|
|
txCustomerPhotoAt
|
|
txCustomerPhotoPath
|
|
}
|
|
customInfoRequests {
|
|
customerId
|
|
override
|
|
overrideBy
|
|
overrideAt
|
|
customerData
|
|
customInfoRequest {
|
|
id
|
|
enabled
|
|
customRequest
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const SET_CUSTOMER = gql`
|
|
mutation setCustomer($customerId: ID!, $customerInput: CustomerInput) {
|
|
setCustomer(customerId: $customerId, customerInput: $customerInput) {
|
|
id
|
|
authorizedOverride
|
|
frontCameraPath
|
|
frontCameraOverride
|
|
phone
|
|
smsOverride
|
|
idCardData
|
|
idCardDataOverride
|
|
idCardDataExpiration
|
|
idCardPhotoPath
|
|
idCardPhotoOverride
|
|
usSsn
|
|
usSsnOverride
|
|
sanctions
|
|
sanctionsAt
|
|
sanctionsOverride
|
|
totalTxs
|
|
totalSpent
|
|
lastActive
|
|
lastTxFiat
|
|
lastTxFiatCode
|
|
lastTxClass
|
|
subscriberInfo
|
|
phoneOverride
|
|
}
|
|
}
|
|
`
|
|
const EDIT_CUSTOMER = gql`
|
|
mutation editCustomer($customerId: ID!, $customerEdit: CustomerEdit) {
|
|
editCustomer(customerId: $customerId, customerEdit: $customerEdit) {
|
|
id
|
|
idCardData
|
|
usSsn
|
|
}
|
|
}
|
|
`
|
|
|
|
const REPLACE_CUSTOMER_PHOTO = gql`
|
|
mutation replacePhoto(
|
|
$customerId: ID!
|
|
$photoType: String
|
|
$newPhoto: Upload
|
|
) {
|
|
replacePhoto(
|
|
customerId: $customerId
|
|
photoType: $photoType
|
|
newPhoto: $newPhoto
|
|
) {
|
|
id
|
|
newPhoto
|
|
photoType
|
|
}
|
|
}
|
|
`
|
|
|
|
const DELETE_EDITED_CUSTOMER = gql`
|
|
mutation deleteEditedData($customerId: ID!, $customerEdit: CustomerEdit) {
|
|
deleteEditedData(customerId: $customerId, customerEdit: $customerEdit) {
|
|
id
|
|
frontCameraPath
|
|
idCardData
|
|
idCardPhotoPath
|
|
usSsn
|
|
}
|
|
}
|
|
`
|
|
|
|
const SET_AUTHORIZED_REQUEST = gql`
|
|
mutation setAuthorizedCustomRequest(
|
|
$customerId: ID!
|
|
$infoRequestId: ID!
|
|
$override: String!
|
|
) {
|
|
setAuthorizedCustomRequest(
|
|
customerId: $customerId
|
|
infoRequestId: $infoRequestId
|
|
override: $override
|
|
)
|
|
}
|
|
`
|
|
|
|
const SET_CUSTOMER_CUSTOM_INFO_REQUEST = gql`
|
|
mutation setCustomerCustomInfoRequest(
|
|
$customerId: ID!
|
|
$infoRequestId: ID!
|
|
$data: JSON!
|
|
) {
|
|
setCustomerCustomInfoRequest(
|
|
customerId: $customerId
|
|
infoRequestId: $infoRequestId
|
|
data: $data
|
|
)
|
|
}
|
|
`
|
|
|
|
const CREATE_NOTE = gql`
|
|
mutation createCustomerNote(
|
|
$customerId: ID!
|
|
$title: String!
|
|
$content: String!
|
|
) {
|
|
createCustomerNote(
|
|
customerId: $customerId
|
|
title: $title
|
|
content: $content
|
|
)
|
|
}
|
|
`
|
|
|
|
const DELETE_NOTE = gql`
|
|
mutation deleteCustomerNote($noteId: ID!) {
|
|
deleteCustomerNote(noteId: $noteId)
|
|
}
|
|
`
|
|
|
|
const EDIT_NOTE = gql`
|
|
mutation editCustomerNote($noteId: ID!, $newContent: String!) {
|
|
editCustomerNote(noteId: $noteId, newContent: $newContent)
|
|
}
|
|
`
|
|
|
|
const ENABLE_TEST_CUSTOMER = gql`
|
|
mutation enableTestCustomer($customerId: ID!) {
|
|
enableTestCustomer(customerId: $customerId)
|
|
}
|
|
`
|
|
|
|
const DISABLE_TEST_CUSTOMER = gql`
|
|
mutation disableTestCustomer($customerId: ID!) {
|
|
disableTestCustomer(customerId: $customerId)
|
|
}
|
|
`
|
|
|
|
const GET_DATA = gql`
|
|
query getData {
|
|
config
|
|
}
|
|
`
|
|
|
|
const SET_CUSTOM_ENTRY = gql`
|
|
mutation addCustomField($customerId: ID!, $label: String!, $value: String!) {
|
|
addCustomField(customerId: $customerId, label: $label, value: $value)
|
|
}
|
|
`
|
|
|
|
const EDIT_CUSTOM_ENTRY = gql`
|
|
mutation saveCustomField($customerId: ID!, $fieldId: ID!, $value: String!) {
|
|
saveCustomField(customerId: $customerId, fieldId: $fieldId, value: $value)
|
|
}
|
|
`
|
|
|
|
const GET_ACTIVE_CUSTOM_REQUESTS = gql`
|
|
query customInfoRequests($onlyEnabled: Boolean) {
|
|
customInfoRequests(onlyEnabled: $onlyEnabled) {
|
|
id
|
|
customRequest
|
|
}
|
|
}
|
|
`
|
|
|
|
const CustomerProfile = memo(() => {
|
|
const history = useHistory()
|
|
|
|
const [showCompliance, setShowCompliance] = useState(false)
|
|
const [wizard, setWizard] = useState(false)
|
|
const [error] = useState(null)
|
|
const [clickedItem, setClickedItem] = useState('overview')
|
|
const { id: customerId } = useParams()
|
|
|
|
const {
|
|
data: customerResponse,
|
|
refetch: getCustomer,
|
|
loading: customerLoading
|
|
} = useQuery(GET_CUSTOMER, {
|
|
variables: { customerId }
|
|
})
|
|
|
|
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
|
|
|
const { data: activeCustomRequests } = useQuery(GET_ACTIVE_CUSTOM_REQUESTS, {
|
|
variables: {
|
|
onlyEnabled: true
|
|
}
|
|
})
|
|
|
|
const [setCustomEntry] = useMutation(SET_CUSTOM_ENTRY, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [editCustomEntry] = useMutation(EDIT_CUSTOM_ENTRY, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [replaceCustomerPhoto] = useMutation(REPLACE_CUSTOMER_PHOTO, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [editCustomerData] = useMutation(EDIT_CUSTOMER, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [deleteCustomerEditedData] = useMutation(DELETE_EDITED_CUSTOMER, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [setCustomer] = useMutation(SET_CUSTOMER, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [authorizeCustomRequest] = useMutation(SET_AUTHORIZED_REQUEST, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [setCustomerCustomInfoRequest] = useMutation(
|
|
SET_CUSTOMER_CUSTOM_INFO_REQUEST,
|
|
{
|
|
onCompleted: () => getCustomer()
|
|
}
|
|
)
|
|
|
|
const [createNote] = useMutation(CREATE_NOTE, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [deleteNote] = useMutation(DELETE_NOTE, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [editNote] = useMutation(EDIT_NOTE, {
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const saveCustomEntry = it => {
|
|
setCustomEntry({
|
|
variables: {
|
|
customerId,
|
|
label: it.title,
|
|
value: it.data
|
|
}
|
|
})
|
|
setWizard(null)
|
|
}
|
|
|
|
const updateCustomEntry = it => {
|
|
editCustomEntry({
|
|
variables: {
|
|
customerId,
|
|
fieldId: it.fieldId,
|
|
value: it.value
|
|
}
|
|
})
|
|
}
|
|
|
|
const [enableTestCustomer] = useMutation(ENABLE_TEST_CUSTOMER, {
|
|
variables: { customerId },
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const [disableTestCustomer] = useMutation(DISABLE_TEST_CUSTOMER, {
|
|
variables: { customerId },
|
|
onCompleted: () => getCustomer()
|
|
})
|
|
|
|
const updateCustomer = it =>
|
|
setCustomer({
|
|
variables: {
|
|
customerId,
|
|
customerInput: it
|
|
}
|
|
})
|
|
|
|
const replacePhoto = it => {
|
|
replaceCustomerPhoto({
|
|
variables: {
|
|
customerId,
|
|
newPhoto: it.newPhoto,
|
|
photoType: it.photoType
|
|
}
|
|
})
|
|
setWizard(null)
|
|
}
|
|
|
|
const editCustomer = it => {
|
|
editCustomerData({
|
|
variables: {
|
|
customerId,
|
|
customerEdit: it
|
|
}
|
|
})
|
|
setWizard(null)
|
|
}
|
|
|
|
const deleteEditedData = it =>
|
|
deleteCustomerEditedData({
|
|
variables: {
|
|
customerId,
|
|
customerEdit: it
|
|
}
|
|
})
|
|
|
|
const createCustomerNote = it =>
|
|
createNote({
|
|
variables: {
|
|
customerId,
|
|
title: it.title,
|
|
content: it.content
|
|
}
|
|
})
|
|
|
|
const deleteCustomerNote = it =>
|
|
deleteNote({
|
|
variables: {
|
|
noteId: it.noteId
|
|
}
|
|
})
|
|
|
|
const editCustomerNote = it =>
|
|
editNote({
|
|
variables: {
|
|
noteId: it.noteId,
|
|
newContent: it.newContent
|
|
}
|
|
})
|
|
|
|
const retrieveAdditionalData = () =>
|
|
setCustomer({
|
|
variables: {
|
|
customerId,
|
|
customerInput: {
|
|
subscriberInfo: true
|
|
}
|
|
}
|
|
})
|
|
|
|
const onClickSidebarItem = code => setClickedItem(code)
|
|
|
|
const configData = R.path(['config'])(customerResponse) ?? []
|
|
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
|
|
const customerData = R.path(['customer'])(customerResponse) ?? []
|
|
const rawTransactions = R.path(['transactions'])(customerData) ?? []
|
|
const sortedTransactions = R.sort(R.descend(R.prop('cryptoAtoms')))(
|
|
rawTransactions
|
|
)
|
|
const name = getName(customerData)
|
|
const blocked =
|
|
R.path(['authorizedOverride'])(customerData) === OVERRIDE_REJECTED
|
|
|
|
const isSuspended = customerData.isSuspended
|
|
const isCustomerData = clickedItem === 'customerData'
|
|
const isOverview = clickedItem === 'overview'
|
|
const isNotes = clickedItem === 'notes'
|
|
const isPhotos = clickedItem === 'photos'
|
|
|
|
const frontCameraData = R.pick(['frontCameraPath', 'frontCameraAt'])(
|
|
customerData
|
|
)
|
|
const txPhotosData =
|
|
sortedTransactions &&
|
|
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
|
|
sortedTransactions
|
|
)
|
|
|
|
const photosData = formatPhotosData(R.append(frontCameraData, txPhotosData))
|
|
|
|
const loading = customerLoading || configLoading
|
|
|
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
|
|
|
const customInfoRequirementOptions =
|
|
activeCustomRequests?.customInfoRequests?.map(it => ({
|
|
value: it.id,
|
|
display: it.customRequest.name
|
|
})) ?? []
|
|
|
|
const classes = useStyles()
|
|
|
|
return (
|
|
<>
|
|
<Breadcrumbs
|
|
classes={{ root: classes.breadcrumbs }}
|
|
separator={<NavigateNextIcon fontSize="small" />}
|
|
aria-label="breadcrumb">
|
|
<Label1
|
|
noMargin
|
|
className={classes.labelLink}
|
|
onClick={() => history.push('/compliance/customers')}>
|
|
Customers
|
|
</Label1>
|
|
<Label2 noMargin className={classes.labelLink}>
|
|
{name.length
|
|
? name
|
|
: getFormattedPhone(
|
|
R.path(['phone'])(customerData),
|
|
locale.country
|
|
)}
|
|
</Label2>
|
|
</Breadcrumbs>
|
|
<div className={classes.panels}>
|
|
<div className={classes.leftSidePanel}>
|
|
{!loading && !customerData.isAnonymous && (
|
|
<>
|
|
<CustomerSidebar
|
|
isSelected={code => code === clickedItem}
|
|
onClick={onClickSidebarItem}
|
|
/>
|
|
<div>
|
|
<Label1 className={classes.actionLabel}>Actions</Label1>
|
|
<div className={classes.actionBar}>
|
|
<ActionButton
|
|
className={classes.actionButton}
|
|
color="primary"
|
|
Icon={DataIcon}
|
|
InverseIcon={DataReversedIcon}
|
|
onClick={() => setWizard(true)}>
|
|
{`Manual data entry`}
|
|
</ActionButton>
|
|
{/* <ActionButton
|
|
className={classes.actionButton}
|
|
color="primary"
|
|
Icon={Discount}
|
|
InverseIcon={DiscountReversedIcon}
|
|
onClick={() => {}}>
|
|
{`Add individual discount`}
|
|
</ActionButton> */}
|
|
{isSuspended && (
|
|
<ActionButton
|
|
className={classes.actionButton}
|
|
color="primary"
|
|
Icon={AuthorizeIcon}
|
|
InverseIcon={AuthorizeReversedIcon}
|
|
onClick={() =>
|
|
updateCustomer({
|
|
suspendedUntil: null
|
|
})
|
|
}>
|
|
{`Unsuspend customer`}
|
|
</ActionButton>
|
|
)}
|
|
<ActionButton
|
|
color="primary"
|
|
className={classes.actionButton}
|
|
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
|
InverseIcon={
|
|
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
|
}
|
|
onClick={() =>
|
|
updateCustomer({
|
|
authorizedOverride: blocked
|
|
? OVERRIDE_AUTHORIZED
|
|
: OVERRIDE_REJECTED
|
|
})
|
|
}>
|
|
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
|
</ActionButton>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label1 className={classes.actionLabel}>
|
|
{`Special user status`}
|
|
</Label1>
|
|
<div className={classes.actionBar}>
|
|
<div className={classes.userStatusAction}>
|
|
<Switch
|
|
checked={!!R.path(['isTestCustomer'])(customerData)}
|
|
value={!!R.path(['isTestCustomer'])(customerData)}
|
|
onChange={() =>
|
|
R.path(['isTestCustomer'])(customerData)
|
|
? disableTestCustomer()
|
|
: enableTestCustomer()
|
|
}
|
|
/>
|
|
{`Test user`}
|
|
</div>
|
|
</div>
|
|
<ActionButton
|
|
color="primary"
|
|
className={classes.actionButton}
|
|
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.rightSidePanel}>
|
|
{isOverview && (
|
|
<div>
|
|
<Box
|
|
className={classes.customerDetails}
|
|
display="flex"
|
|
justifyContent="space-between">
|
|
<CustomerDetails
|
|
customer={customerData}
|
|
photosData={photosData}
|
|
locale={locale}
|
|
setShowCompliance={() => setShowCompliance(!showCompliance)}
|
|
/>
|
|
</Box>
|
|
<div>
|
|
<TransactionsList
|
|
customer={customerData}
|
|
data={sortedTransactions}
|
|
locale={locale}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isCustomerData && (
|
|
<div>
|
|
<CustomerData
|
|
locale={locale}
|
|
customer={customerData}
|
|
updateCustomer={updateCustomer}
|
|
replacePhoto={replacePhoto}
|
|
editCustomer={editCustomer}
|
|
deleteEditedData={deleteEditedData}
|
|
updateCustomRequest={setCustomerCustomInfoRequest}
|
|
authorizeCustomRequest={authorizeCustomRequest}
|
|
updateCustomEntry={updateCustomEntry}
|
|
retrieveAdditionalData={retrieveAdditionalData}></CustomerData>
|
|
</div>
|
|
)}
|
|
{isNotes && (
|
|
<div>
|
|
<CustomerNotes
|
|
customer={customerData}
|
|
createNote={createCustomerNote}
|
|
deleteNote={deleteCustomerNote}
|
|
editNote={editCustomerNote}
|
|
timezone={timezone}></CustomerNotes>
|
|
</div>
|
|
)}
|
|
{isPhotos && (
|
|
<div>
|
|
<CustomerPhotos photosData={photosData} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{wizard && (
|
|
<Wizard
|
|
error={error?.message}
|
|
save={saveCustomEntry}
|
|
addPhoto={replacePhoto}
|
|
addCustomerData={editCustomer}
|
|
onClose={() => setWizard(null)}
|
|
customInfoRequirementOptions={customInfoRequirementOptions}
|
|
/>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
})
|
|
|
|
export default CustomerProfile
|