Fix: make percentage chart work properly

Fix: code review

Fix: rework components according to PR requested changes

Fix: fix repeated code buildRatesNoCommission

Also renames id to deviceId in transactions quer

Fix: pr requested changes

Chore: move inline styles to classes

Chore: remove comment

Fix: bad equality !process.env.NODE_ENV === 'production'
This commit is contained in:
Cesar 2020-12-07 19:58:20 +00:00 committed by Josh Harvey
parent 5572fb0eb1
commit ae7eaca10c
43 changed files with 818 additions and 1578 deletions

5
lib/forex.js Normal file
View file

@ -0,0 +1,5 @@
const axios = require('axios')
const getFiatRates = () => axios.get('https://bitpay.com/api/rates').then(response => response.data)
module.exports = { getFiatRates }

View file

@ -18,12 +18,11 @@ const couponManager = require('../../coupons')
const serverVersion = require('../../../package.json').version
const transactions = require('../transactions')
const funding = require('../funding')
const forex = require('../../forex')
const supervisor = require('../supervisor')
const serverLogs = require('../server-logs')
const pairing = require('../pairing')
const plugins = require('../../plugins')
const ticker = require('../../ticker')
const {
accounts: accountsConfig,
@ -240,6 +239,8 @@ const typeDefs = gql`
created: Date
age: Float
deviceTime: Date
}
type Rate {
code: String
name: String
@ -268,25 +269,22 @@ const typeDefs = gql`
until: Date
limit: Int
offset: Int
id: ID
deviceId: ID
): [Transaction]
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
accounts: JSONObject
config: JSONObject
blacklist: [Blacklist]
# userTokens: [UserToken]
<<<<<<< HEAD
coupons: [Coupon]
cryptoRates: JSONObject
fiatRates: [Rate]
}
type SupportLogsResponse {
id: ID!
timestamp: Date!
deviceId: ID
=======
rates: JSONObject
btcRates(to: String, from: String): [Rate]
>>>>>>> 9d88b4f... Feat: make dashboard and machine profile page
}
enum MachineAction {
@ -356,15 +354,26 @@ const resolvers = {
serverLogs.getServerLogs(from, until, limit, offset),
serverLogsCsv: (...[, { from, until, limit, offset }]) =>
serverLogs.getServerLogs(from, until, limit, offset).then(parseAsync),
transactions: (...[, { from, until, limit, offset, id }]) =>
transactions.batch(from, until, limit, offset, id),
transactions: (...[, { from, until, limit, offset, deviceId }]) =>
transactions.batch(from, until, limit, offset, deviceId),
transactionsCsv: (...[, { from, until, limit, offset }]) =>
transactions.batch(from, until, limit, offset).then(parseAsync),
config: () => settingsLoader.loadLatestConfigOrNone(),
accounts: () => settingsLoader.loadAccounts(),
blacklist: () => blacklist.getBlacklist(),
// userTokens: () => tokenManager.getTokenList()
coupons: () => couponManager.getAvailableCoupons()
coupons: () => couponManager.getAvailableCoupons(),
cryptoRates: () =>
settingsLoader.loadLatest().then(settings => {
const pi = plugins(settings)
return pi.getRawRates().then(r => {
return {
withCommissions: pi.buildRates(r),
withoutCommissions: pi.buildRatesNoCommission(r)
}
})
}),
fiatRates: () => forex.getFiatRates()
},
Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }),

View file

