feat: created the search component
style: added spec styles fix: fixed font color on search input style: added box-shadow to the search component feat: added local search functionality to the search component feat: integrated search component into the transactions page feat: allow multiple filter selection on the search component fix: let the user select only one filter for each type feat: added chips for the selected filters on the transactions page feat: added the remove function on the filter chips style: styled items according to spec refactor: simplified search component (moved logic to the outside) feat: added transaction filters to the gql query feat: added a 'clear all filters' button feat: added a filters query feat: added a gql query for the transaction filters fix: fixed the transactions gql query so it haves the same options as the transaction filters feat: added a 'loading' feature to the search box (shown while loading the filters) fix: fetch transactions and filters separately in the transactions page fix: style export fix: packages fix: transaction conflicts
This commit is contained in:
parent
468f2cb28b
commit
852bf7b089
14 changed files with 1343 additions and 888 deletions
|
|
@ -16,7 +16,16 @@ const cashInLow = require('./cash-in-low')
|
||||||
const PENDING_INTERVAL = '60 minutes'
|
const PENDING_INTERVAL = '60 minutes'
|
||||||
const MAX_PENDING = 10
|
const MAX_PENDING = 10
|
||||||
|
|
||||||
module.exports = { post, monitorPending, cancel, PENDING_INTERVAL }
|
const TRANSACTION_STATES = `
|
||||||
|
case
|
||||||
|
when operator_completed then 'Cancelled'
|
||||||
|
when error is not null then 'Error'
|
||||||
|
when send_confirmed then 'Sent'
|
||||||
|
when ((not send_confirmed) and (created <= now() - interval '${PENDING_INTERVAL}')) then 'Expired'
|
||||||
|
else 'Pending'
|
||||||
|
end`
|
||||||
|
|
||||||
|
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL, TRANSACTION_STATES}
|
||||||
|
|
||||||
function post (machineTx, pi) {
|
function post (machineTx, pi) {
|
||||||
return cashInAtomic.atomic(machineTx, pi)
|
return cashInAtomic.atomic(machineTx, pi)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,15 @@ const BN = require('../bn')
|
||||||
|
|
||||||
const REDEEMABLE_AGE = T.day
|
const REDEEMABLE_AGE = T.day
|
||||||
|
|
||||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE }
|
const CASH_OUT_TRANSACTION_STATES = `
|
||||||
|
case
|
||||||
|
when error is not null then 'Error'
|
||||||
|
when dispense then 'Success'
|
||||||
|
when (extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) >= ${REDEEMABLE_AGE} then 'Expired'
|
||||||
|
else 'Pending'
|
||||||
|
end`
|
||||||
|
|
||||||
|
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||||
|
|
||||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||||
|
|
||||||
|
|
|
||||||
30
lib/new-admin/filters.js
Normal file
30
lib/new-admin/filters.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
const db = require('../db')
|
||||||
|
const cashInTx = require('../cash-in/cash-in-tx')
|
||||||
|
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
||||||
|
|
||||||
|
function transaction() {
|
||||||
|
const sql = `select distinct * from (
|
||||||
|
select 'type' as type, 'Cash In' as value union
|
||||||
|
select 'type' as type, 'Cash Out' as value union
|
||||||
|
select 'machine' as type, name as value from devices d inner join cash_in_txs t on d.device_id = t.device_id union
|
||||||
|
select 'machine' as type, name as value from devices d inner join cash_out_txs t on d.device_id = t.device_id union
|
||||||
|
select 'customer' as type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') as value
|
||||||
|
from customers c inner join cash_in_txs t on c.id = t.customer_id
|
||||||
|
where c.id_card_data::json->>'firstName' is not null or c.id_card_data::json->>'lastName' is not null union
|
||||||
|
select 'customer' as type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') as value
|
||||||
|
from customers c inner join cash_out_txs t on c.id = t.customer_id
|
||||||
|
where c.id_card_data::json->>'firstName' is not null or c.id_card_data::json->>'lastName' is not null union
|
||||||
|
select 'fiat' as type, fiat_code as value from cash_in_txs union
|
||||||
|
select 'fiat' as type, fiat_code as value from cash_out_txs union
|
||||||
|
select 'crypto' as type, crypto_code as value from cash_in_txs union
|
||||||
|
select 'crypto' as type, crypto_code as value from cash_out_txs union
|
||||||
|
select 'address' as type, to_address as value from cash_in_txs union
|
||||||
|
select 'address' as type, to_address as value from cash_in_txs union
|
||||||
|
select 'status' as type, ${cashInTx.TRANSACTION_STATES} as value from cash_in_txs union
|
||||||
|
select 'status' as type, ${CASH_OUT_TRANSACTION_STATES} as value from cash_out_txs
|
||||||
|
) f`
|
||||||
|
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { transaction }
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const DataLoader = require('dataloader')
|
const DataLoader = require('dataloader')
|
||||||
const { parseAsync } = require('json2csv')
|
const { parseAsync } = require('json2csv')
|
||||||
|
|
||||||
|
const filters = require('../../filters')
|
||||||
const transactions = require('../../services/transactions')
|
const transactions = require('../../services/transactions')
|
||||||
const anonymous = require('../../../constants').anonymousCustomer
|
const anonymous = require('../../../constants').anonymousCustomer
|
||||||
|
|
||||||
|
|
@ -25,14 +26,16 @@ const resolvers = {
|
||||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
transactions: (...[, { from, until, limit, offset, deviceId }]) =>
|
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||||
transactions.batch(from, until, limit, offset, deviceId),
|
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
||||||
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||||
transactions.batch(from, until, limit, offset).then(data => parseAsync(data, { fields: txLogFields })),
|
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status)
|
||||||
|
.then(data => parseAsync(data, {fields: tx_logFields})),
|
||||||
transactionCsv: (...[, { id, txClass }]) =>
|
transactionCsv: (...[, { id, txClass }]) =>
|
||||||
transactions.getTx(id, txClass).then(parseAsync),
|
transactions.getTx(id, txClass).then(parseAsync),
|
||||||
txAssociatedDataCsv: (...[, { id, txClass }]) =>
|
txAssociatedDataCsv: (...[, { id, txClass }]) =>
|
||||||
transactions.getTxAssociatedData(id, txClass).then(parseAsync)
|
transactions.getTxAssociatedData(id, txClass).then(parseAsync),
|
||||||
|
transactionFilters: () => filters.transaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,17 @@ const typeDef = gql`
|
||||||
discount: Int
|
discount: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Filter {
|
||||||
|
type: String
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID): [Transaction] @auth
|
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): [Transaction] @auth
|
||||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String @auth
|
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): String @auth
|
||||||
transactionCsv(id: ID, txClass: String): String @auth
|
transactionCsv(id: ID, txClass: String): String @auth
|
||||||
txAssociatedDataCsv(id: ID, txClass: String): String @auth
|
txAssociatedDataCsv(id: ID, txClass: String): String @auth
|
||||||
|
transactionFilters: [Filter] @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const db = require('../../db')
|
||||||
const machineLoader = require('../../machine-loader')
|
const machineLoader = require('../../machine-loader')
|
||||||
const tx = require('../../tx')
|
const tx = require('../../tx')
|
||||||
const cashInTx = require('../../cash-in/cash-in-tx')
|
const cashInTx = require('../../cash-in/cash-in-tx')
|
||||||
const { REDEEMABLE_AGE } = require('../../cash-out/cash-out-helper')
|
const { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/cash-out-helper')
|
||||||
|
|
||||||
const NUM_RESULTS = 1000
|
const NUM_RESULTS = 1000
|
||||||
|
|
||||||
|
|
@ -24,7 +24,20 @@ function addNames (txs) {
|
||||||
|
|
||||||
const camelize = _.mapKeys(_.camelCase)
|
const camelize = _.mapKeys(_.camelCase)
|
||||||
|
|
||||||
function batch (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0, id = null) {
|
function batch (
|
||||||
|
from = new Date(0).toISOString(),
|
||||||
|
until = new Date().toISOString(),
|
||||||
|
limit = null,
|
||||||
|
offset = 0,
|
||||||
|
id = null,
|
||||||
|
txClass = null,
|
||||||
|
machineName = null,
|
||||||
|
customerName = null,
|
||||||
|
fiatCode = null,
|
||||||
|
cryptoCode = null,
|
||||||
|
toAddress = null,
|
||||||
|
status = null
|
||||||
|
) {
|
||||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||||
|
|
||||||
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
||||||
|
|
@ -32,15 +45,23 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
||||||
c.id_card_data_number as customer_id_card_data_number,
|
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_expiration as customer_id_card_data_expiration,
|
||||||
c.id_card_data as customer_id_card_data,
|
c.id_card_data as customer_id_card_data,
|
||||||
c.name as customer_name,
|
concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') as customer_name,
|
||||||
c.front_camera_path as customer_front_camera_path,
|
c.front_camera_path as customer_front_camera_path,
|
||||||
c.id_card_photo_path as customer_id_card_photo_path,
|
c.id_card_photo_path as customer_id_card_photo_path,
|
||||||
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
|
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
|
||||||
from cash_in_txs as txs
|
from (select *, ${cashInTx.TRANSACTION_STATES} as txStatus from cash_in_txs) as txs
|
||||||
left outer join customers c on txs.customer_id = c.id
|
left outer join customers c on txs.customer_id = c.id
|
||||||
|
inner join devices d on txs.device_id = d.device_id
|
||||||
where txs.created >= $2 and txs.created <= $3 ${
|
where txs.created >= $2 and txs.created <= $3 ${
|
||||||
id !== null ? `and txs.device_id = $6` : ``
|
id !== null ? `and txs.device_id = $6` : ``
|
||||||
}
|
}
|
||||||
|
and ($7 is null or $7 = 'Cash In')
|
||||||
|
and ($8 is null or d.name = $8)
|
||||||
|
and ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||||
|
and ($10 is null or txs.fiat_code = $10)
|
||||||
|
and ($11 is null or txs.crypto_code = $11)
|
||||||
|
and ($12 is null or txs.to_address = $12)
|
||||||
|
and ($13 is null or txs.txStatus = $13)
|
||||||
order by created desc limit $4 offset $5`
|
order by created desc limit $4 offset $5`
|
||||||
|
|
||||||
const cashOutSql = `select 'cashOut' as tx_class,
|
const cashOutSql = `select 'cashOut' as tx_class,
|
||||||
|
|
@ -50,22 +71,30 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
||||||
c.id_card_data_number as customer_id_card_data_number,
|
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_expiration as customer_id_card_data_expiration,
|
||||||
c.id_card_data as customer_id_card_data,
|
c.id_card_data as customer_id_card_data,
|
||||||
c.name as customer_name,
|
concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') as customer_name,
|
||||||
c.front_camera_path as customer_front_camera_path,
|
c.front_camera_path as customer_front_camera_path,
|
||||||
c.id_card_photo_path as customer_id_card_photo_path,
|
c.id_card_photo_path as customer_id_card_photo_path,
|
||||||
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 as expired
|
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 as expired
|
||||||
from cash_out_txs txs
|
from (select *, ${CASH_OUT_TRANSACTION_STATES} as txStatus from cash_out_txs) txs
|
||||||
inner join cash_out_actions actions on txs.id = actions.tx_id
|
inner join cash_out_actions actions on txs.id = actions.tx_id
|
||||||
and actions.action = 'provisionAddress'
|
and actions.action = 'provisionAddress'
|
||||||
left outer join customers c on txs.customer_id = c.id
|
left outer join customers c on txs.customer_id = c.id
|
||||||
|
inner join devices d on txs.device_id = d.device_id
|
||||||
where txs.created >= $2 and txs.created <= $3 ${
|
where txs.created >= $2 and txs.created <= $3 ${
|
||||||
id !== null ? `and txs.device_id = $6` : ``
|
id !== null ? `and txs.device_id = $6` : ``
|
||||||
}
|
}
|
||||||
|
and ($7 is null or $7 = 'Cash Out')
|
||||||
|
and ($8 is null or d.name = $8)
|
||||||
|
and ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||||
|
and ($10 is null or txs.fiat_code = $10)
|
||||||
|
and ($11 is null or txs.crypto_code = $11)
|
||||||
|
and ($12 is null or txs.to_address = $12)
|
||||||
|
and ($13 is null or txs.txStatus = $13)
|
||||||
order by created desc limit $4 offset $5`
|
order by created desc limit $4 offset $5`
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id]),
|
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
||||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id])
|
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
||||||
])
|
])
|
||||||
.then(packager)
|
.then(packager)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
719
new-lamassu-admin/package-lock.json
generated
719
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
91
new-lamassu-admin/src/components/SearchBox.js
Normal file
91
new-lamassu-admin/src/components/SearchBox.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import InputBase from '@material-ui/core/InputBase'
|
||||||
|
import Paper from '@material-ui/core/Paper'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import React, { memo, useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
import { P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as SearchIcon } from 'src/styling/icons/circle buttons/search/zodiac.svg'
|
||||||
|
|
||||||
|
import styles from './SearchBox.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const SearchBox = memo(
|
||||||
|
({
|
||||||
|
loading = false,
|
||||||
|
filters = [],
|
||||||
|
options = [],
|
||||||
|
inputPlaceholder = '',
|
||||||
|
size,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles({ size })
|
||||||
|
|
||||||
|
const [popupOpen, setPopupOpen] = useState(false)
|
||||||
|
|
||||||
|
const inputClasses = {
|
||||||
|
[classes.input]: true,
|
||||||
|
[classes.inputWithPopup]: popupOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerOnChange = filters => onChange(filters)
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
useEffect(() => innerOnChange(filters), [filters])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MAutocomplete
|
||||||
|
loading={loading}
|
||||||
|
classes={{ option: classes.autocomplete }}
|
||||||
|
value={filters}
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={it => it.value}
|
||||||
|
renderOption={it => (
|
||||||
|
<div className={classes.item}>
|
||||||
|
<P className={classes.itemLabel}>{it.value}</P>
|
||||||
|
<P className={classes.itemType}>{it.type}</P>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
autoHighlight
|
||||||
|
disableClearable
|
||||||
|
clearOnEscape
|
||||||
|
multiple
|
||||||
|
filterSelectedOptions
|
||||||
|
getOptionSelected={(option, value) => option.type === value.type}
|
||||||
|
PaperComponent={({ children }) => (
|
||||||
|
<Paper elevation={0} className={classes.popup}>
|
||||||
|
<div className={classes.separator} />
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
renderInput={params => {
|
||||||
|
return (
|
||||||
|
<InputBase
|
||||||
|
ref={params.InputProps.ref}
|
||||||
|
{...params}
|
||||||
|
className={classnames(inputClasses)}
|
||||||
|
startAdornment={<SearchIcon className={classes.iconButton} />}
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
|
inputProps={{
|
||||||
|
className: classes.bold,
|
||||||
|
classes: {
|
||||||
|
root: classes.size
|
||||||
|
},
|
||||||
|
...params.inputProps
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onOpen={() => setPopupOpen(true)}
|
||||||
|
onClose={() => setPopupOpen(false)}
|
||||||
|
onChange={(_, filters) => innerOnChange(filters)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SearchBox
|
||||||
78
new-lamassu-admin/src/components/SearchBox.styles.js
Normal file
78
new-lamassu-admin/src/components/SearchBox.styles.js
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import baseButtonStyles from 'src/components/buttons/BaseButton.styles'
|
||||||
|
import { bySize, bold } from 'src/styling/helpers'
|
||||||
|
import { zircon, comet, primaryColor } from 'src/styling/variables'
|
||||||
|
|
||||||
|
const { baseButton } = baseButtonStyles
|
||||||
|
|
||||||
|
const searchBoxBorderRadius = baseButton.height / 2
|
||||||
|
const searchBoxHeight = 32
|
||||||
|
const popupBorderRadiusFocus = baseButton.height / 4
|
||||||
|
|
||||||
|
const hoverColor = 'rgba(0, 0, 0, 0.08)'
|
||||||
|
const boxShadow = `0 4px 4px 0 ${hoverColor}`
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
size: ({ size }) => ({
|
||||||
|
marginTop: size === 'lg' ? 0 : 2,
|
||||||
|
...bySize(size)
|
||||||
|
}),
|
||||||
|
bold,
|
||||||
|
autocomplete: {
|
||||||
|
'&[data-focus="true"]': {
|
||||||
|
backgroundColor: hoverColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popup: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderRadius: [[0, 0, popupBorderRadiusFocus, popupBorderRadiusFocus]],
|
||||||
|
backgroundColor: zircon,
|
||||||
|
boxShadow
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
width: '88%',
|
||||||
|
height: 1,
|
||||||
|
margin: '0 auto',
|
||||||
|
border: 'solid 0.5px',
|
||||||
|
borderColor: comet
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
width: '100%',
|
||||||
|
height: 36,
|
||||||
|
alignItems: 'center'
|
||||||
|
},
|
||||||
|
itemLabel: {
|
||||||
|
margin: [0],
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
},
|
||||||
|
itemType: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
fontSize: 12,
|
||||||
|
color: comet,
|
||||||
|
margin: [0]
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
width: 273,
|
||||||
|
padding: [[8, 12]],
|
||||||
|
alignItems: 'center',
|
||||||
|
height: searchBoxHeight,
|
||||||
|
borderRadius: searchBoxBorderRadius,
|
||||||
|
backgroundColor: zircon,
|
||||||
|
color: primaryColor
|
||||||
|
},
|
||||||
|
inputWithPopup: {
|
||||||
|
borderRadius: [[popupBorderRadiusFocus, popupBorderRadiusFocus, 0, 0]],
|
||||||
|
boxShadow
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
marginRight: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default styles
|
||||||
|
|
@ -4,22 +4,27 @@ import BigNumber from 'bignumber.js'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import { utils as coinUtils } from 'lamassu-coins'
|
import { utils as coinUtils } from 'lamassu-coins'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Chip from 'src/components/Chip'
|
||||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||||
|
import SearchBox from 'src/components/SearchBox'
|
||||||
import Title from 'src/components/Title'
|
import Title from 'src/components/Title'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { P } from 'src/components/typography'
|
||||||
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
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 TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||||
import { formatDate } from 'src/utils/timezones'
|
import { formatDate } from 'src/utils/timezones'
|
||||||
|
|
||||||
import DetailsRow from './DetailsCard'
|
import DetailsRow from './DetailsCard'
|
||||||
import { mainStyles } from './Transactions.styles'
|
import { mainStyles, chipStyles } from './Transactions.styles'
|
||||||
import { getStatus } from './helper'
|
import { getStatus /*, getStatusProperties */ } from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(mainStyles)
|
const useStyles = makeStyles(mainStyles)
|
||||||
|
const useChipStyles = makeStyles(chipStyles)
|
||||||
|
|
||||||
const NUM_LOG_RESULTS = 1000
|
const NUM_LOG_RESULTS = 1000
|
||||||
|
|
||||||
|
|
@ -35,9 +40,40 @@ const GET_TRANSACTIONS_CSV = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GET_TRANSACTION_FILTERS = gql`
|
||||||
|
query filters {
|
||||||
|
transactionFilters {
|
||||||
|
type
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const GET_TRANSACTIONS = gql`
|
const GET_TRANSACTIONS = gql`
|
||||||
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
query transactions(
|
||||||
transactions(limit: $limit, from: $from, until: $until) {
|
$limit: Int
|
||||||
|
$from: DateTime
|
||||||
|
$until: DateTime
|
||||||
|
$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
|
id
|
||||||
txClass
|
txClass
|
||||||
txHash
|
txHash
|
||||||
|
|
@ -72,12 +108,23 @@ const GET_TRANSACTIONS = gql`
|
||||||
const Transactions = () => {
|
const Transactions = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { data: txResponse, loading } = useQuery(GET_TRANSACTIONS, {
|
const chipClasses = useChipStyles()
|
||||||
variables: {
|
|
||||||
limit: NUM_LOG_RESULTS
|
const [filters, setFilters] = useState([])
|
||||||
},
|
const { data: filtersResponse, loading: loadingFilters } = useQuery(
|
||||||
pollInterval: 10000
|
GET_TRANSACTION_FILTERS
|
||||||
})
|
)
|
||||||
|
const [filteredTransactions, setFilteredTransactions] = useState([])
|
||||||
|
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
|
||||||
|
const { data: txResponse, loading: loadingTransactions, refetch } = useQuery(
|
||||||
|
GET_TRANSACTIONS,
|
||||||
|
{
|
||||||
|
variables,
|
||||||
|
onCompleted: data =>
|
||||||
|
setFilteredTransactions(R.path(['transactions'])(data)),
|
||||||
|
pollInterval: 10000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
||||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||||
|
|
@ -167,11 +214,51 @@ const Transactions = () => {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const onFilterChange = filters => {
|
||||||
|
const filtersObject = R.compose(
|
||||||
|
R.mergeAll,
|
||||||
|
R.map(f => ({
|
||||||
|
[f.type]: f.value
|
||||||
|
}))
|
||||||
|
)(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 =>
|
||||||
|
setFilters(
|
||||||
|
R.filter(f => !R.whereEq(R.pick(['type', 'value'], f), filter))(filters)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classes.titleWrapper}>
|
<div className={classes.titleWrapper}>
|
||||||
<div className={classes.titleAndButtonsContainer}>
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
<Title>Transactions</Title>
|
<Title>Transactions</Title>
|
||||||
|
<div className={classes.buttonsWrapper}>
|
||||||
|
<SearchBox
|
||||||
|
loading={loadingFilters}
|
||||||
|
filters={filters}
|
||||||
|
options={filterOptions}
|
||||||
|
inputPlaceholder={'Search Transactions'}
|
||||||
|
onChange={onFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{txResponse && (
|
{txResponse && (
|
||||||
<div className={classes.buttonsWrapper}>
|
<div className={classes.buttonsWrapper}>
|
||||||
<LogsDowloaderPopover
|
<LogsDowloaderPopover
|
||||||
|
|
@ -194,11 +281,33 @@ const Transactions = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{filters.length > 0 && (
|
||||||
|
<>
|
||||||
|
<P className={classes.text}>{'Filters:'}</P>
|
||||||
|
<div>
|
||||||
|
{filters.map((f, idx) => (
|
||||||
|
<Chip
|
||||||
|
key={idx}
|
||||||
|
classes={chipClasses}
|
||||||
|
label={`${f.type}: ${f.value}`}
|
||||||
|
onDelete={() => onFilterDelete(f)}
|
||||||
|
deleteIcon={<CloseIcon className={classes.button} />}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Chip
|
||||||
|
classes={chipClasses}
|
||||||
|
label={`Delete filters`}
|
||||||
|
onDelete={() => setFilters([])}
|
||||||
|
deleteIcon={<CloseIcon className={classes.button} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DataTable
|
<DataTable
|
||||||
loading={loading && configLoading}
|
loading={loadingTransactions && configLoading}
|
||||||
emptyText="No transactions so far"
|
emptyText="No transactions so far"
|
||||||
elements={elements}
|
elements={elements}
|
||||||
data={R.path(['transactions'])(txResponse)}
|
data={filteredTransactions}
|
||||||
Details={DetailsRow}
|
Details={DetailsRow}
|
||||||
expandable
|
expandable
|
||||||
rowSize="sm"
|
rowSize="sm"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
import typographyStyles from 'src/components/typography/styles'
|
import typographyStyles from 'src/components/typography/styles'
|
||||||
import baseStyles from 'src/pages/Logs.styles'
|
import baseStyles from 'src/pages/Logs.styles'
|
||||||
import { offColor, white } from 'src/styling/variables'
|
import {
|
||||||
|
offColor,
|
||||||
|
white,
|
||||||
|
primaryColor,
|
||||||
|
zircon,
|
||||||
|
smallestFontSize,
|
||||||
|
inputFontFamily,
|
||||||
|
inputFontWeight,
|
||||||
|
spacer
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
const { label1, mono, p } = typographyStyles
|
const { label1, mono, p } = typographyStyles
|
||||||
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
|
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
|
||||||
|
|
@ -64,6 +73,10 @@ const mainStyles = {
|
||||||
titleWrapper,
|
titleWrapper,
|
||||||
titleAndButtonsContainer,
|
titleAndButtonsContainer,
|
||||||
buttonsWrapper,
|
buttonsWrapper,
|
||||||
|
text: {
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: 0
|
||||||
|
},
|
||||||
headerLabels: {
|
headerLabels: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -102,7 +115,35 @@ const mainStyles = {
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
paddingLeft: 5,
|
paddingLeft: 5,
|
||||||
paddingRight: 5
|
paddingRight: 5
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
marginLeft: 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { cpcStyles, detailsRowStyles, labelStyles, mainStyles }
|
const chipStyles = {
|
||||||
|
root: {
|
||||||
|
borderRadius: spacer / 2,
|
||||||
|
marginTop: spacer / 2,
|
||||||
|
marginRight: spacer / 4,
|
||||||
|
marginBottom: spacer / 2,
|
||||||
|
marginLeft: spacer / 4,
|
||||||
|
height: spacer * 3,
|
||||||
|
backgroundColor: zircon,
|
||||||
|
'&:hover, &:focus, &:active': {
|
||||||
|
backgroundColor: zircon
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: smallestFontSize,
|
||||||
|
fontWeight: inputFontWeight,
|
||||||
|
fontFamily: inputFontFamily,
|
||||||
|
paddingRight: spacer / 2,
|
||||||
|
paddingLeft: spacer / 2,
|
||||||
|
color: primaryColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cpcStyles, detailsRowStyles, labelStyles, mainStyles, chipStyles }
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,12 @@ const getStatusDetails = it => {
|
||||||
return it.hasError ? it.hasError : null
|
return it.hasError ? it.hasError : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getStatus, getStatusDetails }
|
const getStatusProperties = status => ({
|
||||||
|
hasError: status === 'Error' || null,
|
||||||
|
dispense: status === 'Success' || null,
|
||||||
|
expired: status === 'Expired' || null,
|
||||||
|
operatorCompleted: status === 'Cancelled' || null,
|
||||||
|
sendConfirmed: status === 'Sent' || null
|
||||||
|
})
|
||||||
|
|
||||||
|
export { getStatus, getStatusProperties, getStatusDetails }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
<title>icon/search/dark02</title>
|
||||||
<desc>Created with Sketch.</desc>
|
<g id="icon/search/dark02" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="icon/sf-small/search/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Group" transform="translate(1.000000, 1.000000)" stroke="#1B2559" stroke-width="2">
|
||||||
<path d="M15.8635238,8.17028571 C15.8635238,12.4198095 12.4187619,15.8645714 8.1692381,15.8645714 C3.92066667,15.8645714 0.475904762,12.4198095 0.475904762,8.17028571 C0.475904762,3.9207619 3.92066667,0.476 8.1692381,0.476 C12.4187619,0.476 15.8635238,3.9207619 15.8635238,8.17028571 Z" id="Stroke-1" stroke="#1B2559" stroke-width="2"></path>
|
<path d="M14.2771714,7.35325714 C14.2771714,11.1778286 11.1768857,14.2781143 7.35231429,14.2781143 C3.5286,14.2781143 0.428314286,11.1778286 0.428314286,7.35325714 C0.428314286,3.52868571 3.5286,0.4284 7.35231429,0.4284 C11.1768857,0.4284 14.2771714,3.52868571 14.2771714,7.35325714 Z" id="Stroke-1"></path>
|
||||||
<line x1="13.7035238" y1="13.7046667" x2="19.4844762" y2="19.485619" id="Stroke-3" stroke="#1B2559" stroke-width="2" stroke-linecap="round"></line>
|
<line x1="12.3331714" y1="12.3342" x2="17.5360286" y2="17.5370571" id="Stroke-3" stroke-linecap="round"></line>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 888 B |
1015
package-lock.json
generated
1015
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue