lamassu-server/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
2024-05-29 09:24:18 +01:00

771 lines
20 KiB
JavaScript

import { useQuery, useMutation } from '@apollo/react-hooks'
import {
makeStyles,
Breadcrumbs,
Box,
DialogActions,
DialogContent,
Dialog
} 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 ErrorMessage from 'src/components/ErrorMessage'
import { Button, IconButton, ActionButton } from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import { Label1, Label2, H2, Info3 } from 'src/components/typography'
import {
OVERRIDE_AUTHORIZED,
OVERRIDE_REJECTED
} from 'src/pages/Customers/components/propertyCard'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/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 { 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
email
isAnonymous
smsOverride
idCardData
idCardDataOverride
idCardDataExpiration
idCardPhotoPath
idCardPhotoOverride
idCardPhotoAt
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
email
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 [retrieve, setRetrieve] = useState(false)
const [showCompliance, setShowCompliance] = useState(false)
const [wizard, setWizard] = useState(false)
const [error, setError] = 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()
setRetrieve(false)
},
onError: error => setError(error)
})
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 IDphotoData = customerData.idCardPhotoPath
? [
{
photoDir: 'id-card-photo',
path: customerData.idCardPhotoPath,
date: customerData.idCardPhotoAt
}
]
: []
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()
const email = R.path(['email'])(customerData)
const phone = R.path(['phone'])(customerData)
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
: email?.length
? email
: getFormattedPhone(phone, 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>
</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)}
timezone={timezone}
/>
</Box>
<div>
<TransactionsList
customer={customerData}
data={sortedTransactions}
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}
setRetrieve={setRetrieve}
retrieveAdditionalDataDialog={
<RetrieveDataDialog
onDismissed={() => {
setError(null)
setRetrieve(false)
}}
onConfirmed={() => {
setError(null)
retrieveAdditionalData()
}}
error={error}
open={retrieve}></RetrieveDataDialog>
}></CustomerData>
</div>
)}
{isNotes && (
<div>
<CustomerNotes
customer={customerData}
createNote={createCustomerNote}
deleteNote={deleteCustomerNote}
editNote={editCustomerNote}
timezone={timezone}></CustomerNotes>
</div>
)}
{isPhotos && (
<div>
<CustomerPhotos
photosData={R.concat(photosData, IDphotoData)}
timezone={timezone}
/>
</div>
)}
</div>
{wizard && (
<Wizard
error={error?.message}
save={saveCustomEntry}
addPhoto={replacePhoto}
addCustomerData={editCustomer}
onClose={() => setWizard(null)}
customInfoRequirementOptions={customInfoRequirementOptions}
/>
)}
</div>
</>
)
})
const RetrieveDataDialog = ({
onConfirmed,
onDismissed,
open,
error,
props
}) => {
const classes = useStyles()
return (
<Dialog
open={open}
aria-labelledby="form-dialog-title"
PaperProps={{
style: {
borderRadius: 8,
minWidth: 656,
bottom: 125,
right: 7
}
}}
{...props}>
<div className={classes.closeButton}>
<IconButton
size={16}
aria-label="close"
onClick={() => onDismissed(false)}>
<CloseIcon />
</IconButton>
</div>
<H2 className={classes.dialogTitle}>{'Retrieve API data from Twilio'}</H2>
<DialogContent className={classes.dialogContent}>
<Info3>{`With this action you'll be using Twilio's API to retrieve additional
data from this user. This includes name and address, if available.\n`}</Info3>
<Info3>{` There is a small cost from Twilio for each retrieval. Would you like
to proceed?`}</Info3>
</DialogContent>
{error && (
<ErrorMessage className={classes.errorMessage}>
Failed to fetch additional data
</ErrorMessage>
)}
<DialogActions className={classes.dialogActions}>
<Button
backgroundColor="grey"
className={classes.cancelButton}
onClick={() => onDismissed(false)}>
Cancel
</Button>
<Button
onClick={() => {
onConfirmed()
}}>
Confirm
</Button>
</DialogActions>
</Dialog>
)
}
export default CustomerProfile