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
This commit is contained in:
parent
4320df2d61
commit
507027cdee
10 changed files with 327 additions and 4 deletions
|
|
@ -377,6 +377,37 @@ function batch () {
|
|||
}, customers)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all customers, ordered by last activity
|
||||
* and with aggregate columns based on their
|
||||
* transactions
|
||||
*
|
||||
* @returns {array} Array of customers with it's
|
||||
*/
|
||||
function getCustomersList () {
|
||||
const sql = `select name, phone, 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, 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
|
||||
limit $2`
|
||||
return db.any(sql, [ anonymous.uuid, NUM_RESULTS ])
|
||||
.then(customers => Promise.all(_.map(customer => {
|
||||
return populateOverrideUsernames(customer)
|
||||
.then(camelize)
|
||||
}, customers)))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} id customer id
|
||||
* @param {Object} patch customer update record
|
||||
|
|
@ -482,4 +513,4 @@ function updateFrontCamera (id, patch) {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = { add, get, batch, getById, update, updatePhotoCard, updateFrontCamera }
|
||||
module.exports = { add, get, batch, getCustomersList, getById, update, updatePhotoCard, updateFrontCamera }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
|||
const got = require('got')
|
||||
|
||||
const machineLoader = require('../../machine-loader')
|
||||
const customers = require('../../customers')
|
||||
const { machineAction } = require('../machines')
|
||||
const logs = require('../../logs')
|
||||
const supportLogs = require('../../support_logs')
|
||||
|
|
@ -58,6 +59,17 @@ const typeDefs = gql`
|
|||
statuses: [MachineStatus]
|
||||
}
|
||||
|
||||
type Customer {
|
||||
name: String!
|
||||
phone: String
|
||||
totalTxs: Int
|
||||
totalSpent: String
|
||||
lastActive: Date
|
||||
lastTxFiat: String
|
||||
lastTxFiatCode: String
|
||||
lastTxClass: String
|
||||
}
|
||||
|
||||
type Account {
|
||||
code: String!
|
||||
display: String!
|
||||
|
|
@ -146,6 +158,7 @@ const typeDefs = gql`
|
|||
accounts: [Account]
|
||||
cryptoCurrencies: [CryptoCurrency]
|
||||
machines: [Machine]
|
||||
customers: [Customer]
|
||||
machineLogs(deviceId: ID!): [MachineLog]
|
||||
funding: [CoinFunds]
|
||||
serverVersion: String!
|
||||
|
|
@ -190,6 +203,7 @@ const resolvers = {
|
|||
accounts: () => accounts,
|
||||
cryptoCurrencies: () => coins,
|
||||
machines: () => machineLoader.getMachineNames(),
|
||||
customers: () => customers.getCustomersList(),
|
||||
funding: () => funding.getFunding(),
|
||||
machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId),
|
||||
serverVersion: () => serverVersion,
|
||||
|
|
|
|||
86
new-lamassu-admin/src/components/dataTable/DataTable.js
Normal file
86
new-lamassu-admin/src/components/dataTable/DataTable.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React, { memo } from 'react'
|
||||
import {
|
||||
AutoSizer,
|
||||
List,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache
|
||||
} from 'react-virtualized'
|
||||
|
||||
import { THead, Th, Tr, Td } from 'src/components/fake-table/Table'
|
||||
import { mainWidth } from 'src/styling/variables'
|
||||
|
||||
const DataTable = memo(({ elements, data }) => {
|
||||
const cache = new CellMeasurerCache({
|
||||
defaultHeight: 62,
|
||||
fixedWidth: true
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<THead>
|
||||
{elements.map(({ size, className, textAlign, header }, idx) => (
|
||||
<Th
|
||||
key={idx}
|
||||
size={size}
|
||||
className={className}
|
||||
textAlign={textAlign}>
|
||||
{header}
|
||||
</Th>
|
||||
))}
|
||||
</THead>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={mainWidth}
|
||||
rowCount={data.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={({ index, isScrolling, key, parent, style }) => (
|
||||
<CellMeasurer
|
||||
cache={cache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
parent={parent}
|
||||
rowIndex={index}>
|
||||
<div style={style}>
|
||||
<Tr
|
||||
error={data[index].error}
|
||||
errorMessage={data[index].errorMessage}>
|
||||
{elements.map(
|
||||
(
|
||||
{
|
||||
header,
|
||||
size,
|
||||
className,
|
||||
textAlign,
|
||||
view = it => it?.toString()
|
||||
},
|
||||
idx
|
||||
) => (
|
||||
<Td
|
||||
key={idx}
|
||||
size={size}
|
||||
className={className}
|
||||
textAlign={textAlign}>
|
||||
{view(data[index])}
|
||||
</Td>
|
||||
)
|
||||
)}
|
||||
</Tr>
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
)}
|
||||
overscanRowCount={50}
|
||||
deferredMeasurementCache={cache}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default DataTable
|
||||
3
new-lamassu-admin/src/components/dataTable/index.js
Normal file
3
new-lamassu-admin/src/components/dataTable/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import DataTable from './DataTable'
|
||||
|
||||
export { DataTable }
|
||||
106
new-lamassu-admin/src/pages/Customers/Customers.js
Normal file
106
new-lamassu-admin/src/pages/Customers/Customers.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { gql } from 'apollo-boost'
|
||||
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
||||
|
||||
import Title from 'src/components/Title'
|
||||
import { DataTable } from 'src/components/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 GET_CUSTOMERS = gql`
|
||||
{
|
||||
customers {
|
||||
name
|
||||
phone
|
||||
totalTxs
|
||||
totalSpent
|
||||
lastActive
|
||||
lastTxFiat
|
||||
lastTxFiatCode
|
||||
lastTxClass
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Customers = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { data: customersResponse } = useQuery(GET_CUSTOMERS)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Name',
|
||||
size: 277,
|
||||
view: R.path(['name'])
|
||||
},
|
||||
{
|
||||
header: 'Phone',
|
||||
size: 166,
|
||||
view: it => parsePhoneNumberFromString(it.phone).formatInternational()
|
||||
},
|
||||
{
|
||||
header: 'Total TXs',
|
||||
size: 174,
|
||||
textAlign: 'right',
|
||||
view: it => `${Number.parseInt(it.totalTxs)}`
|
||||
},
|
||||
{
|
||||
header: 'Total spent',
|
||||
size: 188,
|
||||
textAlign: 'right',
|
||||
view: it => `${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode}`
|
||||
},
|
||||
{
|
||||
header: 'Last active',
|
||||
size: 197,
|
||||
view: it => moment.utc(it.lastActive).format('YYYY-MM-D')
|
||||
},
|
||||
{
|
||||
header: 'Last transaction',
|
||||
size: 198,
|
||||
textAlign: 'right',
|
||||
view: it => (
|
||||
<>
|
||||
{`${Number.parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode} `}
|
||||
{it.lastTxClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
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.descend('lastActive')])(
|
||||
R.path(['customers'])(customersResponse) ?? []
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Customers
|
||||
28
new-lamassu-admin/src/pages/Customers/Customers.styles.js
Normal file
28
new-lamassu-admin/src/pages/Customers/Customers.styles.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import baseStyles from 'src/pages/Logs.styles'
|
||||
|
||||
const { label1 } = typographyStyles
|
||||
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
|
||||
|
||||
const mainStyles = {
|
||||
titleWrapper,
|
||||
titleAndButtonsContainer,
|
||||
buttonsWrapper,
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
'& div': {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& > div:first-child': {
|
||||
marginRight: 24
|
||||
},
|
||||
'& span': {
|
||||
extend: label1,
|
||||
marginLeft: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { mainStyles }
|
||||
3
new-lamassu-admin/src/pages/Customers/index.js
Normal file
3
new-lamassu-admin/src/pages/Customers/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Customers from './Customers'
|
||||
|
||||
export default Customers
|
||||
|
|
@ -13,6 +13,7 @@ import AuthRegister from 'src/pages/AuthRegister'
|
|||
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
|
||||
|
||||
import MachineStatus from '../pages/maintenance/MachineStatus'
|
||||
import Customers from '../pages/Customers'
|
||||
|
||||
const tree = [
|
||||
{ key: 'transactions', label: 'Transactions', route: '/transactions' },
|
||||
|
|
@ -55,6 +56,28 @@ const tree = [
|
|||
},
|
||||
{ key: 'info', label: 'Operator Info', route: '/settings/operator-info' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'compliance',
|
||||
label: 'Compliance',
|
||||
route: '/compliance',
|
||||
children: [
|
||||
// {
|
||||
// key: 'triggers',
|
||||
// label: 'Triggers',
|
||||
// route: '/compliance/triggers'
|
||||
// },
|
||||
{
|
||||
key: 'customers',
|
||||
label: 'Customers',
|
||||
route: '/compliance/customers'
|
||||
}
|
||||
// {
|
||||
// key: 'blacklist',
|
||||
// label: 'Blacklist',
|
||||
// route: '/compliance/blacklist'
|
||||
// }
|
||||
]
|
||||
}
|
||||
// compliance: { label: 'Compliance', children: [{ label: 'Locale', route: '/locale' }] }
|
||||
]
|
||||
|
|
@ -88,6 +111,7 @@ const Routes = () => (
|
|||
<Route path="/transactions" component={Transactions} />
|
||||
<Route path="/register" component={AuthRegister} />
|
||||
<Route path="/maintenance/machine-status" component={MachineStatus} />
|
||||
<Route path="/compliance/customers" component={Customers} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
|
|
|||
33
package-lock.json
generated
33
package-lock.json
generated
|
|
@ -2640,7 +2640,8 @@
|
|||
"bn.js": {
|
||||
"version": "4.11.8",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
|
||||
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
|
||||
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
|
||||
"optional": true
|
||||
},
|
||||
"bs58": {
|
||||
"version": "4.0.1",
|
||||
|
|
@ -8132,6 +8133,15 @@
|
|||
"type-check": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"libphonenumber-js": {
|
||||
"version": "1.7.38",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.7.38.tgz",
|
||||
"integrity": "sha512-3NMPjWl15E51vWtXYdhVXo+DtZcR35OmXfJQWcwQ28XEbpSWOnQXbkaIZq8RmMv5jiY4fMQxBY7MUXvioApOHg==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.0",
|
||||
"xml2js": "^0.4.17"
|
||||
}
|
||||
},
|
||||
"libpq": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.7.tgz",
|
||||
|
|
@ -8545,7 +8555,8 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
|
||||
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -12728,7 +12739,7 @@
|
|||
"resolved": "https://registry.npmjs.org/web3/-/web3-0.20.6.tgz",
|
||||
"integrity": "sha1-PpcwauAk+yThCj11yIQwJWIhUSA=",
|
||||
"requires": {
|
||||
"bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934",
|
||||
"bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git",
|
||||
"crypto-js": "^3.1.4",
|
||||
"utf8": "^2.1.1",
|
||||
"xhr2": "*",
|
||||
|
|
@ -12999,6 +13010,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"helmet": "^3.8.1",
|
||||
"inquirer": "^5.2.0",
|
||||
"kraken-api": "github:DeX3/npm-kraken-api",
|
||||
"libphonenumber-js": "^1.7.38",
|
||||
"lnd-async": "^1.2.0",
|
||||
"lodash": "^4.17.10",
|
||||
"longjohn": "^0.2.12",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue