lamassu-server/new-lamassu-admin/src/pages/Transactions/Transactions.js
2021-12-13 19:30:39 +00:00

376 lines
9.3 KiB
JavaScript

import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import gql from 'graphql-tag'
import { utils as coinUtils } from 'lamassu-coins'
import * as R from 'ramda'
import React, { useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
import SearchBox from 'src/components/SearchBox'
import SearchFilter from 'src/components/SearchFilter'
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 { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { ReactComponent as CustomerLinkWhiteIcon } from 'src/styling/icons/month arrows/right_white.svg'
import { errorColor } from 'src/styling/variables'
import { formatDate } from 'src/utils/timezones'
import DetailsRow from './DetailsCard'
import { mainStyles } from './Transactions.styles'
import { getStatus } from './helper'
const useStyles = makeStyles(mainStyles)
const NUM_LOG_RESULTS = 1000
const GET_DATA = gql`
query getData {
config
}
`
const GET_TRANSACTIONS_CSV = gql`
query transactions(
$simplified: Boolean
$limit: Int
$from: Date
$until: Date
$timezone: String
) {
transactionsCsv(
simplified: $simplified
limit: $limit
from: $from
until: $until
timezone: $timezone
)
}
`
const GET_TRANSACTION_FILTERS = gql`
query filters {
transactionFilters {
type
value
}
}
`
const GET_TRANSACTIONS = gql`
query transactions(
$limit: Int
$from: Date
$until: Date
$txClass: String
$machineName: String
$customerName: String
$fiatCode: String
$cryptoCode: String
$toAddress: String
$status: String
) {
transactions(
limit: $limit
from: $from
until: $until
txClass: $txClass
machineName: $machineName
customerName: $customerName
fiatCode: $fiatCode
cryptoCode: $cryptoCode
toAddress: $toAddress
status: $status
) {
id
txClass
txHash
toAddress
commissionPercentage
expired
machineName
operatorCompleted
sendConfirmed
dispense
hasError: error
deviceId
fiat
cashInFee
fiatCode
cryptoAtoms
cryptoCode
toAddress
created
customerName
customerIdCardData
customerIdCardPhotoPath
customerFrontCameraPath
customerPhone
discount
customerId
isAnonymous
batched
batchTime
}
}
`
const getFiltersObj = filters =>
R.reduce((s, f) => ({ ...s, [f.type]: f.value }), {}, filters)
const Transactions = () => {
const classes = useStyles()
const history = useHistory()
const [filters, setFilters] = useState([])
const { data: filtersResponse, loading: filtersLoading } = useQuery(
GET_TRANSACTION_FILTERS
)
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
const {
data: txData,
loading: transactionsLoading,
refetch,
startPolling,
stopPolling
} = useQuery(GET_TRANSACTIONS, { variables })
useEffect(() => {
startPolling(10000)
return stopPolling
})
const txList = txData?.transactions ?? []
const { data: configResponse, configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse)
const redirect = customerId => {
return history.push(`/compliance/customer/${customerId}`)
}
const formatCustomerName = customer => {
const { firstName, lastName } = customer
return `${R.o(R.toUpper, R.head)(firstName)}. ${lastName}`
}
const getCustomerDisplayName = tx => {
if (tx.isAnonymous) return 'Anonymous'
if (tx.customerName) return tx.customerName
if (tx.customerIdCardData) return formatCustomerName(tx.customerIdCardData)
return tx.customerPhone
}
const elements = [
{
header: '',
width: 32,
size: 'sm',
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
},
{
header: 'Machine',
name: 'machineName',
width: 160,
size: 'sm',
view: R.path(['machineName'])
},
{
header: 'Customer',
width: 202,
size: 'sm',
view: it => (
<div className={classes.flexWrapper}>
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
{!it.isAnonymous && (
<div onClick={() => redirect(it.customerId)}>
{it.hasError ? (
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
) : (
<CustomerLinkIcon className={classes.customerLinkIcon} />
)}
</div>
)}
</div>
)
},
{
header: 'Cash',
width: 144,
textAlign: 'right',
size: 'sm',
view: it => `${Number.parseFloat(it.fiat)} ${it.fiatCode}`
},
{
header: 'Crypto',
width: 150,
textAlign: 'right',
size: 'sm',
view: it =>
`${coinUtils.toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode)} ${
it.cryptoCode
}`
},
{
header: 'Address',
view: it => coinUtils.formatCryptoAddress(it.cryptoCode, it.toAddress),
className: classes.overflowTd,
size: 'sm',
width: 140
},
{
header: 'Date (UTC)',
view: it =>
timezone && formatDate(it.created, timezone, 'yyyy-MM-dd HH:mm:ss'),
textAlign: 'right',
size: 'sm',
width: 195
},
{
header: 'Status',
view: it => getStatus(it),
textAlign: 'left',
size: 'sm',
width: 80
}
]
const onFilterChange = filters => {
const filtersObject = getFiltersObj(filters)
setFilters(filters)
setVariables({
limit: NUM_LOG_RESULTS,
txClass: filtersObject.type,
machineName: filtersObject.machine,
customerName: filtersObject.customer,
fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address,
status: filtersObject.status
})
refetch && refetch()
}
const onFilterDelete = filter => {
const newFilters = R.filter(
f => !R.whereEq(R.pick(['type', 'value'], f), filter)
)(filters)
setFilters(newFilters)
const filtersObject = getFiltersObj(newFilters)
setVariables({
limit: NUM_LOG_RESULTS,
txClass: filtersObject.type,
machineName: filtersObject.machine,
customerName: filtersObject.customer,
fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address,
status: filtersObject.status
})
refetch && refetch()
}
const deleteAllFilters = () => {
setFilters([])
const filtersObject = getFiltersObj([])
setVariables({
limit: NUM_LOG_RESULTS,
txClass: filtersObject.type,
machineName: filtersObject.machine,
customerName: filtersObject.customer,
fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address,
status: filtersObject.status
})
refetch && refetch()
}
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
const loading = transactionsLoading || filtersLoading || configLoading
const errorLabel = (
<svg width={12} height={12}>
<rect width={12} height={12} rx={3} fill={errorColor} />
</svg>
)
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Transactions</Title>
<div className={classes.buttonsWrapper}>
<SearchBox
loading={filtersLoading}
filters={filters}
options={filterOptions}
inputPlaceholder={'Search Transactions'}
onChange={onFilterChange}
/>
</div>
{txList && (
<div className={classes.buttonsWrapper}>
<LogsDowloaderPopover
title="Download logs"
name="transactions"
query={GET_TRANSACTIONS_CSV}
getLogs={logs => R.path(['transactionsCsv'])(logs)}
simplified
timezone={timezone}
args={{ timezone }}
/>
</div>
)}
</div>
<div className={classes.headerLabels}>
<div>
<TxInIcon />
<span>Cash-in</span>
</div>
<div>
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
{errorLabel}
<span>Transaction error</span>
</div>
</div>
</div>
{filters.length > 0 && (
<SearchFilter
entries={txList.length}
filters={filters}
onFilterDelete={onFilterDelete}
deleteAllFilters={deleteAllFilters}
/>
)}
<DataTable
loading={loading}
emptyText="No transactions so far"
elements={elements}
data={txList}
Details={DetailsRow}
expandable
rowSize="sm"
timezone={timezone}
/>
</>
)
}
export default Transactions