@ -24,19 +24,8 @@ function addNames (txs) {
const camelize = _.mapKeys(_.camelCase)
function batch (
from = new Date(0).toISOString(),
until = new Date().toISOString(),
limit = null,
offset = 0,
id = null
) {
const packager = _.flow(
_.flatten,
_.orderBy(_.property('created'), ['desc']),
_.map(camelize),
addNames
)
function batch (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0, id = null) {
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class, txs.*,
c.phone as customer_phone,
@ -47,7 +36,7 @@ function batch (
c.front_camera_path as customer_front_camera_path,
c.id_card_photo_path as customer_id_card_photo_path,
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
from cash_in_txs as txs
from cash_in_txs as txs
left outer join customers c on txs.customer_id = c.id
where txs.created >= $2 and txs.created <= $3 ${
id !== null ? `and txs.device_id = $6` : ``
@ -65,7 +54,7 @@ function batch (
c.front_camera_path as customer_front_camera_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
from cash_out_txs txs
from cash_out_txs txs
inner join cash_out_actions actions on txs.id = actions.tx_id
and actions.action = 'provisionAddress'
left outer join customers c on txs.customer_id = c.id

View file

@ -35,7 +35,8 @@ const PONG_TTL = '1 week'
const tradesQueues = {}
function plugins (settings, deviceId) {
function buildRatesNoCommission (tickers) {
function internalBuildRates (tickers, withCommission = true) {
const localeConfig = configManager.getLocale(deviceId, settings.config)
const cryptoCodes = localeConfig.cryptoCurrencies
@ -43,30 +44,7 @@ function plugins (settings, deviceId) {
cryptoCodes.forEach((cryptoCode, i) => {
const rateRec = tickers[i]
if (!rateRec) return
if (Date.now() - rateRec.timestamp > STALE_TICKER)
return logger.warn('Stale rate for ' + cryptoCode)
const rate = rateRec.rates
rates[cryptoCode] = {
cashIn: rate.ask.round(5),
cashOut: rate.bid.round(5)
}
})
return rates
}
function buildRates (tickers) {
const localeConfig = configManager.getLocale(deviceId, settings.config)
const cryptoCodes = localeConfig.cryptoCurrencies
const rates = {}
cryptoCodes.forEach((cryptoCode, i) => {
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
const rateRec = tickers[i]
if (!rateRec) return
@ -78,15 +56,26 @@ function plugins (settings, deviceId) {
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
const rate = rateRec.rates
rates[cryptoCode] = {
withCommission ? rates[cryptoCode] = {
cashIn: rate.ask.mul(cashInCommission).round(5),
cashOut: cashOutCommission && rate.bid.div(cashOutCommission).round(5)
} : rates[cryptoCode] = {
cashIn: rate.ask.round(5),
cashOut: rate.bid.round(5)
}
})
return rates
}
function buildRatesNoCommission (tickers) {
return internalBuildRates(tickers, false)
}
function buildRates (tickers) {
return internalBuildRates(tickers, true)
}
function getNotificationConfig () {
return configManager.getGlobalNotifications(settings.config)
}

View file

@ -2,7 +2,6 @@ const mem = require('mem')
const configManager = require('./new-config-manager')
const ph = require('./plugin-helper')
const logger = require('./logger')
const axios = require('axios')
const lastRate = {}
@ -40,26 +39,4 @@ const getRates = mem(_getRates, {
cacheKey: (settings, fiatCode, cryptoCode) => JSON.stringify([fiatCode, cryptoCode])
})
const getBtcRates = (to = null, from = 'USD') => {
// if to !== null, then we return only the rates with from (default USD) and to (so an array with 2 items)
return axios.get('https://bitpay.com/api/rates').then(response => {
const fxRates = response.data
if (to === null) {
return fxRates
}
const toRate = fxRates.find(o => o.code === to)
const fromRate = fxRates.find(o => o.code === from)
let res = []
if (toRate && to !== from) {
res = [...res, toRate]
}
if (fromRate) {
res = [...res, fromRate]
}
return res
})
}
module.exports = { getBtcRates, getRates }
module.exports = { getRates }

View file

@ -0,0 +1,44 @@
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import React from 'react'
import { white } from 'src/styling/variables'
const cardState = Object.freeze({
DEFAULT: 'default',
SHRUNK: 'shrunk',
EXPANDED: 'expanded'
})
const styles = {
card: {
wordWrap: 'break-word',
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)',
borderRadius: 12,
padding: 24,
backgroundColor: white
}
}
const useStyles = makeStyles(styles)
const CollapsibleCard = ({ className, state, shrunkComponent, children }) => {
const classes = useStyles()
return (
<div className={className}>
<Grid item>
<div className={classes.card}>
{state === cardState.SHRUNK ? shrunkComponent : children}
</div>
</Grid>
</div>
)
}
CollapsibleCard.propTypes = {
shrunkComponent: PropTypes.node.isRequired
}
export default CollapsibleCard
export { cardState }

View file

@ -1,12 +1,12 @@
import Button from '@material-ui/core/Button'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState, useEffect } from 'react'
import React from 'react'
import { cardState as cardState_ } from 'src/components/CollapsibleCard'
import { Label1, H4 } from 'src/components/typography'
import styles from '../Dashboard.styles'
import styles from './Alerts.styles'
import AlertsTable from './AlertsTable'
const NUM_TO_RENDER = 3
@ -23,99 +23,54 @@ const data = {
const useStyles = makeStyles(styles)
const Alerts = ({ cardState, setRightSideState }) => {
const Alerts = ({ onReset, onExpand, size }) => {
const classes = useStyles()
const [showAllItems, setShowAllItems] = useState(false)
const [showExpandButton, setShowExpandButton] = useState(false)
const [numToRender, setNumToRender] = useState(NUM_TO_RENDER)
useEffect(() => {
if (showAllItems) {
setShowExpandButton(false)
setNumToRender(data?.alerts.length)
} else if (data && data?.alerts.length > numToRender) {
setShowExpandButton(true)
}
if (cardState.cardSize === 'small' || cardState.cardSize === 'default') {
setShowAllItems(false)
setNumToRender(NUM_TO_RENDER)
}
}, [cardState.cardSize, numToRender, showAllItems])
const showAllItems = size === cardState_.EXPANDED
const reset = () => {
setRightSideState({
systemStatus: { cardSize: 'default', buttonName: 'Show less' },
alerts: { cardSize: 'default', buttonName: 'Show less' }
})
setShowAllItems(false)
setNumToRender(NUM_TO_RENDER)
}
const showAllClick = () => {
setShowExpandButton(false)
setShowAllItems(true)
setRightSideState({
systemStatus: { cardSize: 'small', buttonName: 'Show machines' },
alerts: { cardSize: 'big', buttonName: 'Show less' }
})
}
const alertsLength = () => (data ? data.alerts.length : 0)
return (
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between'
}}>
<H4 className={classes.h4}>{`Alerts ${
data ? `(${data.alerts.length})` : 0
}`}</H4>
{(showAllItems || cardState.cardSize === 'small') && (
<>
<Label1
style={{
textAlign: 'center',
marginBottom: 0,
marginTop: 0
}}>
<Button
onClick={reset}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{cardState.buttonName}
</Button>
</Label1>
</>
<div className={classes.container}>
<H4 className={classes.h4}>{`Alerts (${alertsLength()})`}</H4>
{showAllItems && (
<Label1 className={classes.upperButtonLabel}>
<Button
onClick={onReset}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{'Show less'}
</Button>
</Label1>
)}
</div>
{cardState.cardSize !== 'small' && (
<>
<Grid container spacing={1}>
<Grid item xs={12}>
<AlertsTable
numToRender={numToRender}
alerts={data?.alerts ?? []}
/>
{showExpandButton && (
<>
<Label1 style={{ textAlign: 'center', marginBottom: 0 }}>
<Button
onClick={showAllClick}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{`Show all (${data.alerts.length})`}
</Button>
</Label1>
</>
)}
</Grid>
<>
<Grid container spacing={1}>
<Grid item xs={12}>
<AlertsTable
numToRender={showAllItems ? data?.alerts.length : NUM_TO_RENDER}
alerts={data?.alerts ?? []}
/>
{!showAllItems && (
<>
<Label1 className={classes.centerLabel}>
<Button
onClick={() => onExpand('alerts')}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{`Show all (${data.alerts.length})`}
</Button>
</Label1>
</>
)}
</Grid>
</>
)}
</Grid>
</>
</>
)
}

View file

@ -6,6 +6,15 @@ import {
} from 'src/styling/variables'
const styles = {
container: {
display: 'flex',
justifyContent: 'space-between'
},
centerLabel: {
textAlign: 'center',
marginBottom: 0,
marginTop: 0
},
label: {
margin: 0,
color: offColor

View file

@ -1,35 +1,21 @@
import { withStyles } from '@material-ui/core'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import * as R from 'ramda'
import React from 'react'
import styles from './Alerts.styles'
// const useStyles = makeStyles(styles)
const StyledListItem = withStyles(() => ({
root: {
...styles.root
}
}))(ListItem)
const AlertsTable = ({ numToRender, alerts }) => {
// const classes = useStyles()
const alertsToRender = R.slice(0, numToRender, alerts)
return (
<>
<List dense>
{alerts.map((alert, idx) => {
if (idx < numToRender) {
return (
<StyledListItem key={idx}>
<ListItemText primary={alert.text} />
</StyledListItem>
)
} else return null
})}
</List>
</>
<List dense>
{alertsToRender.map((alert, idx) => {
return (
<ListItem key={idx}>
<ListItemText primary={alert.text} />
</ListItem>
)
})}
</List>
)
}

View file

@ -1,5 +1,6 @@
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import React from 'react'
import TitleSection from 'src/components/layout/TitleSection'
@ -19,13 +20,17 @@ const Dashboard = () => {
<>
<TitleSection title="Dashboard">
<div className={classes.headerLabels}>
<div>
<div
className={classnames(
classes.headerLabelContainer,
classes.headerLabelContainerMargin
)}>
<TxOutIcon />
<span>Cash-out</span>
<span className={classes.headerLabelSpan}>Cash-out</span>
</div>
<div>
<div className={classes.headerLabelContainer}>
<TxInIcon />
<span>Cash-in</span>
<span className={classes.headerLabelSpan}>Cash-in</span>
</div>
</div>
</TitleSection>

View file

@ -3,6 +3,21 @@ import { spacer, white, primaryColor } from 'src/styling/variables'
const { label1 } = typographyStyles
const styles = {
headerLabels: {
display: 'flex',
flexDirection: 'row'
},
headerLabelContainerMargin: {
marginRight: 24
},
headerLabelContainer: {
display: 'flex',
alignItems: 'center'
},
headerLabelSpan: {
extend: label1,
marginLeft: 6
},
root: {
flexGrow: 1,
marginBottom: 108
@ -17,37 +32,12 @@ const styles = {
padding: 24,
backgroundColor: white
},
h4: {
margin: 0,
marginRight: spacer * 8
leftSideMargin: {
marginRight: 24
},
label: {
color: primaryColor,
minHeight: 0,
minWidth: 0,
padding: 0,
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent'
}
},
actionButton: {
marginTop: -4
},
headerLabels: {
container: {
display: 'flex',
flexDirection: 'row',
'& div': {
display: 'flex',
alignItems: 'center'
},
'& > div:first-child': {
marginRight: 24
},
'& span': {
extend: label1,
marginLeft: 6
}
justifyContent: 'space-between'
},
button: {
color: primaryColor,
@ -58,6 +48,15 @@ const styles = {
'&:hover': {
backgroundColor: 'transparent'
}
},
upperButtonLabel: {
textAlign: 'center',
marginBottom: 0,
marginTop: 16,
marginLeft: spacer
},
alertsCard: {
marginBottom: 16
}
}

View file

@ -1,20 +1,21 @@
import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import BigNumber from 'bignumber.js'
import classnames from 'classnames'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import { Label2 } from 'src/components/typography'
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 { white, spacer } from 'src/styling/variables'
import { fromNamespace } from 'src/utils/config'
import styles from './Footer.styles'
const GET_DATA = gql`
query getData {
rates
cryptoRates
cryptoCurrencies {
code
display
@ -26,23 +27,20 @@ const GET_DATA = gql`
}
}
`
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
const useStyles = makeStyles(styles)
const Footer = () => {
const { data, loading } = useQuery(GET_DATA)
const [expanded, setExpanded] = useState(false)
const [canExpand, setCanExpand] = useState(false)
const [delayedExpand, setDelayedExpand] = useState(null)
const classes = useStyles()
useEffect(() => {
if (data && data.rates && data.rates.withCommissions) {
const numItems = R.keys(data.rates.withCommissions).length
if (numItems > 4) {
setCanExpand(true)
}
}
}, [data])
const classes = useStyles({
bigFooter: R.keys(data?.cryptoRates?.withCommissions).length < 8,
expanded
})
const canExpand = R.keys(data?.cryptoRates.withCommissions ?? []).length > 4
const wallets = fromNamespace('wallets')(data?.config)
@ -56,49 +54,51 @@ const Footer = () => {
const tickerName = data.accountsConfig[tickerIdx].display
const cashInNoCommission = parseFloat(
R.path(['rates', 'withoutCommissions', key, 'cashIn'])(data)
R.path(['cryptoRates', 'withoutCommissions', key, 'cashIn'])(data)
)
const cashOutNoCommission = parseFloat(
R.path(['rates', 'withoutCommissions', key, 'cashOut'])(data)
R.path(['cryptoRates', 'withoutCommissions', key, 'cashOut'])(data)
)
// check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
// to see reason for this implementation. It makes 1.005 round to 1.01 and not 1
// const monetaryValue = +(Math.round(askBidAvg + 'e+2') + 'e-2')
const avgOfAskBid = +(
Math.round((cashInNoCommission + cashOutNoCommission) / 2 + 'e+2') + 'e-2'
const avgOfAskBid = new BigNumber(
(cashInNoCommission + cashOutNoCommission) / 2
)
const cashIn = +(
Math.round(
parseFloat(R.path(['rates', 'withCommissions', key, 'cashIn'])(data)) +
'e+2'
) + 'e-2'
.decimalPlaces(2)
.toNumber()
const cashIn = new BigNumber(
parseFloat(
R.path(['cryptoRates', 'withCommissions', key, 'cashIn'])(data)
)
)
const cashOut = +(
Math.round(
parseFloat(R.path(['rates', 'withCommissions', key, 'cashOut'])(data)) +
'e+2'
) + 'e-2'
.decimalPlaces(2)
.toNumber()
const cashOut = new BigNumber(
parseFloat(
R.path(['cryptoRates', 'withCommissions', key, 'cashOut'])(data)
)
)
.decimalPlaces(2)
.toNumber()
const localeFiatCurrency = data.config.locale_fiatCurrency
const localeLanguage = data.config.locale_languages[0]
return (
<Grid key={key} item xs={3} style={{ marginBottom: 18 }}>
<Grid key={key} item xs={3} className={classes.footerItemContainer}>
<Label2 className={classes.label}>
{data.cryptoCurrencies[idx].display}
</Label2>
<div className={classes.headerLabels}>
<div>
<div className={classes.headerLabel}>
<TxInIcon />
<Label2>{` ${cashIn.toLocaleString(
'en-US'
localeLanguage
)} ${localeFiatCurrency}`}</Label2>
</div>
<div>
<div className={classnames(classes.headerLabel, classes.txOutMargin)}>
<TxOutIcon />
<Label2>{` ${cashOut.toLocaleString(
'en-US'
localeLanguage
)} ${localeFiatCurrency}`}</Label2>
</div>
</div>
@ -106,70 +106,38 @@ const Footer = () => {
className={
classes.tickerLabel
}>{`${tickerName}: ${avgOfAskBid.toLocaleString(
'en-US'
localeLanguage
)} ${localeFiatCurrency}`}</Label2>
</Grid>
)
}
const makeFooterExpandedClass = () => {
return {
height:
R.keys(data.rates.withCommissions).length < 8
? spacer * 12 * 2 + spacer * 2
: spacer * 12 * 3 + spacer * 3,
maxHeight: '50vh',
position: 'fixed',
left: 0,
bottom: 0,
width: '100vw',
backgroundColor: white,
textAlign: 'left'
}
}
const expand = () => {
if (canExpand) {
setExpanded(true)
}
}
const shrink = () => {
setExpanded(false)
}
const handleMouseEnter = () => {
setDelayedExpand(
setTimeout(() => {
expand()
}, 300)
)
setDelayedExpand(setTimeout(() => canExpand && setExpanded(true), 300))
}
const handleMouseLeave = () => {
clearTimeout(delayedExpand)
shrink()
setExpanded(false)
}
return (
<>
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={classes.footer}
style={expanded ? makeFooterExpandedClass() : null}>
<div className={classes.content}>
{!loading && data && (
<Grid container spacing={1}>
<Grid container className={classes.footerContainer}>
{R.keys(data.rates.withCommissions).map(key =>
renderFooterItem(key)
)}
</Grid>
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={classes.footer}>
<div className={classes.content}>
{!loading && data && (
<Grid container spacing={1}>
<Grid container className={classes.footerContainer}>
{R.keys(data.cryptoRates.withCommissions).map(key =>
renderFooterItem(key)
)}
</Grid>
)}
</div>
</Grid>
)}
</div>
</>
</div>
)
}

View file

@ -1,75 +1,39 @@
import typographyStyles from 'src/components/typography/styles'
import {
backgroundColor,
offColor,
errorColor,
primaryColor,
white,
spacer
} from 'src/styling/variables'
const { label1 } = typographyStyles
import { offColor, white, spacer } from 'src/styling/variables'
const styles = {
label: {
color: offColor
},
tickerLabel: {
color: offColor,
marginTop: -5
},
row: {
backgroundColor: backgroundColor,
borderBottom: 'none'
},
header: {
headerLabels: {
whiteSpace: 'pre',
display: 'flex',
alignItems: 'center',
whiteSpace: 'pre'
flexDirection: 'row',
marginTop: -20
},
error: {
color: errorColor
headerLabel: {
display: 'flex',
alignItems: 'center'
},
button: {
color: primaryColor,
minHeight: 0,
minWidth: 0,
padding: 0,
textTransform: 'none',
'&:hover': {
backgroundColor: 'transparent'
}
txOutMargin: {
marginLeft: spacer * 3
},
statusHeader: {
marginLeft: 2
},
table: {
maxHeight: 440,
'&::-webkit-scrollbar': {
width: 7
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: offColor,
borderRadius: 5
}
},
tableBody: {
overflow: 'auto'
},
h4: {
marginTop: 0
},
root: {
flexGrow: 1
},
footer: {
footer: ({ expanded, bigFooter }) => ({
height: expanded
? bigFooter
? spacer * 12 * 2 + spacer * 2
: spacer * 12 * 3 + spacer * 3
: spacer * 12,
position: 'fixed',
left: 0,
bottom: 0,
width: '100vw',
backgroundColor: white,
textAlign: 'left',
height: spacer * 12,
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
}),
tickerLabel: {
color: offColor,
marginTop: -5
},
content: {
width: 1200,
@ -77,26 +41,12 @@ const styles = {
backgroundColor: white,
marginTop: 4
},
headerLabels: {
whiteSpace: 'pre',
display: 'flex',
flexDirection: 'row',
'& div': {
display: 'flex',
alignItems: 'center'
},
'& > div:first-child': {
marginRight: 24
},
'& span': {
extend: label1,
marginLeft: 6
},
marginTop: -20
},
footerContainer: {
marginLeft: spacer * 5,
marginBottom: spacer * 2
},
footerItemContainer: {
marginBottom: 18
}
}

View file

@ -7,20 +7,18 @@ import SystemPerformance from './SystemPerformance'
const useStyles = makeStyles(styles)
const RightSide = () => {
const LeftSide = () => {
const classes = useStyles()
return (
<>
<Grid item xs={6}>
<Grid item style={{ marginRight: 24 }}>
<div className={classes.card}>
<SystemPerformance />
</div>
</Grid>
<Grid item xs={6}>
<Grid item className={classes.leftSideMargin}>
<div className={classes.card}>
<SystemPerformance />
</div>
</Grid>
</>
</Grid>
)
}
export default RightSide
export default LeftSide

View file

@ -1,49 +1,88 @@
import Button from '@material-ui/core/Button'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import CollapsibleCard, { cardState } from 'src/components/CollapsibleCard'
import { H4, Label1 } from 'src/components/typography'
// import Alerts from './Alerts'
import styles from './Dashboard.styles'
import SystemStatus from './SystemStatus'
const useStyles = makeStyles(styles)
const RightSide = () => {
const ShrunkCard = ({ title, buttonName, onUnshrink }) => {
const classes = useStyles()
return (
<div className={classes.container}>
<H4 className={classes.h4}>{title}</H4>
<>
<Label1 className={classes.upperButtonLabel}>
<Button
onClick={onUnshrink}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{buttonName}
</Button>
</Label1>
</>
</div>
)
}
const [rightSideState, setRightSide] = useState({
alerts: {
cardSize: 'default',
buttonName: 'Show less'
},
systemStatus: {
cardSize: 'default',
buttonName: 'Show less'
}
})
const RightSide = () => {
// const classes = useStyles()
const [systemStatusSize, setSystemStatusSize] = useState(cardState.DEFAULT)
// const [alertsSize, setAlertsSize] = useState(cardState.DEFAULT)
const setRightSideState = newState => {
setRightSide(newState)
const onReset = () => {
// setAlertsSize(cardState.DEFAULT)
setSystemStatusSize(cardState.DEFAULT)
}
return (
<>
<Grid item xs={6}>
{/* <Grid item style={{ marginBottom: 16 }}>
<div className={classes.card}>
<Alerts
cardState={rightSideState.alerts}
setRightSideState={setRightSideState}
{/* <CollapsibleCard
className={classes.alertsCard}
state={alertsSize}
shrunkComponent={
<ShrunkCard
title={'Alerts'}
buttonName={'Show alerts'}
onUnshrink={onReset}
/>
</div>
</Grid> */}
<Grid item>
<div className={classes.card}>
<SystemStatus
cardState={rightSideState.systemStatus}
setRightSideState={setRightSideState}
}>
<Alerts
onExpand={() => {
setAlertsSize(cardState.EXPANDED)
setSystemStatusSize(cardState.SHRUNK)
}}
onReset={onReset}
size={alertsSize}
/>
</CollapsibleCard> */}
<CollapsibleCard
state={systemStatusSize}
shrunkComponent={
<ShrunkCard
title={'System status'}
buttonName={'Show machines'}
onUnshrink={onReset}
/>
</div>
</Grid>
}>
<SystemStatus
onExpand={() => {
setSystemStatusSize(cardState.EXPANDED)
// setAlertsSize(cardState.SHRUNK)
}}
onReset={onReset}
size={systemStatusSize}
/>
</CollapsibleCard>
</Grid>
</>
)

View file

@ -1,4 +1,5 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import React from 'react'
import { ReactComponent as CashIn } from 'src/styling/icons/direction/cash-in.svg'
@ -30,58 +31,46 @@ const styles = {
fontFamily: fontSecondary,
fontWeight: 700,
color: fontColor
}
},
cashIn: ({ value }) => ({
width: `${value}%`,
marginRight: 4
}),
cashOut: ({ value }) => ({
width: `${100 - value}%`
})
}
const useStyles = makeStyles(styles)
const PercentageChart = () => {
const classes = useStyles()
const value = 50
const PercentageChart = ({ cashIn, cashOut }) => {
const value = cashIn || cashOut !== 0 ? cashIn : 50
const classes = useStyles({ value })
const buildPercentageView = (value, direction) => {
switch (direction) {
case 'cashIn':
if (value > 20) {
return (
<>
<CashIn />
<span className={classes.label}>{` ${value}%`}</span>
</>
)
}
return null
case 'cashOut':
if (value > 20) {
return (
<>
<CashOut />
<span className={classes.label}>{` ${value}%`}</span>
</>
)
}
return null
default:
return null
const Operation = direction === 'cashIn' ? CashIn : CashOut
if (value > 25) {
return (
<>
<Operation />
{value > 25 && <span className={classes.label}>{` ${value}%`}</span>}
</>
)
}
if (value >= 10) {
return <Operation />
}
}
return (
<>
<div className={classes.wrapper}>
<div
className={classes.percentageBox}
style={{ width: `${value}%`, marginRight: 4 }}>
{buildPercentageView(value, 'cashIn')}
</div>
<div
className={classes.percentageBox}
style={{ width: `${100 - value}%` }}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
<div className={classes.wrapper}>
<div className={classnames(classes.percentageBox, classes.cashIn)}>
{buildPercentageView(value, 'cashIn')}
</div>
</>
<div className={classnames(classes.percentageBox, classes.cashOut)}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
</div>
)
}

View file

@ -1,80 +1,66 @@
import * as d3 from 'd3'
import * as R from 'ramda'
import React, { useEffect, useRef, useCallback, useState } from 'react'
import React, { useEffect, useRef, useCallback } from 'react'
import { backgroundColor, zircon, primaryColor } from 'src/styling/variables'
const transactionProfit = tx => {
const cashInFee = tx.cashInFee ? Number.parseFloat(tx.cashInFee) : 0
const commission =
Number.parseFloat(tx.commissionPercentage) * Number.parseFloat(tx.fiat)
return commission + cashInFee
}
const mockPoint = tx => {
const date = new Date(tx.created)
date.setHours(date.getHours() - 1)
return { created: date.toISOString(), profit: tx.profit }
}
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
const formatDay = ({ created }) =>
new Date(created).toISOString().substring(0, 10)
const formatHour = ({ created }) =>
new Date(created).toISOString().substring(0, 13)
const reducer = (acc, tx) => {
const currentProfit = acc.profit || 0
return { ...tx, profit: currentProfit + transactionProfit(tx) }
}
const RefLineChart = ({ data: realData, timeFrame }) => {
const svgRef = useRef()
// this variable will flip to true if there's no data points or the profit is zero
// this will force the line graph to touch the x axis instead of centering,
// centering is bad because it gives the impression that there could be negative values
// so, if this is true the y domain should be [0, 0.1]
const [zeroProfit, setZeroProfit] = useState(false)
const drawGraph = useCallback(() => {
const svg = d3.select(svgRef.current)
const margin = { top: 0, right: 0, bottom: 0, left: 0 }
const width = 336 - margin.left - margin.right
const height = 128 - margin.top - margin.bottom
const transactionProfit = tx => {
let cashInFee = 0
if (tx.cashInFee) {
cashInFee = Number.parseFloat(tx.cashInFee)
}
const commission =
Number.parseFloat(tx.commissionPercentage) * Number.parseFloat(tx.fiat)
return commission + cashInFee
}
const massageData = () => {
const methods = {
day: function(obj) {
return new Date(obj.created).toISOString().substring(0, 10)
},
hour: function(obj) {
return new Date(obj.created).toISOString().substring(0, 13)
}
}
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day
const method = timeFrame === 'Day' ? formatHour : formatDay
const method = timeFrame === 'Day' ? 'hour' : 'day'
const f = methods[method]
const groupedTx = R.values(R.groupBy(f)(realData))
let aggregatedTX = groupedTx.map(list => {
const temp = { ...list[0], profit: transactionProfit(list[0]) }
if (list.length > 1) {
for (let i = 1; i < list.length; i++) {
temp.profit += transactionProfit(list[i])
}
}
return temp
})
// if no point exists, then create a (0,0) point
const aggregatedTX = R.values(R.reduceBy(reducer, [], method, realData))
// if no point exists, then return 2 points at y = 0
if (aggregatedTX.length === 0) {
setZeroProfit(true)
aggregatedTX = [{ created: new Date().toISOString(), profit: 0 }]
} else {
setZeroProfit(false)
const mockPoint1 = { created: new Date().toISOString(), profit: 0 }
const mockPoint2 = mockPoint(mockPoint1)
return [[mockPoint1, mockPoint2], true]
}
// create point on the left if only one point exists, otherwise line won't be drawn
// if only one point exists, create point on the left - otherwise the line won't be drawn
if (aggregatedTX.length === 1) {
const temp = { ...aggregatedTX[0] }
const date = new Date(temp.created)
date.setHours(date.getHours() - 1)
temp.created = date.toISOString()
aggregatedTX = [...aggregatedTX, temp]
return [R.append(mockPoint(aggregatedTX[0]), aggregatedTX), false]
}
return aggregatedTX
// the boolean value is for zeroProfit. It makes the line render at y = 0 instead of y = 50% of container height
return [aggregatedTX, false]
}
/* Important step to make the graph look good!
This function groups transactions by either day or hour depending on the time grame
This function groups transactions by either day or hour depending on the time frame
This makes the line look smooth and not all wonky when there are many transactions in a given time
*/
const data = massageData()
const [data, zeroProfit] = massageData()
// sets width of the graph
svg.attr('width', width)
@ -162,7 +148,7 @@ const RefLineChart = ({ data: realData, timeFrame }) => {
.attr('stroke-width', '2')
.attr('stroke-linejoin', 'round')
.attr('stroke', primaryColor)
}, [realData, timeFrame, zeroProfit])
}, [realData, timeFrame])
useEffect(() => {
// first we clear old chart DOM elements on component update

View file

@ -18,21 +18,17 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
// finds maximum value for the Y axis. Minimum value is 100. If value is multiple of 1000, add 100
// (this is because the Y axis looks best with multiples of 100)
const findMaxY = () => {
if (realData.length === 0) {
return 100
}
let maxY = d3.max(realData, t => parseFloat(t.fiat))
maxY = 100 * Math.ceil(maxY / 100)
if (maxY < 100) {
return 100
} else if (maxY % 1000 === 0) {
return maxY + 100
}
if (realData.length === 0) return 100
const maxvalueTx =
100 * Math.ceil(d3.max(realData, t => parseFloat(t.fiat)) / 100)
const maxY = Math.max(100, maxvalueTx)
if (maxY % 1000 === 0) return maxY + 100
return maxY
}
// changes values of arguments in some d3 function calls to make the graph labels look good according to the selected time frame
const findXAxisSettings = () => {
// case 'Day' or default
const res = {
nice: null,
ticks: 4,
@ -41,11 +37,8 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
timeRange: [50, 500]
}
switch (timeFrame) {
case 'Day':
return res
case 'Week':
return {
...res,
nice: 7,
ticks: 7,
subtractDays: 7,
@ -54,7 +47,6 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
}
case 'Month':
return {
...res,
nice: 6,
ticks: 6,
subtractDays: 30,
@ -133,10 +125,8 @@ const RefScatterplot = ({ data: realData, timeFrame }) => {
.ticks(xAxisSettings.ticks)
.tickSize(0)
.tickFormat(d3.timeFormat(xAxisSettings.timeFormat))
// .tickFormat(d3.timeFormat('%H:%M'))
)
.selectAll('text')
// .attr('dx', '4em')
.attr('dy', '1.5em')
// this is for the x axis line. It is the same color as the horizontal grid lines
g.append('g')

View file

@ -1,134 +0,0 @@
/*eslint-disable*/
import { scaleLinear, scaleTime, max, axisLeft, axisBottom, select } from 'd3'
import React, { useMemo } from 'react'
import moment from 'moment'
const data = [
[0, '2020-11-08T18:00:05.664Z'],
[40.01301, '2020-11-09T11:17:05.664Z']
]
const marginTop = 10
const marginRight = 30
const marginBottom = 30
const marginLeft = 60
const width = 510 - marginLeft - marginRight
const height = 141 - marginTop - marginBottom
const Scatterplot = ({ data: realData }) => {
const x = scaleTime()
.domain([
moment()
.add(-1, 'day')
.valueOf(),
moment().valueOf()
])
.range([0, width])
.nice()
const y = scaleLinear()
.domain([0, 1000])
.range([height, 0])
.nice()
// viewBox="0 0 540 141"
return (
<>
<svg
width={width + marginLeft + marginRight}
height={height + marginTop + marginBottom}>
<g transform={`translate(${marginLeft},${marginTop})`}>
<XAxis
transform={`translate(0, ${height + marginTop})`}
scale={x}
numTicks={6}
/>
<g>{axisLeft(y)}</g>
{/* <YAxis transform={`translate(0, 0)`} scale={y} numTicks={6} /> */}
<RenderCircles data={data} scale={{ x, y }} />
</g>
</svg>
</>
)
}
const XAxis = ({
range = [10, 500],
transform,
scale: xScale,
numTicks = 7
}) => {
const ticks = useMemo(() => {
return xScale.ticks(numTicks).map(value => ({
value,
xOffset: xScale(value)
}))
}, [range.join('-')])
return (
<g transform={transform}>
{ticks.map(({ value, xOffset }) => (
<g key={value} transform={`translate(${xOffset}, 0)`}>
<text
key={value}
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateY(10px)'
}}>
{value.getHours()}
</text>
</g>
))}
</g>
)
}
const YAxis = ({
range = [10, 500],
transform,
scale: xScale,
numTicks = 7
}) => {
const ticks = useMemo(() => {
return xScale.ticks(numTicks).map(value => ({
value,
xOffset: xScale(value)
}))
}, [range.join('-')])
return (
<g transform={transform}>
{ticks.map(({ value, xOffset }) => (
<g key={value} transform={`translate(0, ${xOffset})`}>
<text
key={value}
style={{
fontSize: '10px',
textAnchor: 'middle',
transform: 'translateX(-10px)'
}}>
{value}
</text>
</g>
))}
</g>
)
}
const RenderCircles = ({ data, scale }) => {
let renderCircles = data.map((item, idx) => {
return (
<circle
cx={scale.x(new Date(item[1]))}
cy={scale.y(item[0])}
r="4"
style={{ fill: 'rgba(25, 158, 199, .9)' }}
key={idx}
/>
)
})
return <g>{renderCircles}</g>
}
export default Scatterplot

View file

@ -1,21 +1,20 @@
// import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import * as R from 'ramda'
import React, { useState } from 'react'
import { H4 } from 'src/components/typography'
import styles from './SystemPerformance.styles'
const useStyles = makeStyles(styles)
const ranges = ['Month', 'Week', 'Day']
const Nav = ({ handleSetRange }) => {
const classes = useStyles()
const [clickedItem, setClickedItem] = useState('Day')
const isSelected = innerText => {
return innerText === clickedItem
}
const isSelected = R.equals(clickedItem)
const handleClick = range => {
setClickedItem(range)
handleSetRange(range)
@ -26,34 +25,21 @@ const Nav = ({ handleSetRange }) => {
<div className={classes.titleAndButtonsContainer}>
<H4 className={classes.h4}>{'System performance'}</H4>
</div>
<div style={{ display: 'flex' }}>
<div
onClick={e => handleClick(e.target.innerText)}
className={
isSelected('Month')
? classnames(classes.newHighlightedLabel, classes.navButton)
: classnames(classes.label, classes.navButton)
}>
Month
</div>
<div
onClick={e => handleClick(e.target.innerText)}
className={
isSelected('Week')
? classnames(classes.newHighlightedLabel, classes.navButton)
: classnames(classes.label, classes.navButton)
}>
Week
</div>
<div
className={
isSelected('Day')
? classnames(classes.newHighlightedLabel, classes.navButton)
: classnames(classes.label, classes.navButton)
}
onClick={e => handleClick(e.target.innerText)}>
Day
</div>
<div className={classes.navContainer}>
{ranges.map((it, idx) => {
return (
<div
key={idx}
onClick={e => handleClick(e.target.innerText)}
className={
isSelected(it)
? classnames(classes.newHighlightedLabel, classes.navButton)
: classnames(classes.label, classes.navButton)
}>
{it}
</div>
)
})}
</div>
</div>
)

View file

@ -1,10 +1,11 @@
import { useQuery } from '@apollo/react-hooks'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import BigNumber from 'bignumber.js'
import gql from 'graphql-tag'
import moment from 'moment'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import React, { useState } from 'react'
import { Label2 } from 'src/components/typography/index'
import { ReactComponent as TriangleDown } from 'src/styling/icons/arrow/triangle_down.svg'
@ -18,18 +19,31 @@ import InfoWithLabel from './InfoWithLabel'
import Nav from './Nav'
import styles from './SystemPerformance.styles'
const isNotProp = R.curry(R.compose(R.isNil, R.prop))
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
const getFiats = R.map(R.prop('fiat'))
const getProps = propName => R.map(R.prop(propName))
const useStyles = makeStyles(styles)
const mapToFee = R.map(R.prop('cashInFee'))
const getDateSecondsAgo = (seconds = 0, startDate = null) => {
if (startDate) {
return moment(startDate).subtract(seconds, 'second')
}
return moment().subtract(seconds, 'second')
const date = startDate ? moment(startDate) : moment()
return date.subtract(seconds, 'second')
}
// const now = moment()
const ranges = {
Day: {
left: getDateSecondsAgo(2 * 24 * 3600, moment()),
right: getDateSecondsAgo(24 * 3600, moment())
},
Week: {
left: getDateSecondsAgo(14 * 24 * 3600, moment()),
right: getDateSecondsAgo(7 * 24 * 3600, moment())
},
Month: {
left: getDateSecondsAgo(60 * 24 * 3600, moment()),
right: getDateSecondsAgo(30 * 24 * 3600, moment())
}
}
const GET_DATA = gql`
query getData {
@ -42,7 +56,7 @@ const GET_DATA = gql`
txClass
error
}
btcRates {
fiatRates {
code
name
rate
@ -51,134 +65,72 @@ const GET_DATA = gql`
}
`
const reducer = (acc, it) =>
(acc +=
Number.parseFloat(it.commissionPercentage) * Number.parseFloat(it.fiat))
const SystemPerformance = () => {
const classes = useStyles()
const [selectedRange, setSelectedRange] = useState('Day')
const [transactionsToShow, setTransactionsToShow] = useState([])
const [transactionsLastTimePeriod, setTransactionsLastTimePeriod] = useState(
[]
)
const { data, loading } = useQuery(GET_DATA)
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
useEffect(() => {
const isInRange = (getLastTimePeriod = false) => t => {
const now = moment()
switch (selectedRange) {
case 'Day':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateSecondsAgo(2 * 24 * 3600, now),
getDateSecondsAgo(24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(getDateSecondsAgo(24 * 3600, now), now)
)
case 'Week':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateSecondsAgo(14 * 24 * 3600, now),
getDateSecondsAgo(7 * 24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(
getDateSecondsAgo(7 * 24 * 3600, now),
now
)
)
case 'Month':
if (getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(
getDateSecondsAgo(60 * 24 * 3600, now),
getDateSecondsAgo(30 * 24 * 3600, now)
)
)
}
return (
t.error === null &&
moment(t.created).isBetween(
getDateSecondsAgo(30 * 24 * 3600, now),
now
)
)
default:
return t.error === null && true
}
const isInRangeAndNoError = getLastTimePeriod => t => {
if (t.error !== null) return false
if (!getLastTimePeriod) {
return (
t.error === null &&
moment(t.created).isBetween(ranges[selectedRange].right, moment())
)
}
const convertFiatToLocale = item => {
if (item.fiatCode === fiatLocale) return item
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.btcRates)
const localeRate = R.find(R.propEq('code', fiatLocale))(data.btcRates)
const multiplier = localeRate.rate / itemRate.rate
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
}
setTransactionsToShow(
R.map(convertFiatToLocale)(
R.filter(isInRange(false), data?.transactions ?? [])
return (
t.error === null &&
moment(t.created).isBetween(
ranges[selectedRange].left,
ranges[selectedRange].right
)
)
setTransactionsLastTimePeriod(
R.map(convertFiatToLocale)(
R.filter(isInRange(true), data?.transactions ?? [])
)
)
}, [data, fiatLocale, selectedRange])
const handleSetRange = range => {
setSelectedRange(range)
}
const convertFiatToLocale = item => {
if (item.fiatCode === fiatLocale) return item
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.fiatRates)
const localeRate = R.find(R.propEq('code', fiatLocale))(data.fiatRates)
const multiplier = localeRate.rate / itemRate.rate
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
}
const transactionsToShow = R.map(convertFiatToLocale)(
R.filter(isInRangeAndNoError(false), data?.transactions ?? [])
)
const transactionsLastTimePeriod = R.map(convertFiatToLocale)(
R.filter(isInRangeAndNoError(true), data?.transactions ?? [])
)
const getNumTransactions = () => {
return R.length(R.filter(isNotProp('error'), transactionsToShow))
return R.length(transactionsToShow)
}
const getFiatVolume = () => {
// for explanation check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
return +(
Math.round(
R.sum(getFiats(R.filter(isNotProp('error'), transactionsToShow))) +
'e+2'
) + 'e-2'
)
}
const getFiatVolume = () =>
new BigNumber(R.sum(getFiats(transactionsToShow)))
.decimalPlaces(2)
.toNumber()
const getProfit = (transactions = transactionsToShow) => {
const cashInFees = R.sum(
getProps('cashInFee')(R.filter(isNotProp('error'), transactions))
)
let commissionFees = 0
transactions.forEach(t => {
if (t.error === null) {
commissionFees +=
Number.parseFloat(t.commissionPercentage) * Number.parseFloat(t.fiat)
}
})
return +(Math.round(commissionFees + cashInFees + 'e+2') + 'e-2')
const getProfit = transactions => {
const cashInFees = R.sum(mapToFee(transactions))
const commissionFees = R.reduce(reducer, 0, transactions)
return new BigNumber(commissionFees + cashInFees)
.decimalPlaces(2)
.toNumber()
}
const getPercentChange = () => {
const thisTimePeriodProfit = getProfit(transactionsToShow)
const previousTimePeriodProfit = getProfit(transactionsLastTimePeriod)
if (previousTimePeriodProfit === 0) {
return 100
}
if (previousTimePeriodProfit === 0) return 100
return Math.round(
(100 * (thisTimePeriodProfit - previousTimePeriodProfit)) /
Math.abs(previousTimePeriodProfit)
@ -186,36 +138,17 @@ const SystemPerformance = () => {
}
const getDirectionPercent = () => {
const directions = {
cashIn: 0,
cashOut: 0,
length: 0
const [cashIn, cashOut] = R.partition(R.propEq('txClass', 'cashIn'))(
transactionsToShow
)
const totalLength = cashIn.length + cashOut.length
if (totalLength === 0) {
return { cashIn: 0, cashOut: 0 }
}
transactionsToShow.forEach(t => {
if (t.error === null) {
switch (t.txClass) {
case 'cashIn':
directions.cashIn += 1
directions.length += 1
break
case 'cashOut':
directions.cashOut += 1
directions.length += 1
break
default:
break
}
}
})
return {
cashIn:
directions.length > 0
? Math.round((directions.cashIn / directions.length) * 100)
: 0,
cashOut:
directions.length > 0
? Math.round((directions.cashOut / directions.length) * 100)
: 0
cashIn: Math.round((cashIn.length / totalLength) * 100),
cashOut: Math.round((cashOut.length / totalLength) * 100)
}
}
@ -223,7 +156,7 @@ const SystemPerformance = () => {
return (
<>
<Nav handleSetRange={handleSetRange} />
<Nav handleSetRange={setSelectedRange} />
{!loading && (
<>
<Grid container spacing={2}>
@ -241,7 +174,7 @@ const SystemPerformance = () => {
</Grid>
{/* todo new customers */}
</Grid>
<Grid container style={{ marginTop: 30 }}>
<Grid container className={classes.gridContainer}>
<Grid item xs={12}>
<Label2>Transactions</Label2>
<Scatterplot
@ -250,27 +183,23 @@ const SystemPerformance = () => {
/>
</Grid>
</Grid>
<Grid container style={{ marginTop: 30 }}>
<Grid container className={classes.gridContainer}>
<Grid item xs={8}>
<Label2>Profit from commissions</Label2>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
margin: '0 26px -30px 16px',
position: 'relative'
}}>
<div className={classes.profitContainer}>
<div className={classes.profitLabel}>
{`${getProfit()} ${data?.config.locale_fiatCurrency}`}
{`${getProfit(transactionsToShow)} ${
data?.config.locale_fiatCurrency
}`}
</div>
<div
className={
percentChange <= 0 ? classes.percentDown : classes.percentUp
}>
{percentChange <= 0 ? (
<TriangleDown style={{ height: 13 }} />
<TriangleDown className={classes.percentDown} />
) : (
<TriangleUp style={{ height: 10 }} />
<TriangleUp className={classes.percentUp} />
)}{' '}
{`${percentChange}%`}
</div>
@ -281,7 +210,10 @@ const SystemPerformance = () => {
<Label2>Direction</Label2>
<Grid container>
<Grid item xs>
<PercentageChart data={getDirectionPercent()} />
<PercentageChart
cashIn={getDirectionPercent().cashIn}
cashOut={getDirectionPercent().cashOut}
/>
</Grid>
</Grid>
</Grid>

View file

@ -5,8 +5,8 @@ import {
fontSize3,
fontSecondary,
fontColor,
secondaryColorDarker,
linkSecondaryColor
spring4,
tomato
} from 'src/styling/variables'
const styles = {
@ -59,6 +59,9 @@ const styles = {
navButton: {
marginLeft: 24
},
navContainer: {
display: 'flex'
},
profitLabel: {
fontSize: fontSize3,
fontFamily: fontSecondary,
@ -69,13 +72,24 @@ const styles = {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 700,
color: secondaryColorDarker
color: spring4,
height: 10
},
percentDown: {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 700,
color: linkSecondaryColor
color: tomato,
height: 13
},
profitContainer: {
display: 'flex',
justifyContent: 'space-between',
margin: '0 26px -30px 16px',
position: 'relative'
},
gridContainer: {
marginTop: 30
}
}

View file

@ -5,6 +5,7 @@ import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import classnames from 'classnames'
import React from 'react'
import { useHistory } from 'react-router-dom'
@ -57,73 +58,70 @@ const MachinesTable = ({ machines, numToRender }) => {
}
return (
<>
<TableContainer className={classes.table}>
<Table>
<TableHead>
<TableRow>
<HeaderCell>
<div className={classes.header}>
<Label2 className={classes.label}>Machines</Label2>
</div>
</HeaderCell>
<HeaderCell>
<div className={`${classes.header} ${classes.statusHeader}`}>
<Label2 className={classes.label}>Status</Label2>
</div>
</HeaderCell>
{/* <HeaderCell>
<TableContainer className={classes.table}>
<Table>
<TableHead>
<TableRow>
<HeaderCell>
<div className={classes.header}>
<Label2 className={classes.label}>Machines</Label2>
</div>
</HeaderCell>
<HeaderCell>
<div className={`${classes.header} ${classes.statusHeader}`}>
<Label2 className={classes.label}>Status</Label2>
</div>
</HeaderCell>
{/* <HeaderCell>
<div className={classes.header}>
<TxInIcon />
</div>
</HeaderCell> */}
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
<Label2 className={classes.label}> 1</Label2>
</div>
</HeaderCell>
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
<Label2 className={classes.label}> 2</Label2>
</div>
</HeaderCell>
</TableRow>
</TableHead>
<TableBody>
{machines.map((machine, idx) => {
if (idx < numToRender) {
return (
<TableRow
onClick={() => redirect(machine.name)}
style={{ cursor: 'pointer' }}
key={machine.deviceId + idx}
className={classes.row}>
<StyledCell align="left">
<TL2>{machine.name}</TL2>
</StyledCell>
<StyledCell>
<Status status={machine.statuses[0]} />
</StyledCell>
{/* <StyledCell align="left">
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
<Label2 className={classes.label}> 1</Label2>
</div>
</HeaderCell>
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
<Label2 className={classes.label}> 2</Label2>
</div>
</HeaderCell>
</TableRow>
</TableHead>
<TableBody>
{machines.map((machine, idx) => {
if (idx < numToRender) {
return (
<TableRow
onClick={() => redirect(machine.name)}
className={classnames(classes.row, classes.clickableRow)}
key={machine.deviceId + idx}>
<StyledCell align="left">
<TL2>{machine.name}</TL2>
</StyledCell>
<StyledCell>
<Status status={machine.statuses[0]} />
</StyledCell>
{/* <StyledCell align="left">
{makePercentageText(machine.cashbox)}
</StyledCell> */}
<StyledCell align="left">
{makePercentageText(machine.cassette1)}
</StyledCell>
<StyledCell align="left">
{makePercentageText(machine.cassette2)}
</StyledCell>
</TableRow>
)
}
return null
})}
</TableBody>
</Table>
</TableContainer>
</>
<StyledCell align="left">
{makePercentageText(machine.cassette1)}
</StyledCell>
<StyledCell align="left">
{makePercentageText(machine.cassette2)}
</StyledCell>
</TableRow>
)
}
return null
})}
</TableBody>
</Table>
</TableContainer>
)
}

View file

@ -6,6 +6,10 @@ import {
} from 'src/styling/variables'
const styles = {
container: {
display: 'flex',
justifyContent: 'space-between'
},
label: {
margin: 0,
color: offColor
@ -14,6 +18,9 @@ const styles = {
backgroundColor: backgroundColor,
borderBottom: 'none'
},
clickableRow: {
cursor: 'pointer'
},
header: {
display: 'flex',
alignItems: 'center',
@ -32,11 +39,21 @@ const styles = {
backgroundColor: 'transparent'
}
},
buttonLabel: {
textAlign: 'center',
marginBottom: 0
},
upperButtonLabel: {
textAlign: 'center',
marginBottom: 0,
marginTop: 0
},
statusHeader: {
marginLeft: 2
},
/* table: {
maxHeight: 440,
/* // temporary class, until alerts are enabled. Delete this table class and uncomment the other one
table: {
height: 463,
'&::-webkit-scrollbar': {
width: 7
},
@ -45,10 +62,8 @@ const styles = {
borderRadius: 5
}
}, */
// temporary, when notifications are enabled delete this one and decomment above
table: {
maxHeight: 465,
minHeight: 465,
maxHeight: 440,
'&::-webkit-scrollbar': {
width: 7
},
@ -62,6 +77,15 @@ const styles = {
},
h4: {
marginTop: 0
},
tl2: {
display: 'inline'
},
label1: {
display: 'inline'
},
machinesTableContainer: {
marginTop: 23
}
}

View file

@ -1,10 +1,11 @@
import { useQuery } from '@apollo/react-hooks'
// import Button from '@material-ui/core/Button'
import Button from '@material-ui/core/Button'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag'
import React, { useState, useEffect } from 'react'
import React from 'react'
import { cardState as cardState_ } from 'src/components/CollapsibleCard'
// import ActionButton from 'src/components/buttons/ActionButton'
import { H4, TL2, Label1 } from 'src/components/typography'
@ -14,7 +15,7 @@ import styles from './MachinesTable.styles'
const useStyles = makeStyles(styles)
// number of machines in the table to render on page load
// const NUM_TO_RENDER = 3
const NUM_TO_RENDER = 3
const GET_DATA = gql`
query getData {
@ -38,97 +39,56 @@ const GET_DATA = gql`
}
`
const parseUptime = time => {
/* const parseUptime = time => {
if (time < 60) return `${time}s`
if (time < 3600) return `${Math.floor(time / 60)}m`
if (time < 86400) return `${Math.floor(time / 60 / 60)}h`
return `${Math.floor(time / 60 / 60 / 24)}d`
}
} */
const SystemStatus = ({ cardState, setRightSideState }) => {
const SystemStatus = ({ onReset, onExpand, size }) => {
const classes = useStyles()
const { data, loading } = useQuery(GET_DATA)
const [showAllItems, setShowAllItems] = useState(false)
// const [showExpandButton, setShowExpandButton] = useState(false)
// const [numToRender, setNumToRender] = useState(NUM_TO_RENDER)
useEffect(() => {
/* if (showAllItems) {
setShowExpandButton(false)
setNumToRender(data?.machines.length)
} else if (data && data?.machines.length > numToRender) {
setShowExpandButton(true)
} */
if (cardState.cardSize === 'small' || cardState.cardSize === 'default') {
setShowAllItems(false)
// setNumToRender(NUM_TO_RENDER)
}
}, [cardState.cardSize, data, /* numToRender, */ showAllItems])
const showAllItems = size === cardState_.EXPANDED
/* const reset = () => {
setShowAllItems(false)
setNumToRender(NUM_TO_RENDER)
setRightSideState({
systemStatus: { cardSize: 'default', buttonName: 'Show less' },
alerts: { cardSize: 'default', buttonName: 'Show less' }
})
}
const showAllClick = () => {
setShowExpandButton(false)
setShowAllItems(true)
setRightSideState({
systemStatus: { cardSize: 'big', buttonName: 'Show less' },
alerts: { cardSize: 'small', buttonName: 'Show alerts' }
})
} */
// placeholder data
if (data) {
data.uptime = [{ time: 1854125, state: 'RUNNING' }]
}
const uptime = data?.uptime ?? [{}]
// const uptime = data?.uptime ?? [{}]
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div className={classes.container}>
<H4 className={classes.h4}>System status</H4>{' '}
</div>
{/* {(showAllItems || cardState.cardSize === 'small') && (
<>
<Label1
style={{
textAlign: 'center',
marginBottom: 0,
marginTop: 0
}}>
<Button
onClick={reset}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{cardState.buttonName}
</Button>
</Label1>
</>
{showAllItems && (
<Label1 className={classes.upperButtonLabel}>
<Button
onClick={onReset}
size="small"
disableRipple
disableFocusRipple
className={classes.button}>
{'Show less'}
</Button>
</Label1>
)}
</div> */}
{!loading && cardState.cardSize !== 'small' && (
</div>
{!loading && (
<>
<Grid container spacing={1}>
{/*
On hold until system uptime is implemented
<Grid item xs={4}>
<TL2 style={{ display: 'inline' }}>
<TL2 className={classes.tl2}>
{parseUptime(uptime[0].time)}
</TL2>
<Label1 style={{ display: 'inline' }}> System up time</Label1>
<Label1 className={classes.label1}> System up time</Label1>
</Grid> */}
<Grid item xs={4}>
<TL2 className={classes.tl2}>{data?.serverVersion}</TL2>
<Label1 className={classes.label1}> server version</Label1>
</Grid>
<Grid item xs={4}>
<TL2 style={{ display: 'inline' }}>{data?.serverVersion}</TL2>
<Label1 style={{ display: 'inline' }}> server version</Label1>
</Grid>
<Grid item xs={4}>
{/* <ActionButton
{/*
On hold until system update features are implemented
<ActionButton
color="primary"
className={classes.actionButton}
onClick={() => console.log('Upgrade button clicked')}>
@ -136,18 +96,22 @@ const SystemStatus = ({ cardState, setRightSideState }) => {
</ActionButton> */}
</Grid>
</Grid>
<Grid container spacing={1} style={{ marginTop: 23 }}>
<Grid
container
spacing={1}
className={classes.machinesTableContainer}>
<Grid item xs={12}>
<MachinesTable
/* numToRender={numToRender} */
numToRender={Infinity}
numToRender={
showAllItems ? data?.machines.length : NUM_TO_RENDER
}
machines={data?.machines ?? []}
/>
{/* {showExpandButton && (
{!showAllItems && data.machines.length > NUM_TO_RENDER && (
<>
<Label1 style={{ textAlign: 'center', marginBottom: 0 }}>
<Label1 className={classes.buttonLabel}>
<Button
onClick={showAllClick}
onClick={() => onExpand()}
size="small"
disableRipple
disableFocusRipple
@ -156,7 +120,7 @@ const SystemStatus = ({ cardState, setRightSideState }) => {
</Button>
</Label1>
</>
)} */}
)}
</Grid>
</Grid>
</>

View file

@ -115,23 +115,19 @@ const CashCassettes = ({ machine, config, refetchData }) => {
})
}
return (
<>
{machine.name && (
<EditableTable
error={error?.message}
stripeWhen={isCashOutDisabled}
disableRowEdit={isCashOutDisabled}
name="cashboxes"
elements={elements}
enableEdit
data={[machine] || []}
save={onSave}
validationSchema={ValidationSchema}
/>
)}
</>
)
return machine.name ? (
<EditableTable
error={error?.message}
stripeWhen={isCashOutDisabled}
disableRowEdit={isCashOutDisabled}
name="cashboxes"
elements={elements}
enableEdit
data={[machine] || []}
save={onSave}
validationSchema={ValidationSchema}
/>
) : null
}
export default CashCassettes

View file

@ -86,17 +86,15 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
return R.values(commissions)
}
getMachineCommissions()
const machineCommissions = getMachineCommissions()
return (
<>
<EditableTable
name="overrides"
save={saveOverrides}
data={getMachineCommissions()}
elements={overrides(currency)}
/>
</>
<EditableTable
name="overrides"
save={saveOverrides}
data={machineCommissions}
elements={overrides(currency)}
/>
)
}

View file

@ -10,28 +10,24 @@ const useStyles = makeStyles(styles)
const Details = ({ data }) => {
const classes = useStyles()
return (
<>
<div className={classes.row}>
<div className={classes.rowItem}>
{' '}
<Label3 className={classes.label3}>Paired at</Label3>
<P>
{data.pairedAt
? moment(data.pairedAt).format('YYYY-MM-DD HH:mm:ss')
: ''}
</P>
</div>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Machine model</Label3>
<P>{data.model}</P>
</div>
<div className={classes.rowItem}>
{' '}
<Label3 className={classes.label3}>Software version</Label3>
<P>{data.version}</P>
</div>
<div className={classes.row}>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Paired at</Label3>
<P>
{data.pairedAt
? moment(data.pairedAt).format('YYYY-MM-DD HH:mm:ss')
: ''}
</P>
</div>
</>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Machine model</Label3>
<P>{data.model}</P>
</div>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Software version</Label3>
<P>{data.version}</P>
</div>
</div>
)
}

View file

@ -30,6 +30,25 @@ const MACHINE_ACTION = gql`
}
`
const makeLastPing = lastPing => {
if (!lastPing) return null
const now = moment()
const secondsAgo = now.diff(lastPing, 'seconds')
if (secondsAgo < 60) {
return `${secondsAgo} ${secondsAgo === 1 ? 'second' : 'seconds'} ago`
}
if (secondsAgo < 3600) {
const minutes = Math.round(secondsAgo / 60)
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
}
if (secondsAgo < 3600 * 24) {
const hours = Math.round(secondsAgo / 3600)
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
}
const days = Math.round(secondsAgo / 3600 / 24)
return `${days} ${days === 1 ? 'day' : 'days'} ago`
}
const Overview = ({ data, onActionSuccess }) => {
const [action, setAction] = useState('')
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
@ -50,24 +69,6 @@ const Overview = ({ data, onActionSuccess }) => {
const confirmActionDialog = action =>
setAction(action) || setConfirmActionDialogOpen(true)
const makeLastPing = () => {
const now = moment()
const secondsAgo = now.diff(data.lastPing, 'seconds')
if (secondsAgo < 60) {
return `${secondsAgo} ${secondsAgo === 1 ? 'second' : 'seconds'} ago`
}
if (secondsAgo < 3600) {
const minutes = Math.round(secondsAgo / 60)
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
}
if (secondsAgo < 3600 * 24) {
const hours = Math.round(secondsAgo / 3600)
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
}
const days = Math.round(secondsAgo / 3600 / 24)
return `${days} ${days === 1 ? 'day' : 'days'} ago`
}
return (
<>
<div className={classes.row}>
@ -84,7 +85,7 @@ const Overview = ({ data, onActionSuccess }) => {
<div className={classes.row}>
<div className={classes.rowItem}>
<Label3 className={classes.label3}>Last ping</Label3>
<P>{data.lastPing ? makeLastPing() : ''}</P>
<P>{makeLastPing(data.lastPing)}</P>
</div>
</div>
<div className={classes.row}>

View file

@ -1,74 +0,0 @@
import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import { CopyToClipboard as ReactCopyToClipboard } from 'react-copy-to-clipboard'
import Popover from 'src/components/Popper'
import { ReactComponent as CopyIcon } from 'src/styling/icons/action/copy/copy.svg'
import { comet } from 'src/styling/variables'
import { cpcStyles } from './Transactions.styles'
const useStyles = makeStyles(cpcStyles)
const CopyToClipboard = ({
className,
buttonClassname,
children,
...props
}) => {
const [anchorEl, setAnchorEl] = useState(null)
useEffect(() => {
if (anchorEl) setTimeout(() => setAnchorEl(null), 3000)
}, [anchorEl])
const classes = useStyles()
const handleClick = event => {
setAnchorEl(anchorEl ? null : event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const open = Boolean(anchorEl)
const id = open ? 'simple-popper' : undefined
return (
<div className={classes.wrapper}>
{children && (
<>
<div className={classnames(classes.address, className)}>
{children}
</div>
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
<ReactCopyToClipboard text={R.replace(/\s/g, '')(children)}>
<button
aria-describedby={id}
onClick={event => handleClick(event)}>
<CopyIcon />
</button>
</ReactCopyToClipboard>
</div>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
arrowSize={3}
bgColor={comet}
placement="top">
<div className={classes.popoverContent}>
<div>Copied to clipboard!</div>
</div>
</Popover>
</>
)}
</div>
)
}
export default CopyToClipboard

View file

@ -17,12 +17,11 @@ import {
Td,
Th
} from 'src/components/fake-table/Table'
import styles from 'src/components/tables/DataTable.styles'
import { H4 } from 'src/components/typography'
import { ReactComponent as ExpandClosedIcon } from 'src/styling/icons/action/expand/closed.svg'
import { ReactComponent as ExpandOpenIcon } from 'src/styling/icons/action/expand/open.svg'
import styles from './DataTable.styles'
const useStyles = makeStyles(styles)
const Row = ({

View file

@ -1,45 +0,0 @@
import { zircon } from 'src/styling/variables'
const styles = {
expandButton: {
outline: 'none',
border: 'none',
backgroundColor: 'transparent',
cursor: 'pointer',
padding: 4
},
rowWrapper: {
// workaround to shadows cut by r-virtualized when scroll is visible
padding: 1
},
row: {
border: [[2, 'solid', 'transparent']],
borderRadius: 0
},
expanded: {
border: [[2, 'solid', zircon]],
boxShadow: '0 0 8px 0 rgba(0,0,0,0.08)'
},
before: {
paddingTop: 12
},
after: {
paddingBottom: 12
},
pointer: {
cursor: 'pointer'
},
body: {
flex: [[1, 1, 'auto']]
},
table: ({ width }) => ({
marginBottom: 30,
minHeight: 200,
width,
flex: 1,
display: 'flex',
flexDirection: 'column'
})
}
export default styles

View file

@ -1,195 +0,0 @@
import { makeStyles, Box } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import moment from 'moment'
import React, { memo } from 'react'
import { IDButton } from 'src/components/buttons'
import { Label1 } from 'src/components/typography'
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
import { ReactComponent as PhoneIdInverseIcon } from 'src/styling/icons/ID/phone/white.svg'
import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg'
import { ReactComponent as CamIdInverseIcon } from 'src/styling/icons/ID/photo/white.svg'
import { ReactComponent as CamIdIcon } from 'src/styling/icons/ID/photo/zodiac.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 { URI } from 'src/utils/apollo'
import { toUnit, formatCryptoAddress } from 'src/utils/coin'
import { onlyFirstToUpper } from 'src/utils/string'
import CopyToClipboard from './CopyToClipboard'
import styles from './DetailsCard.styles'
import { getStatus } from './helper'
const useStyles = makeStyles(styles)
const formatAddress = (cryptoCode = '', address = '') =>
formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const DetailsRow = ({ it: tx }) => {
const classes = useStyles()
const fiat = Number.parseFloat(tx.fiat)
const crypto = toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode)
const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2)
const commission = Number(fiat * commissionPercentage).toFixed(2)
const exchangeRate = Number(fiat / crypto).toFixed(3)
const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}`
const customer = tx.customerIdCardData && {
name: `${onlyFirstToUpper(
tx.customerIdCardData.firstName
)} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`,
age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'),
country: tx.customerIdCardData.country,
idCardNumber: tx.customerIdCardData.documentNumber,
idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format(
'DD-MM-YYYY'
)
}
return (
<div className={classes.wrapper}>
<div className={classes.row}>
<div className={classes.direction}>
<Label>Direction</Label>
<div>
<span className={classes.txIcon}>
{tx.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}
</span>
<span>{tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'}</span>
</div>
</div>
<div className={classes.availableIds}>
<Label>Available IDs</Label>
<Box display="flex" flexDirection="row">
{tx.customerPhone && (
<IDButton
className={classes.idButton}
name="phone"
Icon={PhoneIdIcon}
InverseIcon={PhoneIdInverseIcon}>
{tx.customerPhone}
</IDButton>
)}
{tx.customerIdCardPhotoPath && !tx.customerIdCardData && (
<IDButton
popoverClassname={classes.popover}
className={classes.idButton}
name="card"
Icon={CardIdIcon}
InverseIcon={CardIdInverseIcon}>
<img
className={classes.idCardPhoto}
src={`${URI}/id-card-photo/${tx.customerIdCardPhotoPath}`}
alt=""
/>
</IDButton>
)}
{tx.customerIdCardData && (
<IDButton
className={classes.idButton}
name="card"
Icon={CardIdIcon}
InverseIcon={CardIdInverseIcon}>
<div className={classes.idCardDataCard}>
<div>
<div>
<Label>Name</Label>
<div>{customer.name}</div>
</div>
<div>
<Label>Age</Label>
<div>{customer.age}</div>
</div>
<div>
<Label>Country</Label>
<div>{customer.country}</div>
</div>
</div>
<div>
<div>
<Label>ID number</Label>
<div>{customer.idCardNumber}</div>
</div>
<div>
<Label>Expiration date</Label>
<div>{customer.idCardExpirationDate}</div>
</div>
</div>
</div>
</IDButton>
)}
{tx.customerFrontCameraPath && (
<IDButton
name="cam"
Icon={CamIdIcon}
InverseIcon={CamIdInverseIcon}>
<img
src={`${URI}/front-camera-photo/${tx.customerFrontCameraPath}`}
alt=""
/>
</IDButton>
)}
</Box>
</div>
<div className={classes.exchangeRate}>
<Label>Exchange rate</Label>
<div>{crypto > 0 ? displayExRate : '-'}</div>
</div>
<div className={classes.commission}>
<Label>Commission</Label>
<div>
{`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}
</div>
</div>
<div>
<Label>Fixed fee</Label>
<div>
{tx.txClass === 'cashIn'
? `${Number.parseFloat(tx.cashInFee)} ${tx.fiatCode}`
: 'N/A'}
</div>
</div>
</div>
<div className={classes.secondRow}>
<div className={classes.address}>
<Label>Address</Label>
<div>
<CopyToClipboard>
{formatAddress(tx.cryptoCode, tx.toAddress)}
</CopyToClipboard>
</div>
</div>
<div className={classes.transactionId}>
<Label>Transaction ID</Label>
<div>
{tx.txClass === 'cashOut' ? (
'N/A'
) : (
<CopyToClipboard>{tx.txHash}</CopyToClipboard>
)}
</div>
</div>
<div className={classes.sessionId}>
<Label>Session ID</Label>
<CopyToClipboard>{tx.id}</CopyToClipboard>
</div>
</div>
<div className={classes.lastRow}>
<div>
<Label>Transaction status</Label>
<span className={classes.bold}>{getStatus(tx)}</span>
</div>
</div>
</div>
)
}
export default memo(DetailsRow, (prev, next) => prev.id === next.id)

View file

@ -1,86 +0,0 @@
import typographyStyles from 'src/components/typography/styles'
import { offColor } from 'src/styling/variables'
const { p } = typographyStyles
const styles = {
wrapper: {
display: 'flex',
flexDirection: 'column',
marginTop: 24
},
row: {
display: 'flex',
flexDirection: 'row',
marginBottom: 36
},
secondRow: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 36
},
lastRow: {
display: 'flex',
flexDirection: 'row',
marginBottom: 32
},
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
txIcon: {
marginRight: 10
},
popover: {
height: 164,
width: 215
},
idButton: {
marginRight: 4
},
idCardDataCard: {
extend: p,
display: 'flex',
padding: [[11, 8]],
// rework this into a proper component
'& > div': {
display: 'flex',
flexDirection: 'column',
'& > div': {
width: 144,
height: 37,
marginBottom: 15,
'&:last-child': {
marginBottom: 0
}
}
}
},
bold: {
fontWeight: 700
},
direction: {
width: 233
},
availableIds: {
width: 232
},
exchangeRate: {
width: 250
},
commission: {
width: 217
},
address: {
width: 280
},
transactionId: {
width: 280
},
sessionId: {
width: 215
}
}
export default styles

View file

@ -1,12 +0,0 @@
import React from 'react'
import { Td } from 'src/components/fake-table/Table'
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
const Stripes = ({ width }) => (
<Td width={width}>
<StripesSvg />
</Td>
)
export default Stripes

View file

@ -6,22 +6,26 @@ import moment from 'moment'
import * as R from 'ramda'
import React, { useEffect, useState } from 'react'
import DetailsRow from 'src/pages/Transactions/DetailsCard'
import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
import { getStatus } from 'src/pages/Transactions/helper'
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 { toUnit, formatCryptoAddress } from 'src/utils/coin'
import DataTable from './DataTable'
import DetailsRow from './DetailsCard'
import { mainStyles } from './Transactions.styles'
import { getStatus } from './helper'
const useStyles = makeStyles(mainStyles)
const NUM_LOG_RESULTS = 5
const GET_TRANSACTIONS = gql`
query transactions($limit: Int, $from: Date, $until: Date, $id: ID) {
transactions(limit: $limit, from: $from, until: $until, id: $id) {
query transactions($limit: Int, $from: Date, $until: Date, $deviceId: ID) {
transactions(
limit: $limit
from: $from
until: $until
deviceId: $deviceId
) {
id
txClass
txHash
@ -61,7 +65,7 @@ const Transactions = ({ id }) => {
{
variables: {
limit: NUM_LOG_RESULTS,
id
deviceId: id
}
}
)

View file

@ -1,23 +0,0 @@
const getCashOutStatus = it => {
if (it.hasError) return 'Error'
if (it.dispense) return 'Success'
if (it.expired) return 'Expired'
return 'Pending'
}
const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error'
if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired'
return 'Pending'
}
const getStatus = it => {
if (it.class === 'cashOut') {
return getCashOutStatus(it)
}
return getCashInStatus(it)
}
export { getStatus }

View file

@ -1,11 +1,15 @@
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import styles from './Machines.styles'
const useStyles = makeStyles(styles)
const MachineSidebar = ({ data, getText, getKey, isSelected, selectItem }) => {
const classes = useStyles()
return (
<List style={{ height: 400, overflowY: 'auto' }}>
<List className={classes.sidebarContainer}>
{data.map((item, idx) => {
return (
<ListItem
@ -20,8 +24,6 @@ const MachineSidebar = ({ data, getText, getKey, isSelected, selectItem }) => {
})}
</List>
)
/* return data.map(item => <button key={getKey(item)}>{getText(item)}</button>) */
}
export default MachineSidebar

View file

@ -3,6 +3,7 @@ import Breadcrumbs from '@material-ui/core/Breadcrumbs'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles'
import NavigateNextIcon from '@material-ui/icons/NavigateNext'
import classnames from 'classnames'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
@ -66,59 +67,58 @@ const Machines = () => {
}, [loading, data, location.state])
return (
<>
<Grid container className={classes.grid}>
<Grid item xs={3}>
<Grid item xs={12}>
<div style={{ marginTop: 32 }}>
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
<Link to="/dashboard" style={{ textDecoration: 'none' }}>
<Label3 className={classes.subtitle}>Dashboard</Label3>
</Link>
<TL2 className={classes.subtitle}>{selectedMachine}</TL2>
</Breadcrumbs>
<Overview data={machineInfo} onActionSuccess={refetch} />
</div>
</Grid>
<Grid item xs={12}>
<Sidebar
isSelected={R.equals(selectedMachine)}
selectItem={setSelectedMachine}
data={machines}
getText={R.prop('name')}
getKey={R.prop('deviceId')}
/>
</Grid>
</Grid>
<Grid item xs={9}>
<div className={classes.content}>
<div className={classes.detailItem} style={{ marginTop: 24 }}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machineInfo} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes
refetchData={refetch}
machine={machineInfo}
config={data?.config ?? false}
/>
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineInfo?.deviceId ?? null} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions
name={'commissions'}
id={machineInfo?.deviceId ?? null}
/>
</div>
<Grid container className={classes.grid}>
<Grid item xs={3}>
<Grid item xs={12}>
<div className={classes.breadcrumbsContainer}>
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
<Link to="/dashboard" className={classes.breadcrumbLink}>
<Label3 className={classes.subtitle}>Dashboard</Label3>
</Link>
<TL2 className={classes.subtitle}>{selectedMachine}</TL2>
</Breadcrumbs>
<Overview data={machineInfo} onActionSuccess={refetch} />
</div>
</Grid>
<Grid item xs={12}>
<Sidebar
isSelected={R.equals(selectedMachine)}
selectItem={setSelectedMachine}
data={machines}
getText={R.prop('name')}
getKey={R.prop('deviceId')}
/>
</Grid>
</Grid>
</>
<Grid item xs={9}>
<div className={classes.content}>
<div
className={classnames(classes.detailItem, classes.detailsMargin)}>
<TL1 className={classes.subtitle}>{'Details'}</TL1>
<Details data={machineInfo} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
<Cassettes
refetchData={refetch}
machine={machineInfo}
config={data?.config ?? false}
/>
</div>
<div className={classes.transactionsItem}>
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
<Transactions id={machineInfo?.deviceId ?? null} />
</div>
<div className={classes.detailItem}>
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
<Commissions
name={'commissions'}
id={machineInfo?.deviceId ?? null}
/>
</div>
</div>
</Grid>
</Grid>
)
}

View file

@ -69,6 +69,19 @@ const styles = {
},
actionButton: {
marginRight: 8
},
breadcrumbsContainer: {
marginTop: 32
},
breadcrumbLink: {
textDecoration: 'none'
},
detailsMargin: {
marginTop: 24
},
sidebarContainer: {
height: 400,
overflowY: 'auto'
}
}

View file

@ -51,18 +51,6 @@ const useStyles = makeStyles({
})
const tree = [
/* {
key: 'dashboard',
label: 'Dashboard',
route: '/dashboard',
component: Dashboard
}, */
/* {
key: 'machines',
label: 'Machines',
route: '/machines',
component: Machines
}, */
{
key: 'transactions',
label: 'Transactions',

View file

@ -108,6 +108,13 @@ export default createMuiTheme({
color: fontColor
}
}
},
MuiListItem: {
root: {
'&:nth-of-type(odd)': {
backgroundColor: backgroundColor
}
}
}
}
})

View file

@ -1,7 +1,7 @@
import * as sanctuary from 'sanctuary'
const checkOnlyDev = () => {
if (!process.env.NODE_ENV === 'production') return false
if (process.env.NODE_ENV !== 'production') return false
return (
process.env.NODE_ENV === 'development' &&