Merge branch 'dev' into backport/commission-rounding
This commit is contained in:
commit
1b9c6f7df7
100 changed files with 1718 additions and 348 deletions
|
|
@ -55,6 +55,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||
common.logger.info(`fallbackfee already defined, skipping...`)
|
||||
} else {
|
||||
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
|
||||
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||
common.logger.info(`rpcworkqueue already defined, skipping...`)
|
||||
} else {
|
||||
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
|
||||
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (isCurrentlyRunning && !isDevMode()) {
|
||||
common.logger.info('Starting wallet...')
|
||||
common.es(`sudo supervisorctl start bitcoin`)
|
||||
|
|
|
|||
|
|
@ -148,18 +148,17 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
|||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// Important: We don't know what kind of error this is
|
||||
// so not safe to assume that funds weren't sent.
|
||||
// Therefore, don't set sendPending to false except for
|
||||
// errors (like InsufficientFundsError) that are guaranteed
|
||||
// not to send.
|
||||
const sendPending = err.name !== 'InsufficientFundsError'
|
||||
// Important: We don't know what kind of error this is
|
||||
// so not safe to assume that funds weren't sent.
|
||||
|
||||
// Setting sendPending to true ensures that the transaction gets
|
||||
// silently terminated and no retries are done
|
||||
|
||||
return {
|
||||
sendTime: 'now()^',
|
||||
error: err.message,
|
||||
errorCode: err.name,
|
||||
sendPending
|
||||
sendPending: true
|
||||
}
|
||||
})
|
||||
.then(sendRec => {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
|
|||
|
||||
function convertBigNumFields (obj) {
|
||||
const convert = (value, key) => {
|
||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat' ])) {
|
||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee', 'fixedFeeCrypto' ])) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
|||
const cashInFee = showCommissions ? commissions.cashIn / 100 : null
|
||||
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
||||
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
||||
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
|
||||
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
|
||||
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
|||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInFixedFee,
|
||||
cashOutFixedFee,
|
||||
cashInRate,
|
||||
cashOutRate
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function getWithEmail (email) {
|
|||
*
|
||||
* @param {string} id Customer's id
|
||||
* @param {object} data Fields to update
|
||||
* @param {string} Acting user's token
|
||||
* @param {string} userToken Acting user's token
|
||||
*
|
||||
* @returns {Promise} Newly updated Customer
|
||||
*/
|
||||
|
|
@ -114,6 +114,7 @@ function update (id, data, userToken) {
|
|||
async function updateCustomer (id, data, userToken) {
|
||||
const formattedData = _.pick(
|
||||
[
|
||||
'sanctions',
|
||||
'authorized_override',
|
||||
'id_card_photo_override',
|
||||
'id_card_data_override',
|
||||
|
|
@ -229,7 +230,7 @@ function enhanceEditedPhotos (fields) {
|
|||
/**
|
||||
* Remove the edited data from the db record
|
||||
*
|
||||
* @name enhanceOverrideFields
|
||||
* @name deleteEditedData
|
||||
* @function
|
||||
*
|
||||
* @param {string} id Customer's id
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
'cashInCommission',
|
||||
'cashInFee',
|
||||
'cashOutCommission',
|
||||
'cashOutFee',
|
||||
'cryptoCode',
|
||||
'cryptoCodeDisplay',
|
||||
'cryptoNetwork',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type Coin {
|
|||
display: String!
|
||||
minimumTx: String!
|
||||
cashInFee: String!
|
||||
cashOutFee: String!
|
||||
cashInCommission: String!
|
||||
cashOutCommission: String!
|
||||
cryptoNetwork: String!
|
||||
|
|
|
|||
15
lib/middlewares/addRWBytes.js
Normal file
15
lib/middlewares/addRWBytes.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const addRWBytes = () => (req, res, next) => {
|
||||
const handle = () => {
|
||||
res.removeListener('finish', handle)
|
||||
res.removeListener('close', handle)
|
||||
res.bytesRead = req.connection.bytesRead
|
||||
res.bytesWritten = req.connection.bytesWritten
|
||||
}
|
||||
|
||||
res.on('finish', handle)
|
||||
res.on('close', handle)
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = addRWBytes
|
||||
|
|
@ -14,6 +14,7 @@ const machine = require('./machine.resolver')
|
|||
const notification = require('./notification.resolver')
|
||||
const pairing = require('./pairing.resolver')
|
||||
const rates = require('./rates.resolver')
|
||||
const sanctions = require('./sanctions.resolver')
|
||||
const scalar = require('./scalar.resolver')
|
||||
const settings = require('./settings.resolver')
|
||||
const sms = require('./sms.resolver')
|
||||
|
|
@ -37,6 +38,7 @@ const resolvers = [
|
|||
notification,
|
||||
pairing,
|
||||
rates,
|
||||
sanctions,
|
||||
scalar,
|
||||
settings,
|
||||
sms,
|
||||
|
|
|
|||
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const sanctions = require('../../../sanctions')
|
||||
const authentication = require('../modules/userManagement')
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
||||
const token = authentication.getToken(context)
|
||||
return sanctions.checkByUser(customerId, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -14,6 +14,7 @@ const machine = require('./machine.type')
|
|||
const notification = require('./notification.type')
|
||||
const pairing = require('./pairing.type')
|
||||
const rates = require('./rates.type')
|
||||
const sanctions = require('./sanctions.type')
|
||||
const scalar = require('./scalar.type')
|
||||
const settings = require('./settings.type')
|
||||
const sms = require('./sms.type')
|
||||
|
|
@ -37,6 +38,7 @@ const types = [
|
|||
notification,
|
||||
pairing,
|
||||
rates,
|
||||
sanctions,
|
||||
scalar,
|
||||
settings,
|
||||
sms,
|
||||
|
|
|
|||
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type SanctionMatches {
|
||||
ofacSanctioned: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
checkAgainstSanctions(customerId: ID): SanctionMatches @auth
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = typeDef
|
||||
|
|
@ -23,7 +23,7 @@ const typeDef = gql`
|
|||
errorCode: String
|
||||
operatorCompleted: Boolean
|
||||
sendPending: Boolean
|
||||
cashInFee: String
|
||||
fixedFee: String
|
||||
minimumTx: Float
|
||||
customerId: ID
|
||||
isAnonymous: Boolean
|
||||
|
|
|
|||
|
|
@ -50,7 +50,19 @@ function batch (
|
|||
excludeTestingCustomers = false,
|
||||
simplified
|
||||
) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addProfits, addNames)
|
||||
const packager = _.flow(
|
||||
_.flatten,
|
||||
_.orderBy(_.property('created'), ['desc']),
|
||||
_.map(_.flow(
|
||||
camelize,
|
||||
_.mapKeys(k =>
|
||||
k == 'cashInFee' ? 'fixedFee' :
|
||||
k
|
||||
)
|
||||
)),
|
||||
addProfits,
|
||||
addNames
|
||||
)
|
||||
|
||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||
c.phone AS customer_phone,
|
||||
|
|
@ -153,7 +165,7 @@ function batch (
|
|||
function advancedBatch (data) {
|
||||
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
|
||||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee',
|
||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
|
||||
|
|
@ -169,7 +181,9 @@ function advancedBatch (data) {
|
|||
...it,
|
||||
status: getStatus(it),
|
||||
fiatProfit: getProfit(it).toString(),
|
||||
cryptoAmount: getCryptoAmount(it).toString()
|
||||
cryptoAmount: getCryptoAmount(it).toString(),
|
||||
fixedFee: it.fixedFee ?? null,
|
||||
fee: it.fee ?? null,
|
||||
}))
|
||||
|
||||
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ function plugins (settings, deviceId) {
|
|||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
const minimumTx = new BN(commissions.minimumTx)
|
||||
const cashInFee = new BN(commissions.fixedFee)
|
||||
const cashOutFee = new BN(commissions.cashOutFixedFee)
|
||||
const cashInCommission = new BN(commissions.cashIn)
|
||||
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
|
|
@ -261,6 +262,7 @@ function plugins (settings, deviceId) {
|
|||
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||
minimumTx: BN.max(minimumTx, cashInFee),
|
||||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInCommission,
|
||||
cashOutCommission,
|
||||
cryptoNetwork,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ const nocache = require('nocache')
|
|||
|
||||
const logger = require('./logger')
|
||||
|
||||
const addRWBytes = require('./middlewares/addRWBytes')
|
||||
const authorize = require('./middlewares/authorize')
|
||||
const computeSchema = require('./middlewares/compute-schema')
|
||||
const errorHandler = require('./middlewares/errorHandler')
|
||||
const filterOldRequests = require('./middlewares/filterOldRequests')
|
||||
const computeSchema = require('./middlewares/compute-schema')
|
||||
const findOperatorId = require('./middlewares/operatorId')
|
||||
const populateDeviceId = require('./middlewares/populateDeviceId')
|
||||
const populateSettings = require('./middlewares/populateSettings')
|
||||
|
|
@ -50,11 +51,24 @@ const configRequiredRoutes = [
|
|||
]
|
||||
|
||||
// middleware setup
|
||||
app.use(addRWBytes())
|
||||
app.use(compression({ threshold: 500 }))
|
||||
app.use(helmet())
|
||||
app.use(nocache())
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
app.use(morgan(':method :url :status :response-time ms -- :req[content-length]/:res[content-length] b', { stream: logger.stream }))
|
||||
|
||||
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
||||
|
||||
app.use('/robots.txt', (req, res) => {
|
||||
res.type('text/plain')
|
||||
res.send("User-agent: *\nDisallow: /")
|
||||
})
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.sendStatus(404)
|
||||
})
|
||||
|
||||
// app /pair and /ca routes
|
||||
app.use('/', pairingRoutes)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ function updateCustomer (req, res, next) {
|
|||
.then(_.merge(patch))
|
||||
.then(newPatch => customers.updatePhotoCard(id, newPatch))
|
||||
.then(newPatch => customers.updateFrontCamera(id, newPatch))
|
||||
.then(newPatch => customers.update(id, newPatch, null, txId))
|
||||
.then(newPatch => customers.update(id, newPatch, null))
|
||||
.then(customer => {
|
||||
createPendingManualComplianceNotifs(settings, customer, deviceId)
|
||||
respond(req, res, { customer })
|
||||
|
|
|
|||
44
lib/sanctions.js
Normal file
44
lib/sanctions.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const _ = require('lodash/fp')
|
||||
const ofac = require('./ofac')
|
||||
const T = require('./time')
|
||||
const logger = require('./logger')
|
||||
const customers = require('./customers')
|
||||
|
||||
const sanctionStatus = {
|
||||
loaded: false,
|
||||
timestamp: null
|
||||
}
|
||||
|
||||
const loadOrUpdateSanctions = () => {
|
||||
if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) {
|
||||
logger.info('No sanction lists loaded. Loading sanctions...')
|
||||
return ofac.load()
|
||||
.then(() => {
|
||||
logger.info('OFAC sanction list loaded!')
|
||||
sanctionStatus.loaded = true
|
||||
sanctionStatus.timestamp = Date.now()
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Couldn\'t load OFAC sanction list!')
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const checkByUser = (customerId, userToken) => {
|
||||
return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)])
|
||||
.then(([, customer]) => {
|
||||
const { firstName, lastName, dateOfBirth } = customer?.idCardData
|
||||
const birthdate = _.replace(/-/g, '')(dateOfBirth)
|
||||
const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false })
|
||||
const isOfacSanctioned = _.size(ofacMatches) > 0
|
||||
customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken)
|
||||
|
||||
return { ofacSanctioned: isOfacSanctioned }
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkByUser
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ function massage (tx, pi) {
|
|||
: {
|
||||
cryptoAtoms: new BN(r.cryptoAtoms),
|
||||
fiat: new BN(r.fiat),
|
||||
fixedFee: new BN(r.fixedFee),
|
||||
rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null,
|
||||
commissionPercentage: new BN(r.commissionPercentage)
|
||||
}
|
||||
|
|
@ -69,7 +70,7 @@ function cancel (txId) {
|
|||
}
|
||||
|
||||
function customerHistory (customerId, thresholdDays) {
|
||||
const sql = `SELECT * FROM (
|
||||
const sql = `SELECT ch.id, ch.created, ch.fiat, ch.direction FROM (
|
||||
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
||||
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
||||
FROM cash_in_txs txIn
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN fixed_fee numeric(14, 5) NOT NULL DEFAULT 0;'
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const { saveConfig } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = next => saveConfig({ 'commissions_cashOutFixedFee': 0 })
|
||||
.then(next)
|
||||
.catch(next)
|
||||
|
||||
exports.down = next => next()
|
||||
2
new-lamassu-admin/public/robots.txt
Normal file
2
new-lamassu-admin/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -13,9 +13,10 @@ const useStyles = makeStyles({
|
|||
display: 'flex'
|
||||
},
|
||||
imgInner: {
|
||||
objectFit: 'cover',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: 500,
|
||||
height: 400,
|
||||
marginBottom: 40
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,16 @@ const useStyles = makeStyles({
|
|||
cursor: 'pointer',
|
||||
marginTop: 4
|
||||
},
|
||||
relativelyPositioned: {
|
||||
position: 'relative'
|
||||
},
|
||||
safeSpace: {
|
||||
position: 'absolute',
|
||||
backgroundColor: '#0000',
|
||||
height: 40,
|
||||
left: '-50%',
|
||||
width: '200%'
|
||||
},
|
||||
popoverContent: ({ width }) => ({
|
||||
width,
|
||||
padding: [[10, 15]]
|
||||
|
|
@ -27,6 +37,10 @@ const usePopperHandler = width => {
|
|||
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const openHelpPopper = event => {
|
||||
setHelpPopperAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleCloseHelpPopper = () => {
|
||||
setHelpPopperAnchorEl(null)
|
||||
}
|
||||
|
|
@ -38,25 +52,32 @@ const usePopperHandler = width => {
|
|||
helpPopperAnchorEl,
|
||||
helpPopperOpen,
|
||||
handleOpenHelpPopper,
|
||||
openHelpPopper,
|
||||
handleCloseHelpPopper
|
||||
}
|
||||
}
|
||||
|
||||
const Tooltip = memo(({ children, width, Icon = HelpIcon }) => {
|
||||
const HelpTooltip = memo(({ children, width }) => {
|
||||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div>
|
||||
<div
|
||||
className={handler.classes.relativelyPositioned}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}>
|
||||
{handler.helpPopperOpen && (
|
||||
<div className={handler.classes.safeSpace}></div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={handler.classes.transparentButton}
|
||||
onClick={handler.handleOpenHelpPopper}>
|
||||
<Icon />
|
||||
onMouseEnter={handler.openHelpPopper}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
arrowEnabled={true}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
|
|
@ -69,31 +90,30 @@ const HoverableTooltip = memo(({ parentElements, children, width }) => {
|
|||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!R.isNil(parentElements) && (
|
||||
<div
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}>
|
||||
{parentElements}
|
||||
</div>
|
||||
)}
|
||||
{R.isNil(parentElements) && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
className={handler.classes.transparentButton}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
)}
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
</div>
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div>
|
||||
{!R.isNil(parentElements) && (
|
||||
<div onMouseEnter={handler.handleOpenHelpPopper}>
|
||||
{parentElements}
|
||||
</div>
|
||||
)}
|
||||
{R.isNil(parentElements) && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
className={handler.classes.transparentButton}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
)}
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
})
|
||||
|
||||
export { Tooltip, HoverableTooltip }
|
||||
export { HoverableTooltip, HelpTooltip }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
TDoubleLevelHead,
|
||||
ThDoubleLevel
|
||||
} from 'src/components/fake-table/Table'
|
||||
import { startCase } from 'src/utils/string'
|
||||
import { sentenceCase } from 'src/utils/string'
|
||||
|
||||
import TableCtx from './Context'
|
||||
|
||||
|
|
@ -22,22 +22,27 @@ const styles = {
|
|||
const useStyles = makeStyles(styles)
|
||||
|
||||
const groupSecondHeader = elements => {
|
||||
const [toSHeader, noSHeader] = R.partition(R.has('doubleHeader'))(elements)
|
||||
|
||||
if (!toSHeader.length) {
|
||||
return [elements, THead]
|
||||
}
|
||||
|
||||
const index = R.indexOf(toSHeader[0], elements)
|
||||
const width = R.compose(R.sum, R.map(R.path(['width'])))(toSHeader)
|
||||
|
||||
const innerElements = R.insert(
|
||||
index,
|
||||
{ width, elements: toSHeader, name: toSHeader[0].doubleHeader },
|
||||
noSHeader
|
||||
const doubleHeader = R.prop('doubleHeader')
|
||||
const sameDoubleHeader = (a, b) => doubleHeader(a) === doubleHeader(b)
|
||||
const group = R.pipe(
|
||||
R.groupWith(sameDoubleHeader),
|
||||
R.map(group =>
|
||||
R.isNil(doubleHeader(group[0])) // No doubleHeader
|
||||
? group
|
||||
: [
|
||||
{
|
||||
width: R.sum(R.map(R.prop('width'), group)),
|
||||
elements: group,
|
||||
name: doubleHeader(group[0])
|
||||
}
|
||||
]
|
||||
),
|
||||
R.reduce(R.concat, [])
|
||||
)
|
||||
|
||||
return [innerElements, TDoubleLevelHead]
|
||||
return R.all(R.pipe(doubleHeader, R.isNil), elements)
|
||||
? [elements, THead]
|
||||
: [group(elements), TDoubleLevelHead]
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
|
|
@ -99,7 +104,7 @@ const Header = () => {
|
|||
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
||||
) : (
|
||||
<span className={orderClasses}>
|
||||
{!R.isNil(display) ? display : startCase(name)}{' '}
|
||||
{!R.isNil(display) ? display : sentenceCase(name)}{' '}
|
||||
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ const MachineActions = memo(({ machine, onActionSuccess }) => {
|
|||
display: 'Restart services for'
|
||||
})
|
||||
}}>
|
||||
Restart Services
|
||||
Restart services
|
||||
</ActionButton>
|
||||
{machine.model === 'aveiro' && (
|
||||
<ActionButton
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as R from 'ramda'
|
|||
import React, { useContext } from 'react'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, Info2, P } from 'src/components/typography'
|
||||
|
|
@ -128,9 +128,9 @@ const Accounting = () => {
|
|||
<span className={classes.operation}>
|
||||
{it.description}
|
||||
{!!it.extraInfo && (
|
||||
<HoverableTooltip width={175}>
|
||||
<HelpTooltip width={175}>
|
||||
<P>{it.extraInfo}</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,22 +23,26 @@ import LegendEntry from './components/LegendEntry'
|
|||
import HourOfDayWrapper from './components/wrappers/HourOfDayWrapper'
|
||||
import OverTimeWrapper from './components/wrappers/OverTimeWrapper'
|
||||
import TopMachinesWrapper from './components/wrappers/TopMachinesWrapper'
|
||||
import VolumeOverTimeWrapper from './components/wrappers/VolumeOverTimeWrapper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const MACHINE_OPTIONS = [{ code: 'all', display: 'All machines' }]
|
||||
const REPRESENTING_OPTIONS = [
|
||||
{ code: 'overTime', display: 'Over time' },
|
||||
{ code: 'topMachines', display: 'Top Machines' },
|
||||
{ code: 'volumeOverTime', display: 'Volume' },
|
||||
{ code: 'topMachines', display: 'Top machines' },
|
||||
{ code: 'hourOfTheDay', display: 'Hour of the day' }
|
||||
]
|
||||
const PERIOD_OPTIONS = [
|
||||
{ code: 'day', display: 'Last 24 hours' },
|
||||
{ code: 'threeDays', display: 'Last 3 days' },
|
||||
{ code: 'week', display: 'Last 7 days' },
|
||||
{ code: 'month', display: 'Last 30 days' }
|
||||
]
|
||||
const TIME_OPTIONS = {
|
||||
day: DAY,
|
||||
threeDays: 3 * DAY,
|
||||
week: WEEK,
|
||||
month: MONTH
|
||||
}
|
||||
|
|
@ -77,7 +81,7 @@ const GET_TRANSACTIONS = gql`
|
|||
hasError: error
|
||||
deviceId
|
||||
fiat
|
||||
cashInFee
|
||||
fixedFee
|
||||
fiatCode
|
||||
cryptoAtoms
|
||||
cryptoCode
|
||||
|
|
@ -223,13 +227,11 @@ const Analytics = () => {
|
|||
previous: filteredData(period.code).previous.length
|
||||
}
|
||||
|
||||
const avgAmount = {
|
||||
current:
|
||||
R.sum(R.map(d => d.fiat, filteredData(period.code).current)) /
|
||||
(txs.current === 0 ? 1 : txs.current),
|
||||
previous:
|
||||
R.sum(R.map(d => d.fiat, filteredData(period.code).previous)) /
|
||||
(txs.previous === 0 ? 1 : txs.previous)
|
||||
const median = values => (values.length === 0 ? 0 : R.median(values))
|
||||
|
||||
const medianAmount = {
|
||||
current: median(R.map(d => d.fiat, filteredData(period.code).current)),
|
||||
previous: median(R.map(d => d.fiat, filteredData(period.code).previous))
|
||||
}
|
||||
|
||||
const txVolume = {
|
||||
|
|
@ -265,6 +267,20 @@ const Analytics = () => {
|
|||
currency={fiatLocale}
|
||||
/>
|
||||
)
|
||||
case 'volumeOverTime':
|
||||
return (
|
||||
<VolumeOverTimeWrapper
|
||||
title="Transactions volume over time"
|
||||
representing={representing}
|
||||
period={period}
|
||||
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
|
||||
machines={machineOptions}
|
||||
selectedMachine={machine}
|
||||
handleMachineChange={setMachine}
|
||||
timezone={timezone}
|
||||
currency={fiatLocale}
|
||||
/>
|
||||
)
|
||||
case 'topMachines':
|
||||
return (
|
||||
<TopMachinesWrapper
|
||||
|
|
@ -347,9 +363,9 @@ const Analytics = () => {
|
|||
/>
|
||||
<div className={classes.verticalLine} />
|
||||
<OverviewEntry
|
||||
label="Avg. txn amount"
|
||||
value={avgAmount.current}
|
||||
oldValue={avgAmount.previous}
|
||||
label="Median amount"
|
||||
value={medianAmount.current}
|
||||
oldValue={medianAmount.previous}
|
||||
currency={fiatLocale}
|
||||
/>
|
||||
<div className={classes.verticalLine} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import { offDarkColor, tomato, neon, java } from 'src/styling/variables'
|
||||
import {
|
||||
offColor,
|
||||
offDarkColor,
|
||||
tomato,
|
||||
neon,
|
||||
java
|
||||
} from 'src/styling/variables'
|
||||
|
||||
import typographyStyles from '../../components/typography/styles'
|
||||
|
||||
const { label1 } = typographyStyles
|
||||
|
||||
const styles = {
|
||||
overviewLegend: {
|
||||
|
|
@ -135,6 +145,18 @@ const styles = {
|
|||
topMachinesRadio: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
graphHeaderSwitchBox: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > *': {
|
||||
margin: 0
|
||||
},
|
||||
'& > :first-child': {
|
||||
marginBottom: 2,
|
||||
extend: label1,
|
||||
color: offColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,18 +26,12 @@ const GraphTooltip = ({
|
|||
|
||||
const formattedDateInterval = !R.includes('hourOfDay', representing.code)
|
||||
? [
|
||||
formatDate(
|
||||
dateInterval[1],
|
||||
null,
|
||||
period.code === 'day' ? 'MMM d, HH:mm' : 'MMM d'
|
||||
),
|
||||
formatDate(
|
||||
dateInterval[0],
|
||||
null,
|
||||
period.code === 'day' ? 'HH:mm' : 'MMM d'
|
||||
)
|
||||
formatDate(dateInterval[1], null, 'MMM d'),
|
||||
formatDate(dateInterval[1], null, 'HH:mm'),
|
||||
formatDate(dateInterval[0], null, 'HH:mm')
|
||||
]
|
||||
: [
|
||||
formatDate(dateInterval[1], null, 'MMM d'),
|
||||
formatDateNonUtc(dateInterval[1], 'HH:mm'),
|
||||
formatDateNonUtc(dateInterval[0], 'HH:mm')
|
||||
]
|
||||
|
|
@ -55,10 +49,11 @@ const GraphTooltip = ({
|
|||
|
||||
return (
|
||||
<Paper className={classes.dotOtWrapper}>
|
||||
{!R.includes('hourOfDay', representing.code) && (
|
||||
<Info2 noMargin>{`${formattedDateInterval[0]}`}</Info2>
|
||||
)}
|
||||
<Info2 noMargin>
|
||||
{period.code === 'day' || R.includes('hourOfDay', representing.code)
|
||||
? `${formattedDateInterval[0]} - ${formattedDateInterval[1]}`
|
||||
: `${formattedDateInterval[0]}`}
|
||||
{`${formattedDateInterval[1]} - ${formattedDateInterval[2]}`}
|
||||
</Info2>
|
||||
<P noMargin className={classes.dotOtTransactionAmount}>
|
||||
{R.length(data)}{' '}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Box } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Select } from 'src/components/inputs'
|
||||
import { Select, Switch } from 'src/components/inputs'
|
||||
import { H2 } from 'src/components/typography'
|
||||
import { primaryColor } from 'src/styling/variables'
|
||||
|
||||
|
|
@ -25,11 +25,13 @@ const OverTimeDotGraphHeader = ({
|
|||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [logarithmic, setLogarithmic] = useState()
|
||||
|
||||
const legend = {
|
||||
cashIn: <div className={classes.cashInIcon}></div>,
|
||||
cashOut: <div className={classes.cashOutIcon}></div>,
|
||||
transaction: <div className={classes.txIcon}></div>,
|
||||
average: (
|
||||
median: (
|
||||
<svg height="12" width="18">
|
||||
<path
|
||||
stroke={primaryColor}
|
||||
|
|
@ -53,10 +55,14 @@ const OverTimeDotGraphHeader = ({
|
|||
IconElement={legend.transaction}
|
||||
label={'One transaction'}
|
||||
/>
|
||||
<LegendEntry IconElement={legend.average} label={'Average'} />
|
||||
<LegendEntry IconElement={legend.median} label={'Median'} />
|
||||
</Box>
|
||||
</div>
|
||||
<div className={classes.graphHeaderRight}>
|
||||
<div className={classes.graphHeaderSwitchBox}>
|
||||
<span>Log. scale</span>
|
||||
<Switch onChange={event => setLogarithmic(event.target.checked)} />
|
||||
</div>
|
||||
<Select
|
||||
label="Machines"
|
||||
onSelectedItemChange={handleMachineChange}
|
||||
|
|
@ -74,6 +80,7 @@ const OverTimeDotGraphHeader = ({
|
|||
currency={currency}
|
||||
selectedMachine={selectedMachine}
|
||||
machines={machines}
|
||||
log={logarithmic}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import { Box } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Select, Switch } from 'src/components/inputs'
|
||||
import { H2 } from 'src/components/typography'
|
||||
import { neon, java } from 'src/styling/variables'
|
||||
|
||||
import styles from '../../Analytics.styles'
|
||||
import Graph from '../../graphs/Graph'
|
||||
import LegendEntry from '../LegendEntry'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const VolumeOverTimeGraphHeader = ({
|
||||
title,
|
||||
representing,
|
||||
period,
|
||||
data,
|
||||
machines,
|
||||
selectedMachine,
|
||||
handleMachineChange,
|
||||
timezone,
|
||||
currency
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [logarithmic, setLogarithmic] = useState()
|
||||
|
||||
const legend = {
|
||||
cashIn: (
|
||||
<svg height="12" width="18">
|
||||
<path
|
||||
stroke={java}
|
||||
strokeWidth="3"
|
||||
d="M 3 6 l 12 0"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
cashOut: (
|
||||
<svg height="12" width="18">
|
||||
<path
|
||||
stroke={neon}
|
||||
strokeWidth="3"
|
||||
d="M 3 6 l 12 0"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.graphHeaderWrapper}>
|
||||
<div className={classes.graphHeaderLeft}>
|
||||
<H2 noMargin>{title}</H2>
|
||||
<Box className={classes.graphLegend}>
|
||||
<LegendEntry IconElement={legend.cashIn} label={'Cash-in'} />
|
||||
<LegendEntry IconElement={legend.cashOut} label={'Cash-out'} />
|
||||
</Box>
|
||||
</div>
|
||||
<div className={classes.graphHeaderRight}>
|
||||
<div className={classes.graphHeaderSwitchBox}>
|
||||
<span>Log. scale</span>
|
||||
<Switch onChange={event => setLogarithmic(event.target.checked)} />
|
||||
</div>
|
||||
<Select
|
||||
label="Machines"
|
||||
onSelectedItemChange={handleMachineChange}
|
||||
items={machines}
|
||||
default={machines[0]}
|
||||
selectedItem={selectedMachine}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Graph
|
||||
representing={representing}
|
||||
period={period}
|
||||
data={data}
|
||||
timezone={timezone}
|
||||
currency={currency}
|
||||
selectedMachine={selectedMachine}
|
||||
machines={machines}
|
||||
log={logarithmic}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default VolumeOverTimeGraphHeader
|
||||
|
|
@ -5,6 +5,7 @@ import GraphTooltip from '../components/tooltips/GraphTooltip'
|
|||
|
||||
import HourOfDayBarGraph from './HourOfDayBarGraph'
|
||||
import OverTimeDotGraph from './OverTimeDotGraph'
|
||||
import OverTimeLineGraph from './OverTimeLineGraph'
|
||||
import TopMachinesBarGraph from './TopMachinesBarGraph'
|
||||
|
||||
const GraphWrapper = ({
|
||||
|
|
@ -15,7 +16,8 @@ const GraphWrapper = ({
|
|||
currency,
|
||||
selectedMachine,
|
||||
machines,
|
||||
selectedDay
|
||||
selectedDay,
|
||||
log
|
||||
}) => {
|
||||
const [selectionCoords, setSelectionCoords] = useState(null)
|
||||
const [selectionDateInterval, setSelectionDateInterval] = useState(null)
|
||||
|
|
@ -33,6 +35,20 @@ const GraphWrapper = ({
|
|||
setSelectionDateInterval={setSelectionDateInterval}
|
||||
setSelectionData={setSelectionData}
|
||||
selectedMachine={selectedMachine}
|
||||
log={log}
|
||||
/>
|
||||
)
|
||||
case 'volumeOverTime':
|
||||
return (
|
||||
<OverTimeLineGraph
|
||||
data={data}
|
||||
period={period}
|
||||
timezone={timezone}
|
||||
setSelectionCoords={setSelectionCoords}
|
||||
setSelectionDateInterval={setSelectionDateInterval}
|
||||
setSelectionData={setSelectionData}
|
||||
selectedMachine={selectedMachine}
|
||||
log={log}
|
||||
/>
|
||||
)
|
||||
case 'topMachinesVolume':
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
fontSecondary,
|
||||
subheaderColor
|
||||
} from 'src/styling/variables'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
const Graph = ({
|
||||
|
|
@ -23,7 +24,8 @@ const Graph = ({
|
|||
timezone,
|
||||
setSelectionCoords,
|
||||
setSelectionData,
|
||||
setSelectionDateInterval
|
||||
setSelectionDateInterval,
|
||||
log = false
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ const Graph = ({
|
|||
top: 25,
|
||||
right: 3.5,
|
||||
bottom: 27,
|
||||
left: 36.5
|
||||
left: 38
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
|
@ -46,6 +48,7 @@ const Graph = ({
|
|||
|
||||
const periodDomains = {
|
||||
day: [NOW - DAY, NOW],
|
||||
threeDays: [NOW - 3 * DAY, NOW],
|
||||
week: [NOW - WEEK, NOW],
|
||||
month: [NOW - MONTH, NOW]
|
||||
}
|
||||
|
|
@ -58,6 +61,12 @@ const Graph = ({
|
|||
tick: d3.utcHour.every(1),
|
||||
labelFormat: '%H:%M'
|
||||
},
|
||||
threeDays: {
|
||||
freq: 12,
|
||||
step: 6 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(1),
|
||||
labelFormat: '%a %d'
|
||||
},
|
||||
week: {
|
||||
freq: 7,
|
||||
step: 24 * 60 * 60 * 1000,
|
||||
|
|
@ -164,7 +173,7 @@ const Graph = ({
|
|||
.domain(periodDomains[period.code])
|
||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
||||
|
||||
const y = d3
|
||||
const yLin = d3
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
0,
|
||||
|
|
@ -173,6 +182,16 @@ const Graph = ({
|
|||
.nice()
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const yLog = d3
|
||||
.scaleLog()
|
||||
.domain([
|
||||
(d3.min(data, d => new BigNumber(d.fiat).toNumber()) ?? 1) * 0.9,
|
||||
(d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.1
|
||||
])
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const y = log ? yLog : yLin
|
||||
|
||||
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||
const fullBreakpoints = [
|
||||
graphLimits[1],
|
||||
|
|
@ -219,14 +238,11 @@ const Graph = ({
|
|||
d.getTime() + d.getTimezoneOffset() * MINUTE
|
||||
)
|
||||
})
|
||||
.tickSizeOuter(0)
|
||||
)
|
||||
.call(g => g.select('.domain').remove())
|
||||
.call(g =>
|
||||
g
|
||||
.append('line')
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('y1', -GRAPH_HEIGHT + GRAPH_MARGIN.top + GRAPH_MARGIN.bottom)
|
||||
.attr('x2', GRAPH_MARGIN.left)
|
||||
.select('.domain')
|
||||
.attr('stroke', primaryColor)
|
||||
.attr('stroke-width', 1)
|
||||
),
|
||||
|
|
@ -237,18 +253,23 @@ const Graph = ({
|
|||
g =>
|
||||
g
|
||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||
.call(d3.axisLeft(y).ticks(GRAPH_HEIGHT / 100))
|
||||
.call(g => g.select('.domain').remove())
|
||||
.call(g =>
|
||||
g
|
||||
.selectAll('.tick line')
|
||||
.filter(d => d === 0)
|
||||
.clone()
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.left)
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke', primaryColor)
|
||||
),
|
||||
[GRAPH_MARGIN, y]
|
||||
.call(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.ticks(GRAPH_HEIGHT / 100)
|
||||
.tickSizeOuter(0)
|
||||
.tickFormat(d => {
|
||||
if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
|
||||
|
||||
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||
|
||||
return numberToFiatAmount(d)
|
||||
})
|
||||
)
|
||||
.select('.domain')
|
||||
.attr('stroke', primaryColor)
|
||||
.attr('stroke-width', 1),
|
||||
[GRAPH_MARGIN, y, log]
|
||||
)
|
||||
|
||||
const buildGrid = useCallback(
|
||||
|
|
@ -477,25 +498,23 @@ const Graph = ({
|
|||
|
||||
const buildAvg = useCallback(
|
||||
g => {
|
||||
const median = d3.median(data, d => new BigNumber(d.fiat).toNumber()) ?? 0
|
||||
|
||||
if (log && median === 0) return
|
||||
|
||||
g.attr('stroke', primaryColor)
|
||||
.attr('stroke-width', 3)
|
||||
.attr('stroke-dasharray', '10, 5')
|
||||
.call(g =>
|
||||
g
|
||||
.append('line')
|
||||
.attr(
|
||||
'y1',
|
||||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
||||
)
|
||||
.attr(
|
||||
'y2',
|
||||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
||||
)
|
||||
.attr('y1', 0.5 + y(median))
|
||||
.attr('y2', 0.5 + y(median))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
},
|
||||
[GRAPH_MARGIN, y, data]
|
||||
[GRAPH_MARGIN, y, data, log]
|
||||
)
|
||||
|
||||
const drawData = useCallback(
|
||||
|
|
@ -554,5 +573,6 @@ export default memo(
|
|||
Graph,
|
||||
(prev, next) =>
|
||||
R.equals(prev.period, next.period) &&
|
||||
R.equals(prev.selectedMachine, next.selectedMachine)
|
||||
R.equals(prev.selectedMachine, next.selectedMachine) &&
|
||||
R.equals(prev.log, next.log)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,654 @@
|
|||
import BigNumber from 'bignumber.js'
|
||||
import * as d3 from 'd3'
|
||||
import { getTimezoneOffset } from 'date-fns-tz'
|
||||
import {
|
||||
add,
|
||||
addMilliseconds,
|
||||
compareDesc,
|
||||
differenceInMilliseconds,
|
||||
format,
|
||||
startOfWeek,
|
||||
startOfYear
|
||||
} from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
import {
|
||||
java,
|
||||
neon,
|
||||
subheaderDarkColor,
|
||||
offColor,
|
||||
fontColor,
|
||||
primaryColor,
|
||||
fontSecondary,
|
||||
subheaderColor
|
||||
} from 'src/styling/variables'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
const Graph = ({
|
||||
data,
|
||||
period,
|
||||
timezone,
|
||||
setSelectionCoords,
|
||||
setSelectionData,
|
||||
setSelectionDateInterval,
|
||||
log = false
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
|
||||
const GRAPH_POPOVER_WIDTH = 150
|
||||
const GRAPH_POPOVER_MARGIN = 25
|
||||
const GRAPH_HEIGHT = 401
|
||||
const GRAPH_WIDTH = 1163
|
||||
const GRAPH_MARGIN = useMemo(
|
||||
() => ({
|
||||
top: 25,
|
||||
right: 3.5,
|
||||
bottom: 27,
|
||||
left: 38
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const offset = getTimezoneOffset(timezone)
|
||||
const NOW = Date.now() + offset
|
||||
|
||||
const periodDomains = {
|
||||
day: [NOW - DAY, NOW],
|
||||
threeDays: [NOW - 3 * DAY, NOW],
|
||||
week: [NOW - WEEK, NOW],
|
||||
month: [NOW - MONTH, NOW]
|
||||
}
|
||||
|
||||
const dataPoints = useMemo(
|
||||
() => ({
|
||||
day: {
|
||||
freq: 24,
|
||||
step: 60 * 60 * 1000,
|
||||
tick: d3.utcHour.every(1),
|
||||
labelFormat: '%H:%M'
|
||||
},
|
||||
threeDays: {
|
||||
freq: 12,
|
||||
step: 6 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(1),
|
||||
labelFormat: '%a %d'
|
||||
},
|
||||
week: {
|
||||
freq: 7,
|
||||
step: 24 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(1),
|
||||
labelFormat: '%a %d'
|
||||
},
|
||||
month: {
|
||||
freq: 30,
|
||||
step: 24 * 60 * 60 * 1000,
|
||||
tick: d3.utcDay.every(1),
|
||||
labelFormat: '%d'
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const getPastAndCurrentDayLabels = useCallback(d => {
|
||||
const currentDate = new Date(d)
|
||||
const currentDateDay = currentDate.getUTCDate()
|
||||
const currentDateWeekday = currentDate.getUTCDay()
|
||||
const currentDateMonth = currentDate.getUTCMonth()
|
||||
|
||||
const previousDate = new Date(currentDate.getTime())
|
||||
previousDate.setUTCDate(currentDateDay - 1)
|
||||
|
||||
const previousDateDay = previousDate.getUTCDate()
|
||||
const previousDateWeekday = previousDate.getUTCDay()
|
||||
const previousDateMonth = previousDate.getUTCMonth()
|
||||
|
||||
const daysOfWeek = Array.from(Array(7)).map((_, i) =>
|
||||
format('EEE', add({ days: i }, startOfWeek(new Date())))
|
||||
)
|
||||
|
||||
const months = Array.from(Array(12)).map((_, i) =>
|
||||
format('LLL', add({ months: i }, startOfYear(new Date())))
|
||||
)
|
||||
|
||||
return {
|
||||
previous:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[previousDateMonth]
|
||||
: `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
|
||||
current:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[currentDateMonth]
|
||||
: `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buildTicks = useCallback(
|
||||
domain => {
|
||||
const points = []
|
||||
|
||||
const roundDate = d => {
|
||||
const step = dataPoints[period.code].step
|
||||
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||
}
|
||||
|
||||
for (let i = 0; i <= dataPoints[period.code].freq; i++) {
|
||||
const stepDate = new Date(NOW - i * dataPoints[period.code].step)
|
||||
if (roundDate(stepDate) > domain[1]) continue
|
||||
if (stepDate < domain[0]) continue
|
||||
points.push(roundDate(stepDate))
|
||||
}
|
||||
|
||||
return points
|
||||
},
|
||||
[NOW, dataPoints, period.code]
|
||||
)
|
||||
|
||||
const buildAreas = useCallback(
|
||||
domain => {
|
||||
const points = []
|
||||
|
||||
points.push(domain[1])
|
||||
|
||||
const roundDate = d => {
|
||||
const step = dataPoints[period.code].step
|
||||
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||
}
|
||||
|
||||
for (let i = 0; i <= dataPoints[period.code].freq; i++) {
|
||||
const stepDate = new Date(NOW - i * dataPoints[period.code].step)
|
||||
if (roundDate(stepDate) > new Date(domain[1])) continue
|
||||
if (stepDate < new Date(domain[0])) continue
|
||||
points.push(roundDate(stepDate))
|
||||
}
|
||||
|
||||
points.push(domain[0])
|
||||
|
||||
return points
|
||||
},
|
||||
[NOW, dataPoints, period.code]
|
||||
)
|
||||
|
||||
const x = d3
|
||||
.scaleUtc()
|
||||
.domain(periodDomains[period.code])
|
||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
|
||||
|
||||
// Create a second X axis for mouseover events to be placed correctly across the entire graph width and not limited by X's domain
|
||||
const x2 = d3
|
||||
.scaleUtc()
|
||||
.domain(periodDomains[period.code])
|
||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
||||
|
||||
const bins = buildAreas(x.domain())
|
||||
.sort((a, b) => compareDesc(a.date, b.date))
|
||||
.map(addMilliseconds(-dataPoints[period.code].step))
|
||||
.map((date, i, dates) => {
|
||||
// move first and last bin in such way
|
||||
// that all bin have uniform width
|
||||
if (i === 0)
|
||||
return addMilliseconds(dataPoints[period.code].step, dates[1])
|
||||
else if (i === dates.length - 1)
|
||||
return addMilliseconds(
|
||||
-dataPoints[period.code].step,
|
||||
dates[dates.length - 2]
|
||||
)
|
||||
else return date
|
||||
})
|
||||
.map(date => {
|
||||
const middleOfBin = addMilliseconds(
|
||||
dataPoints[period.code].step / 2,
|
||||
date
|
||||
)
|
||||
|
||||
const txs = data.filter(tx => {
|
||||
const txCreated = new Date(tx.created)
|
||||
const shift = new Date(txCreated.getTime() + offset)
|
||||
|
||||
return (
|
||||
Math.abs(differenceInMilliseconds(shift, middleOfBin)) <
|
||||
dataPoints[period.code].step / 2
|
||||
)
|
||||
})
|
||||
|
||||
const cashIn = txs
|
||||
.filter(tx => tx.txClass === 'cashIn')
|
||||
.reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
|
||||
|
||||
const cashOut = txs
|
||||
.filter(tx => tx.txClass === 'cashOut')
|
||||
.reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
|
||||
|
||||
return { date: middleOfBin, cashIn, cashOut }
|
||||
})
|
||||
|
||||
const min = d3.min(bins, d => Math.min(d.cashIn, d.cashOut)) ?? 0
|
||||
const max = d3.max(bins, d => Math.max(d.cashIn, d.cashOut)) ?? 1000
|
||||
|
||||
const yLin = d3
|
||||
.scaleLinear()
|
||||
.domain([0, (max === min ? min + 1000 : max) * 1.03])
|
||||
.nice()
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const yLog = d3
|
||||
.scaleLog()
|
||||
.domain([
|
||||
min === 0 ? 0.9 : min * 0.9,
|
||||
(max === min ? min + Math.pow(10, 2 * min + 1) : max) * 2
|
||||
])
|
||||
.clamp(true)
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const y = log ? yLog : yLin
|
||||
|
||||
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||
const fullBreakpoints = [
|
||||
graphLimits[1],
|
||||
...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints),
|
||||
dataLimits[0]
|
||||
]
|
||||
|
||||
const intervals = []
|
||||
for (let i = 0; i < fullBreakpoints.length - 1; i++) {
|
||||
intervals.push([fullBreakpoints[i], fullBreakpoints[i + 1]])
|
||||
}
|
||||
|
||||
return intervals
|
||||
}
|
||||
|
||||
const getAreaIntervalByX = (intervals, xValue) => {
|
||||
return R.find(it => xValue <= it[0] && xValue >= it[1], intervals) ?? [0, 0]
|
||||
}
|
||||
|
||||
const getDateIntervalByX = (areas, intervals, xValue) => {
|
||||
const flattenIntervals = R.uniq(R.flatten(intervals))
|
||||
|
||||
// flattenIntervals and areas should have the same number of elements
|
||||
for (let i = intervals.length - 1; i >= 0; i--) {
|
||||
if (xValue < flattenIntervals[i]) {
|
||||
return [areas[i], areas[i + 1]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildXAxis = useCallback(
|
||||
g =>
|
||||
g
|
||||
.attr(
|
||||
'transform',
|
||||
`translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
|
||||
)
|
||||
.call(
|
||||
d3
|
||||
.axisBottom(x)
|
||||
.ticks(dataPoints[period.code].tick)
|
||||
.tickFormat(d => {
|
||||
return d3.timeFormat(dataPoints[period.code].labelFormat)(
|
||||
d.getTime() + d.getTimezoneOffset() * MINUTE
|
||||
)
|
||||
})
|
||||
.tickSizeOuter(0)
|
||||
)
|
||||
.call(g =>
|
||||
g
|
||||
.select('.domain')
|
||||
.attr('stroke', primaryColor)
|
||||
.attr('stroke-width', 1)
|
||||
),
|
||||
[GRAPH_MARGIN, dataPoints, period.code, x]
|
||||
)
|
||||
|
||||
const buildYAxis = useCallback(
|
||||
g =>
|
||||
g
|
||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||
.call(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.ticks(GRAPH_HEIGHT / 100)
|
||||
.tickSizeOuter(0)
|
||||
.tickFormat(d => {
|
||||
if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
|
||||
|
||||
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||
|
||||
return numberToFiatAmount(d)
|
||||
})
|
||||
)
|
||||
.select('.domain')
|
||||
.attr('stroke', primaryColor)
|
||||
.attr('stroke-width', 1),
|
||||
[GRAPH_MARGIN, y, log]
|
||||
)
|
||||
|
||||
const buildGrid = useCallback(
|
||||
g => {
|
||||
g.attr('stroke', subheaderDarkColor)
|
||||
.attr('fill', subheaderDarkColor)
|
||||
// Vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(buildTicks(x.domain()))
|
||||
.join('line')
|
||||
.attr('x1', d => 0.5 + x(d))
|
||||
.attr('x2', d => 0.5 + x(d))
|
||||
.attr('y1', GRAPH_MARGIN.top)
|
||||
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||
)
|
||||
// Horizontal lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.scale()
|
||||
.ticks(GRAPH_HEIGHT / 100)
|
||||
)
|
||||
.join('line')
|
||||
.attr('y1', d => 0.5 + y(d))
|
||||
.attr('y2', d => 0.5 + y(d))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
// Vertical transparent rectangles for events
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(buildAreas(x.domain()))
|
||||
.join('rect')
|
||||
.attr('x', d => x(d))
|
||||
.attr('y', GRAPH_MARGIN.top)
|
||||
.attr('width', d => {
|
||||
const xValue = Math.round(x(d) * 100) / 100
|
||||
const intervals = getAreaInterval(
|
||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||
x.range(),
|
||||
x2.range()
|
||||
)
|
||||
const interval = getAreaIntervalByX(intervals, xValue)
|
||||
return Math.round((interval[0] - interval[1]) * 100) / 100
|
||||
})
|
||||
.attr(
|
||||
'height',
|
||||
GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top
|
||||
)
|
||||
.attr('stroke', 'transparent')
|
||||
.attr('fill', 'transparent')
|
||||
.on('mouseover', d => {
|
||||
const xValue = Math.round(d.target.x.baseVal.value * 100) / 100
|
||||
const areas = buildAreas(x.domain())
|
||||
const intervals = getAreaInterval(
|
||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||
x.range(),
|
||||
x2.range()
|
||||
)
|
||||
|
||||
const dateInterval = getDateIntervalByX(areas, intervals, xValue)
|
||||
if (!dateInterval) return
|
||||
const filteredData = data.filter(it => {
|
||||
const created = new Date(it.created)
|
||||
const tzCreated = created.setTime(created.getTime() + offset)
|
||||
return (
|
||||
tzCreated > new Date(dateInterval[1]) &&
|
||||
tzCreated <= new Date(dateInterval[0])
|
||||
)
|
||||
})
|
||||
|
||||
const rectXCoords = {
|
||||
left: R.clone(d.target.getBoundingClientRect().x),
|
||||
right: R.clone(
|
||||
d.target.getBoundingClientRect().x +
|
||||
d.target.getBoundingClientRect().width
|
||||
)
|
||||
}
|
||||
|
||||
const xCoord =
|
||||
d.target.x.baseVal.value < 0.75 * GRAPH_WIDTH
|
||||
? rectXCoords.right + GRAPH_POPOVER_MARGIN
|
||||
: rectXCoords.left -
|
||||
GRAPH_POPOVER_WIDTH -
|
||||
GRAPH_POPOVER_MARGIN
|
||||
const yCoord = R.clone(d.target.getBoundingClientRect().y)
|
||||
|
||||
setSelectionDateInterval(dateInterval)
|
||||
setSelectionData(filteredData)
|
||||
setSelectionCoords({
|
||||
x: Math.round(xCoord),
|
||||
y: Math.round(yCoord)
|
||||
})
|
||||
|
||||
d3.select(d.target).attr('fill', subheaderColor)
|
||||
})
|
||||
.on('mouseleave', d => {
|
||||
d3.select(d.target).attr('fill', 'transparent')
|
||||
setSelectionDateInterval(null)
|
||||
setSelectionData(null)
|
||||
setSelectionCoords(null)
|
||||
})
|
||||
)
|
||||
// Thick vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(
|
||||
buildTicks(x.domain()).filter(x => {
|
||||
if (period.code === 'day') return x.getUTCHours() === 0
|
||||
return x.getUTCDate() === 1
|
||||
})
|
||||
)
|
||||
.join('line')
|
||||
.attr('class', 'dateSeparator')
|
||||
.attr('x1', d => 0.5 + x(d))
|
||||
.attr('x2', d => 0.5 + x(d))
|
||||
.attr('y1', GRAPH_MARGIN.top - 50)
|
||||
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||
.attr('stroke-width', 5)
|
||||
.join('text')
|
||||
)
|
||||
// Left side breakpoint label
|
||||
.call(g => {
|
||||
const separator = d3
|
||||
?.select('.dateSeparator')
|
||||
?.node()
|
||||
?.getBBox()
|
||||
|
||||
if (!separator) return
|
||||
|
||||
const breakpoint = buildTicks(x.domain()).filter(x => {
|
||||
if (period.code === 'day') return x.getUTCHours() === 0
|
||||
return x.getUTCDate() === 1
|
||||
})
|
||||
|
||||
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||
|
||||
return g
|
||||
.append('text')
|
||||
.attr('x', separator.x - 10)
|
||||
.attr('y', separator.y + 33)
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('dy', '.25em')
|
||||
.text(labels.previous)
|
||||
})
|
||||
// Right side breakpoint label
|
||||
.call(g => {
|
||||
const separator = d3
|
||||
?.select('.dateSeparator')
|
||||
?.node()
|
||||
?.getBBox()
|
||||
|
||||
if (!separator) return
|
||||
|
||||
const breakpoint = buildTicks(x.domain()).filter(x => {
|
||||
if (period.code === 'day') return x.getUTCHours() === 0
|
||||
return x.getUTCDate() === 1
|
||||
})
|
||||
|
||||
const labels = getPastAndCurrentDayLabels(breakpoint)
|
||||
|
||||
return g
|
||||
.append('text')
|
||||
.attr('x', separator.x + 10)
|
||||
.attr('y', separator.y + 33)
|
||||
.attr('text-anchor', 'start')
|
||||
.attr('dy', '.25em')
|
||||
.text(labels.current)
|
||||
})
|
||||
},
|
||||
[
|
||||
GRAPH_MARGIN,
|
||||
buildTicks,
|
||||
getPastAndCurrentDayLabels,
|
||||
x,
|
||||
x2,
|
||||
y,
|
||||
period,
|
||||
buildAreas,
|
||||
data,
|
||||
offset,
|
||||
setSelectionCoords,
|
||||
setSelectionData,
|
||||
setSelectionDateInterval
|
||||
]
|
||||
)
|
||||
|
||||
const formatTicksText = useCallback(
|
||||
() =>
|
||||
d3
|
||||
.selectAll('.tick text')
|
||||
.style('stroke', fontColor)
|
||||
.style('fill', fontColor)
|
||||
.style('stroke-width', 0.5)
|
||||
.style('font-family', fontSecondary),
|
||||
[]
|
||||
)
|
||||
|
||||
const formatText = useCallback(
|
||||
() =>
|
||||
d3
|
||||
.selectAll('text')
|
||||
.style('stroke', offColor)
|
||||
.style('fill', offColor)
|
||||
.style('stroke-width', 0.5)
|
||||
.style('font-family', fontSecondary),
|
||||
[]
|
||||
)
|
||||
|
||||
const formatTicks = useCallback(() => {
|
||||
d3.selectAll('.tick line')
|
||||
.style('stroke', primaryColor)
|
||||
.style('fill', primaryColor)
|
||||
}, [])
|
||||
|
||||
const drawData = useCallback(
|
||||
g => {
|
||||
g.append('clipPath')
|
||||
.attr('id', 'clip-path')
|
||||
.append('rect')
|
||||
.attr('x', GRAPH_MARGIN.left)
|
||||
.attr('y', GRAPH_MARGIN.top)
|
||||
.attr('width', GRAPH_WIDTH)
|
||||
.attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top)
|
||||
.attr('fill', java)
|
||||
|
||||
g.append('g')
|
||||
.attr('clip-path', 'url(#clip-path)')
|
||||
.selectAll('circle .cashIn')
|
||||
.data(bins)
|
||||
.join('circle')
|
||||
.attr('cx', d => x(d.date))
|
||||
.attr('cy', d => y(d.cashIn))
|
||||
.attr('fill', java)
|
||||
.attr('r', d => (d.cashIn === 0 ? 0 : 3.5))
|
||||
|
||||
g.append('path')
|
||||
.datum(bins)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', java)
|
||||
.attr('stroke-width', 3)
|
||||
.attr('clip-path', 'url(#clip-path)')
|
||||
.attr(
|
||||
'd',
|
||||
d3
|
||||
.line()
|
||||
.curve(d3.curveMonotoneX)
|
||||
.x(d => x(d.date))
|
||||
.y(d => y(d.cashIn))
|
||||
)
|
||||
|
||||
g.append('g')
|
||||
.attr('clip-path', 'url(#clip-path)')
|
||||
.selectAll('circle .cashIn')
|
||||
.data(bins)
|
||||
.join('circle')
|
||||
.attr('cx', d => x(d.date))
|
||||
.attr('cy', d => y(d.cashOut))
|
||||
.attr('fill', neon)
|
||||
.attr('r', d => (d.cashOut === 0 ? 0 : 3.5))
|
||||
|
||||
g.append('path')
|
||||
.datum(bins)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', neon)
|
||||
.attr('stroke-width', 3)
|
||||
.attr('clip-path', 'url(#clip-path)')
|
||||
.attr(
|
||||
'd',
|
||||
d3
|
||||
.line()
|
||||
.curve(d3.curveMonotoneX)
|
||||
.x(d => x(d.date))
|
||||
.y(d => y(d.cashOut))
|
||||
)
|
||||
},
|
||||
[x, y, bins, GRAPH_MARGIN]
|
||||
)
|
||||
|
||||
const drawChart = useCallback(() => {
|
||||
const svg = d3
|
||||
.select(ref.current)
|
||||
.attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
|
||||
|
||||
svg.append('g').call(buildGrid)
|
||||
svg.append('g').call(drawData)
|
||||
svg.append('g').call(buildXAxis)
|
||||
svg.append('g').call(buildYAxis)
|
||||
svg.append('g').call(formatTicksText)
|
||||
svg.append('g').call(formatText)
|
||||
svg.append('g').call(formatTicks)
|
||||
|
||||
return svg.node()
|
||||
}, [
|
||||
buildGrid,
|
||||
buildXAxis,
|
||||
buildYAxis,
|
||||
drawData,
|
||||
formatText,
|
||||
formatTicks,
|
||||
formatTicksText
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
d3.select(ref.current)
|
||||
.selectAll('*')
|
||||
.remove()
|
||||
drawChart()
|
||||
}, [drawChart])
|
||||
|
||||
return <svg ref={ref} />
|
||||
}
|
||||
|
||||
export default memo(
|
||||
Graph,
|
||||
(prev, next) =>
|
||||
R.equals(prev.period, next.period) &&
|
||||
R.equals(prev.selectedMachine, next.selectedMachine) &&
|
||||
R.equals(prev.log, next.log)
|
||||
)
|
||||
|
|
@ -7,8 +7,13 @@ import gql from 'graphql-tag'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { Link, Button, IconButton } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import {
|
||||
Link,
|
||||
Button,
|
||||
IconButton,
|
||||
SupportLinkButton
|
||||
} from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
|
@ -251,13 +256,13 @@ const Blacklist = () => {
|
|||
value={enablePaperWalletOnly}
|
||||
/>
|
||||
<Label2>{enablePaperWalletOnly ? 'On' : 'Off'}</Label2>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
The "Enable paper wallet (only)" option means that only paper
|
||||
wallets will be printed for users, and they won't be permitted
|
||||
to scan an address from their own wallet.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
<Box
|
||||
display="flex"
|
||||
|
|
@ -273,13 +278,21 @@ const Blacklist = () => {
|
|||
value={rejectAddressReuse}
|
||||
/>
|
||||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
The "Reject reused addresses" option means that all addresses
|
||||
that are used once will be automatically rejected if there's
|
||||
an attempt to use them again on a new transaction.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
<P>
|
||||
For details please read the relevant knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
|
||||
label="Reject Address Reuse"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<BlacklistTable
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import gql from 'graphql-tag'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
|
@ -92,7 +93,21 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<TitleSection title="Cash-out">
|
||||
<TitleSection
|
||||
title="Cash-out"
|
||||
appendix={
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring cash-out, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115003720192-Enabling-cash-out-on-the-admin"
|
||||
label="Enabling cash-out on the admin"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}>
|
||||
<div className={classes.fudgeFactor}>
|
||||
<P>Transaction fudge factor</P>
|
||||
<Switch
|
||||
|
|
@ -105,7 +120,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
<Label2 className={classes.switchLabel}>
|
||||
{fudgeFactorActive ? 'On' : 'Off'}
|
||||
</Label2>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
Automatically accept customer deposits as complete if their
|
||||
received amount is 100 crypto atoms or less.
|
||||
|
|
@ -114,7 +129,13 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
(Crypto atoms are the smallest unit in each cryptocurrency.
|
||||
E.g., satoshis in Bitcoin, or wei in Ethereum.)
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
<P>For details please read the relevant knowledgebase article:</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360050838011-Automatically-accepting-undersent-deposits-with-Fudge-Factor-"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
</TitleSection>
|
||||
<EditableTable
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ const WizardStep = ({
|
|||
to zero. Make sure you physically put cash inside the cash
|
||||
cassettes to allow the machine to dispense it to your users. If
|
||||
you already did, make sure you set the correct cash cassette bill
|
||||
count for this machine on your Cash Boxes & Cassettes tab under
|
||||
count for this machine on your Cash boxes & cassettes tab under
|
||||
Maintenance.
|
||||
</P>
|
||||
<Info2 className={classes.title}>Default Commissions</Info2>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@ import gql from 'graphql-tag'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { ReactComponent as ReverseListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/white.svg'
|
||||
import { ReactComponent as ListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/zodiac.svg'
|
||||
import { ReactComponent as OverrideLabelIcon } from 'src/styling/icons/status/spring2.svg'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import { P } from '../../components/typography'
|
||||
|
||||
import CommissionsDetails from './components/CommissionsDetails'
|
||||
import CommissionsList from './components/CommissionsList'
|
||||
|
||||
|
|
@ -118,6 +122,24 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
|||
}
|
||||
]}
|
||||
iconClassName={classes.listViewButton}
|
||||
appendix={
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details about commissions, please read the relevant
|
||||
knowledgebase articles:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001211752-Fixed-fees-Minimum-transaction"
|
||||
label="Fixed fees & Minimum transaction"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360061558352-Commissions-and-Profit-Calculations"
|
||||
label="SCommissions and Profit Calculations"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
{!showMachines && !loading && (
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const SHOW_ALL = {
|
|||
const ORDER_OPTIONS = [
|
||||
{
|
||||
code: 'machine',
|
||||
display: 'Machine Name'
|
||||
display: 'Machine name'
|
||||
},
|
||||
{
|
||||
code: 'cryptoCurrencies',
|
||||
|
|
@ -53,7 +53,7 @@ const ORDER_OPTIONS = [
|
|||
},
|
||||
{
|
||||
code: 'fixedFee',
|
||||
display: 'Fixed Fee'
|
||||
display: 'Fixed fee'
|
||||
},
|
||||
{
|
||||
code: 'minimumTx',
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
},
|
||||
{
|
||||
name: 'cryptoCurrencies',
|
||||
width: 280,
|
||||
width: 145,
|
||||
size: 'sm',
|
||||
view: displayCodeArray(cryptoData),
|
||||
input: Autocomplete,
|
||||
|
|
@ -108,7 +108,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
header: cashInHeader,
|
||||
name: 'cashIn',
|
||||
display: 'Cash-in',
|
||||
width: 130,
|
||||
width: 123,
|
||||
input: NumberInput,
|
||||
textAlign: 'right',
|
||||
suffix: '%',
|
||||
|
|
@ -121,7 +121,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
header: cashOutHeader,
|
||||
name: 'cashOut',
|
||||
display: 'Cash-out',
|
||||
width: 130,
|
||||
width: 127,
|
||||
input: NumberInput,
|
||||
textAlign: 'right',
|
||||
suffix: '%',
|
||||
|
|
@ -133,7 +133,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
{
|
||||
name: 'fixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 144,
|
||||
width: 126,
|
||||
input: NumberInput,
|
||||
doubleHeader: 'Cash-in only',
|
||||
textAlign: 'right',
|
||||
|
|
@ -146,7 +146,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
{
|
||||
name: 'minimumTx',
|
||||
display: 'Minimum Tx',
|
||||
width: 169,
|
||||
width: 140,
|
||||
doubleHeader: 'Cash-in only',
|
||||
textAlign: 'center',
|
||||
editingAlign: 'right',
|
||||
|
|
@ -156,6 +156,20 @@ const getOverridesFields = (getData, currency, auxElements) => {
|
|||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cashOutFixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 134,
|
||||
doubleHeader: 'Cash-out only',
|
||||
textAlign: 'center',
|
||||
editingAlign: 'right',
|
||||
input: NumberInput,
|
||||
suffix: currency,
|
||||
bold: bold,
|
||||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -218,6 +232,21 @@ const mainFields = currency => [
|
|||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cashOutFixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 169,
|
||||
size: 'lg',
|
||||
doubleHeader: 'Cash-out only',
|
||||
textAlign: 'center',
|
||||
editingAlign: 'right',
|
||||
input: NumberInput,
|
||||
suffix: currency,
|
||||
bold: bold,
|
||||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -245,7 +274,7 @@ const getSchema = locale => {
|
|||
.max(percentMax)
|
||||
.required(),
|
||||
fixedFee: Yup.number()
|
||||
.label('Fixed Fee')
|
||||
.label('Cash-in fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
|
|
@ -253,6 +282,11 @@ const getSchema = locale => {
|
|||
.label('Minimum Tx')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
cashOutFixedFee: Yup.number()
|
||||
.label('Cash-out fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required()
|
||||
})
|
||||
}
|
||||
|
|
@ -326,7 +360,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
|||
return true
|
||||
}
|
||||
})
|
||||
.label('Crypto Currencies')
|
||||
.label('Crypto currencies')
|
||||
.required()
|
||||
.min(1),
|
||||
cashIn: Yup.number()
|
||||
|
|
@ -340,7 +374,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
|||
.max(percentMax)
|
||||
.required(),
|
||||
fixedFee: Yup.number()
|
||||
.label('Fixed Fee')
|
||||
.label('Cash-in fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
|
|
@ -348,6 +382,11 @@ const getOverridesSchema = (values, rawData, locale) => {
|
|||
.label('Minimum Tx')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
cashOutFixedFee: Yup.number()
|
||||
.label('Cash-out fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required()
|
||||
})
|
||||
}
|
||||
|
|
@ -356,7 +395,8 @@ const defaults = {
|
|||
cashIn: '',
|
||||
cashOut: '',
|
||||
fixedFee: '',
|
||||
minimumTx: ''
|
||||
minimumTx: '',
|
||||
cashOutFixedFee: ''
|
||||
}
|
||||
|
||||
const overridesDefaults = {
|
||||
|
|
@ -365,7 +405,8 @@ const overridesDefaults = {
|
|||
cashIn: '',
|
||||
cashOut: '',
|
||||
fixedFee: '',
|
||||
minimumTx: ''
|
||||
minimumTx: '',
|
||||
cashOutFixedFee: ''
|
||||
}
|
||||
|
||||
const getOrder = ({ machine, cryptoCurrencies }) => {
|
||||
|
|
@ -385,6 +426,7 @@ const createCommissions = (cryptoCode, deviceId, isDefault, config) => {
|
|||
fixedFee: config.fixedFee,
|
||||
cashOut: config.cashOut,
|
||||
cashIn: config.cashIn,
|
||||
cashOutFixedFee: config.cashOutFixedFee,
|
||||
machine: deviceId,
|
||||
cryptoCurrencies: [cryptoCode],
|
||||
default: isDefault,
|
||||
|
|
@ -437,7 +479,7 @@ const getListCommissionsSchema = locale => {
|
|||
.label('Machine')
|
||||
.required(),
|
||||
cryptoCurrencies: Yup.array()
|
||||
.label('Crypto Currency')
|
||||
.label('Crypto currency')
|
||||
.required()
|
||||
.min(1),
|
||||
cashIn: Yup.number()
|
||||
|
|
@ -451,7 +493,7 @@ const getListCommissionsSchema = locale => {
|
|||
.max(percentMax)
|
||||
.required(),
|
||||
fixedFee: Yup.number()
|
||||
.label('Fixed Fee')
|
||||
.label('Cash-in fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
|
|
@ -459,6 +501,11 @@ const getListCommissionsSchema = locale => {
|
|||
.label('Minimum Tx')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required(),
|
||||
cashOutFixedFee: Yup.number()
|
||||
.label('Cash-out fixed fee')
|
||||
.min(0)
|
||||
.max(highestBill)
|
||||
.required()
|
||||
})
|
||||
}
|
||||
|
|
@ -487,7 +534,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
{
|
||||
name: 'cryptoCurrencies',
|
||||
display: 'Crypto Currency',
|
||||
width: 255,
|
||||
width: 150,
|
||||
view: R.prop(0),
|
||||
size: 'sm',
|
||||
editable: false
|
||||
|
|
@ -496,7 +543,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
header: cashInHeader,
|
||||
name: 'cashIn',
|
||||
display: 'Cash-in',
|
||||
width: 130,
|
||||
width: 120,
|
||||
input: NumberInput,
|
||||
textAlign: 'right',
|
||||
suffix: '%',
|
||||
|
|
@ -509,7 +556,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
header: cashOutHeader,
|
||||
name: 'cashOut',
|
||||
display: 'Cash-out',
|
||||
width: 140,
|
||||
width: 126,
|
||||
input: NumberInput,
|
||||
textAlign: 'right',
|
||||
greenText: true,
|
||||
|
|
@ -522,7 +569,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
{
|
||||
name: 'fixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 144,
|
||||
width: 140,
|
||||
input: NumberInput,
|
||||
doubleHeader: 'Cash-in only',
|
||||
textAlign: 'right',
|
||||
|
|
@ -535,7 +582,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
{
|
||||
name: 'minimumTx',
|
||||
display: 'Minimum Tx',
|
||||
width: 144,
|
||||
width: 140,
|
||||
input: NumberInput,
|
||||
doubleHeader: 'Cash-in only',
|
||||
textAlign: 'right',
|
||||
|
|
@ -544,6 +591,20 @@ const getListCommissionsFields = (getData, currency, defaults) => {
|
|||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cashOutFixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 140,
|
||||
input: NumberInput,
|
||||
doubleHeader: 'Cash-out only',
|
||||
textAlign: 'center',
|
||||
editingAlign: 'right',
|
||||
suffix: currency,
|
||||
textStyle: obj => getTextStyle(obj),
|
||||
inputProps: {
|
||||
decimalPlaces: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,10 +73,13 @@ const CustomerData = ({
|
|||
authorizeCustomRequest,
|
||||
updateCustomEntry,
|
||||
retrieveAdditionalDataDialog,
|
||||
setRetrieve
|
||||
setRetrieve,
|
||||
checkAgainstSanctions
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [listView, setListView] = useState(false)
|
||||
const [previewPhoto, setPreviewPhoto] = useState(null)
|
||||
const [previewCard, setPreviewCard] = useState(null)
|
||||
|
||||
const idData = R.path(['idCardData'])(customer)
|
||||
const rawExpirationDate = R.path(['expirationDate'])(idData)
|
||||
|
|
@ -172,6 +175,12 @@ const CustomerData = ({
|
|||
idCardData: R.merge(idData, formatDates(values))
|
||||
}),
|
||||
validationSchema: customerDataSchemas.idCardData,
|
||||
checkAgainstSanctions: () =>
|
||||
checkAgainstSanctions({
|
||||
variables: {
|
||||
customerId: R.path(['id'])(customer)
|
||||
}
|
||||
}),
|
||||
initialValues: initialValues.idCardData,
|
||||
isAvailable: !R.isNil(idData),
|
||||
editable: true
|
||||
|
|
@ -213,9 +222,6 @@ const CustomerData = ({
|
|||
{
|
||||
title: 'Name',
|
||||
titleIcon: <EditIcon className={classes.editIcon} />,
|
||||
authorize: () => {},
|
||||
reject: () => {},
|
||||
save: () => {},
|
||||
isAvailable: false,
|
||||
editable: true
|
||||
},
|
||||
|
|
@ -226,7 +232,7 @@ const CustomerData = ({
|
|||
authorize: () =>
|
||||
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
|
||||
children: <Info3>{sanctionsDisplay}</Info3>,
|
||||
children: () => <Info3>{sanctionsDisplay}</Info3>,
|
||||
isAvailable: !R.isNil(sanctions),
|
||||
editable: true
|
||||
},
|
||||
|
|
@ -238,20 +244,33 @@ const CustomerData = ({
|
|||
authorize: () =>
|
||||
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
|
||||
save: values =>
|
||||
replacePhoto({
|
||||
save: values => {
|
||||
setPreviewPhoto(null)
|
||||
return replacePhoto({
|
||||
newPhoto: values.frontCamera,
|
||||
photoType: 'frontCamera'
|
||||
}),
|
||||
})
|
||||
},
|
||||
cancel: () => setPreviewPhoto(null),
|
||||
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
|
||||
children: customer.frontCameraPath ? (
|
||||
<Photo
|
||||
show={customer.frontCameraPath}
|
||||
src={`${URI}/front-camera-photo/${R.path(['frontCameraPath'])(
|
||||
customer
|
||||
)}`}
|
||||
/>
|
||||
) : null,
|
||||
children: values => {
|
||||
if (values.frontCamera !== previewPhoto) {
|
||||
setPreviewPhoto(values.frontCamera)
|
||||
}
|
||||
|
||||
return customer.frontCameraPath ? (
|
||||
<Photo
|
||||
show={customer.frontCameraPath}
|
||||
src={
|
||||
!R.isNil(previewPhoto)
|
||||
? URL.createObjectURL(previewPhoto)
|
||||
: `${URI}/front-camera-photo/${R.path(['frontCameraPath'])(
|
||||
customer
|
||||
)}`
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
},
|
||||
hasImage: true,
|
||||
validationSchema: customerDataSchemas.frontCamera,
|
||||
initialValues: initialValues.frontCamera,
|
||||
|
|
@ -266,18 +285,33 @@ const CustomerData = ({
|
|||
authorize: () =>
|
||||
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
|
||||
save: values =>
|
||||
replacePhoto({
|
||||
save: values => {
|
||||
setPreviewCard(null)
|
||||
return replacePhoto({
|
||||
newPhoto: values.idCardPhoto,
|
||||
photoType: 'idCardPhoto'
|
||||
}),
|
||||
})
|
||||
},
|
||||
cancel: () => setPreviewCard(null),
|
||||
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
|
||||
children: customer.idCardPhotoPath ? (
|
||||
<Photo
|
||||
show={customer.idCardPhotoPath}
|
||||
src={`${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(customer)}`}
|
||||
/>
|
||||
) : null,
|
||||
children: values => {
|
||||
if (values.idCardPhoto !== previewCard) {
|
||||
setPreviewCard(values.idCardPhoto)
|
||||
}
|
||||
|
||||
return customer.idCardPhotoPath ? (
|
||||
<Photo
|
||||
show={customer.idCardPhotoPath}
|
||||
src={
|
||||
!R.isNil(previewCard)
|
||||
? URL.createObjectURL(previewCard)
|
||||
: `${URI}/id-card-photo/${R.path(['idCardPhotoPath'])(
|
||||
customer
|
||||
)}`
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
},
|
||||
hasImage: true,
|
||||
validationSchema: customerDataSchemas.idCardPhoto,
|
||||
initialValues: initialValues.idCardPhoto,
|
||||
|
|
@ -292,6 +326,7 @@ const CustomerData = ({
|
|||
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
|
||||
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
|
||||
save: values => editCustomer(values),
|
||||
children: () => {},
|
||||
deleteEditedData: () => deleteEditedData({ usSsn: null }),
|
||||
validationSchema: customerDataSchemas.usSsn,
|
||||
initialValues: initialValues.usSsn,
|
||||
|
|
@ -427,6 +462,7 @@ const CustomerData = ({
|
|||
titleIcon,
|
||||
fields,
|
||||
save,
|
||||
cancel,
|
||||
deleteEditedData,
|
||||
retrieveAdditionalData,
|
||||
children,
|
||||
|
|
@ -434,7 +470,8 @@ const CustomerData = ({
|
|||
initialValues,
|
||||
hasImage,
|
||||
hasAdditionalData,
|
||||
editable
|
||||
editable,
|
||||
checkAgainstSanctions
|
||||
},
|
||||
idx
|
||||
) => {
|
||||
|
|
@ -453,8 +490,10 @@ const CustomerData = ({
|
|||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
save={save}
|
||||
cancel={cancel}
|
||||
deleteEditedData={deleteEditedData}
|
||||
retrieveAdditionalData={retrieveAdditionalData}
|
||||
checkAgainstSanctions={checkAgainstSanctions}
|
||||
editable={editable}></EditableCard>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { useQuery, useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import {
|
||||
makeStyles,
|
||||
Breadcrumbs,
|
||||
|
|
@ -292,6 +292,14 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const CHECK_AGAINST_SANCTIONS = gql`
|
||||
query checkAgainstSanctions($customerId: ID) {
|
||||
checkAgainstSanctions(customerId: $customerId) {
|
||||
ofacSanctioned
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CustomerProfile = memo(() => {
|
||||
const history = useHistory()
|
||||
|
||||
|
|
@ -400,6 +408,10 @@ const CustomerProfile = memo(() => {
|
|||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const [checkAgainstSanctions] = useLazyQuery(CHECK_AGAINST_SANCTIONS, {
|
||||
onCompleted: () => getCustomer()
|
||||
})
|
||||
|
||||
const updateCustomer = it =>
|
||||
setCustomer({
|
||||
variables: {
|
||||
|
|
@ -662,6 +674,7 @@ const CustomerProfile = memo(() => {
|
|||
authorizeCustomRequest={authorizeCustomRequest}
|
||||
updateCustomEntry={updateCustomEntry}
|
||||
setRetrieve={setRetrieve}
|
||||
checkAgainstSanctions={checkAgainstSanctions}
|
||||
retrieveAdditionalDataDialog={
|
||||
<RetrieveDataDialog
|
||||
onDismissed={() => {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const CustomersList = ({
|
|||
view: getName
|
||||
},
|
||||
{
|
||||
header: 'Total TXs',
|
||||
header: 'Total Txs',
|
||||
width: 126,
|
||||
textAlign: 'right',
|
||||
view: it => `${Number.parseInt(it.totalTxs)}`
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
|
|||
},
|
||||
{
|
||||
code: 'customerData',
|
||||
display: 'Customer Data',
|
||||
display: 'Customer data',
|
||||
Icon: CustomerDataIcon,
|
||||
InverseIcon: CustomerDataReversedIcon
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import classnames from 'classnames'
|
||||
import { Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import { useState, React } from 'react'
|
||||
import { useState, React, useRef } from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { MainStatus } from 'src/components/Status'
|
||||
// import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { ActionButton } from 'src/components/buttons'
|
||||
import { Label1, P, H3 } from 'src/components/typography'
|
||||
import {
|
||||
|
|
@ -132,23 +133,27 @@ const ReadOnlyField = ({ field, value, ...props }) => {
|
|||
|
||||
const EditableCard = ({
|
||||
fields,
|
||||
save,
|
||||
authorize,
|
||||
save = () => {},
|
||||
cancel = () => {},
|
||||
authorize = () => {},
|
||||
hasImage,
|
||||
reject,
|
||||
reject = () => {},
|
||||
state,
|
||||
title,
|
||||
titleIcon,
|
||||
children,
|
||||
children = () => {},
|
||||
validationSchema,
|
||||
initialValues,
|
||||
deleteEditedData,
|
||||
retrieveAdditionalData,
|
||||
hasAdditionalData = true,
|
||||
editable
|
||||
editable,
|
||||
checkAgainstSanctions
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const formRef = useRef()
|
||||
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [input, setInput] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
|
@ -178,7 +183,7 @@ const EditableCard = ({
|
|||
<H3 className={classes.cardTitle}>{title}</H3>
|
||||
{
|
||||
// TODO: Enable for next release
|
||||
/* <HoverableTooltip width={304}></HoverableTooltip> */
|
||||
/* <HelpTooltip width={304}></HelpTooltip> */
|
||||
}
|
||||
</div>
|
||||
{state && authorize && (
|
||||
|
|
@ -187,8 +192,9 @@ const EditableCard = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
{children(formRef.current?.values ?? {})}
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
|
|
@ -273,6 +279,16 @@ const EditableCard = ({
|
|||
Retrieve API data
|
||||
</ActionButton>
|
||||
)}
|
||||
{checkAgainstSanctions && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
type="button"
|
||||
Icon={DataIcon}
|
||||
InverseIcon={DataReversedIcon}
|
||||
onClick={() => checkAgainstSanctions()}>
|
||||
Check against OFAC sanction list
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{editable && (
|
||||
<ActionButton
|
||||
|
|
@ -314,7 +330,7 @@ const EditableCard = ({
|
|||
{editing && (
|
||||
<div className={classes.editingWrapper}>
|
||||
<div className={classes.replace}>
|
||||
{hasImage && (
|
||||
{hasImage && state !== OVERRIDE_PENDING && (
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
type="button"
|
||||
|
|
@ -359,6 +375,7 @@ const EditableCard = ({
|
|||
color="secondary"
|
||||
Icon={CancelReversedIcon}
|
||||
InverseIcon={CancelReversedIcon}
|
||||
onClick={() => cancel()}
|
||||
type="reset">
|
||||
Cancel
|
||||
</ActionButton>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
|||
size: 160
|
||||
},
|
||||
{
|
||||
header: 'Birth Date',
|
||||
header: 'Birth date',
|
||||
display:
|
||||
(rawDob &&
|
||||
format('yyyy-MM-dd')(parse(new Date(), 'yyyyMMdd', rawDob))) ??
|
||||
|
|
@ -61,7 +61,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
|||
size: 120
|
||||
},
|
||||
{
|
||||
header: 'Expiration Date',
|
||||
header: 'Expiration date',
|
||||
display: ifNotNull(
|
||||
rawExpirationDate,
|
||||
format('yyyy-MM-dd', rawExpirationDate)
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ const customerDataElements = {
|
|||
},
|
||||
{
|
||||
name: 'expirationDate',
|
||||
label: 'Expiration Date',
|
||||
label: 'Expiration date',
|
||||
component: TextInput,
|
||||
editable: true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ 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 transactionProfit = R.prop('profit')
|
||||
|
||||
const mockPoint = (tx, offsetMs, profit) => {
|
||||
const date = new Date(new Date(tx.created).getTime() + offsetMs).toISOString()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
fontSecondary,
|
||||
backgroundColor
|
||||
} from 'src/styling/variables'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
const Graph = ({ data, timeFrame, timezone }) => {
|
||||
|
|
@ -172,7 +173,16 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
g =>
|
||||
g
|
||||
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
|
||||
.call(d3.axisLeft(y).ticks(5))
|
||||
.call(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.ticks(5)
|
||||
.tickFormat(d => {
|
||||
if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
|
||||
|
||||
return numberToFiatAmount(d)
|
||||
})
|
||||
)
|
||||
.call(g => g.select('.domain').remove())
|
||||
.selectAll('text')
|
||||
.attr('dy', '-0.25rem'),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const GET_DATA = gql`
|
|||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
||||
fiatCode
|
||||
fiat
|
||||
cashInFee
|
||||
fixedFee
|
||||
commissionPercentage
|
||||
created
|
||||
txClass
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ const Funding = () => {
|
|||
{funding.length && (
|
||||
<div className={classes.total}>
|
||||
<Label1 className={classes.totalTitle}>
|
||||
Total Crypto Balance
|
||||
Total crypto balance
|
||||
</Label1>
|
||||
<Info1 noMargin>
|
||||
{getConfirmedTotal(funding)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Link, SupportLinkButton } from 'src/components/buttons'
|
||||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import Section from 'src/components/layout/Section'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
|
@ -61,8 +62,9 @@ const GET_DATA = gql`
|
|||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
mutation Save($config: JSONObject, $accounts: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
saveAccounts(accounts: $accounts)
|
||||
}
|
||||
`
|
||||
|
||||
|
|
@ -134,9 +136,9 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
return save(newConfig)
|
||||
}
|
||||
|
||||
const save = config => {
|
||||
const save = (config, accounts) => {
|
||||
setDataToSave(null)
|
||||
return saveConfig({ variables: { config } })
|
||||
return saveConfig({ variables: { config, accounts } })
|
||||
}
|
||||
|
||||
const saveOverrides = it => {
|
||||
|
|
@ -162,8 +164,8 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
const onEditingDefault = (it, editing) => setEditingDefault(editing)
|
||||
const onEditingOverrides = (it, editing) => setEditingOverrides(editing)
|
||||
|
||||
const wizardSave = it =>
|
||||
save(toNamespace(namespaces.WALLETS)(it)).then(it => {
|
||||
const wizardSave = (config, accounts) =>
|
||||
save(toNamespace(namespaces.WALLETS)(config), accounts).then(it => {
|
||||
onChangeFunction()
|
||||
setOnChangeFunction(null)
|
||||
return it
|
||||
|
|
@ -176,7 +178,22 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
close={() => setDataToSave(null)}
|
||||
save={() => dataToSave && save(dataToSave)}
|
||||
/>
|
||||
<TitleSection title="Locales" />
|
||||
<TitleSection
|
||||
title="Locales"
|
||||
appendix={
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring languages, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360016257471-Setting-multiple-machine-languages"
|
||||
label="Setting multiple machine languages"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}
|
||||
/>
|
||||
<Section>
|
||||
<EditableTable
|
||||
title="Default settings"
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const LocaleSchema = Yup.object().shape({
|
|||
.label('Country')
|
||||
.required(),
|
||||
fiatCurrency: Yup.string()
|
||||
.label('Fiat Currency')
|
||||
.label('Fiat currency')
|
||||
.required(),
|
||||
languages: Yup.array()
|
||||
.label('Languages')
|
||||
|
|
@ -165,7 +165,7 @@ const LocaleSchema = Yup.object().shape({
|
|||
.min(1)
|
||||
.max(4),
|
||||
cryptoCurrencies: Yup.array()
|
||||
.label('Crypto Currencies')
|
||||
.label('Crypto currencies')
|
||||
.required()
|
||||
.min(1),
|
||||
timezone: Yup.string()
|
||||
|
|
@ -186,7 +186,7 @@ const OverridesSchema = Yup.object().shape({
|
|||
.min(1)
|
||||
.max(4),
|
||||
cryptoCurrencies: Yup.array()
|
||||
.label('Crypto Currencies')
|
||||
.label('Crypto currencies')
|
||||
.required()
|
||||
.min(1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as Yup from 'yup'
|
|||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { NumberInput, Autocomplete } from 'src/components/inputs/formik'
|
||||
import { H3, TL1, P } from 'src/components/typography'
|
||||
|
|
@ -99,7 +99,7 @@ const IndividualDiscountModal = ({
|
|||
<div>
|
||||
<div className={classes.discountRateWrapper}>
|
||||
<H3>Define discount rate</H3>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
This is a percentage discount off of your existing
|
||||
commission rates for a customer entering this code at
|
||||
|
|
@ -110,7 +110,7 @@ const IndividualDiscountModal = ({
|
|||
code is set for 50%, then you'll instead be charging 4%
|
||||
on transactions using the code.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div className={classes.discountInput}>
|
||||
<Field
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as Yup from 'yup'
|
|||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput, NumberInput } from 'src/components/inputs/formik'
|
||||
import { H3, TL1, P } from 'src/components/typography'
|
||||
|
|
@ -69,7 +69,7 @@ const PromoCodesModal = ({ showModal, onClose, errorMsg, addCode }) => {
|
|||
/>
|
||||
<div className={classes.modalLabel2Wrapper}>
|
||||
<H3 className={classes.modalLabel2}>Define discount rate</H3>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
This is a percentage discount off of your existing
|
||||
commission rates for a customer entering this code at the
|
||||
|
|
@ -80,7 +80,7 @@ const PromoCodesModal = ({ showModal, onClose, errorMsg, addCode }) => {
|
|||
set for 50%, then you'll instead be charging 4% on
|
||||
transactions using the code.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div className={classes.discountInput}>
|
||||
<Field
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ const Logs = () => {
|
|||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>Machine Logs</Title>
|
||||
<Title>Machine logs</Title>
|
||||
{logsResponse && (
|
||||
<div className={classes.buttonsWrapper}>
|
||||
<LogsDowloaderPopover
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
|
|||
cashIn: config.cashIn,
|
||||
cashOut: config.cashOut,
|
||||
fixedFee: config.fixedFee,
|
||||
minimumTx: config.minimumTx
|
||||
minimumTx: config.minimumTx,
|
||||
cashOutFixedFee: config.cashOutFixedFee
|
||||
},
|
||||
R.project(
|
||||
['cashIn', 'cashOut', 'fixedFee', 'minimumTx'],
|
||||
['cashIn', 'cashOut', 'fixedFee', 'minimumTx', 'cashOutFixedFee'],
|
||||
R.filter(
|
||||
o =>
|
||||
R.includes(coin.code, o.cryptoCurrencies) ||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,14 @@ const getOverridesFields = currency => {
|
|||
doubleHeader: 'Cash-in only',
|
||||
textAlign: 'right',
|
||||
suffix: currency
|
||||
},
|
||||
{
|
||||
name: 'cashOutFixedFee',
|
||||
display: 'Fixed fee',
|
||||
width: 155,
|
||||
doubleHeader: 'Cash-out only',
|
||||
textAlign: 'right',
|
||||
suffix: currency
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const GET_TRANSACTIONS = gql`
|
|||
hasError: error
|
||||
deviceId
|
||||
fiat
|
||||
cashInFee
|
||||
fixedFee
|
||||
fiatCode
|
||||
cryptoAtoms
|
||||
cryptoCode
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import React, { useState } from 'react'
|
|||
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { IconButton, Button } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip.js'
|
||||
import { IconButton, Button, SupportLinkButton } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { EmptyTable } from 'src/components/table'
|
||||
|
|
@ -204,7 +205,7 @@ const CashCassettes = () => {
|
|||
!dataLoading && (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Cash Boxes & Cassettes"
|
||||
title="Cash boxes & cassettes"
|
||||
buttons={[
|
||||
{
|
||||
text: 'Cash box history',
|
||||
|
|
@ -229,7 +230,20 @@ const CashCassettes = () => {
|
|||
}
|
||||
]}
|
||||
iconClassName={classes.listViewButton}
|
||||
className={classes.tableWidth}>
|
||||
className={classes.tableWidth}
|
||||
appendix={
|
||||
<HelpTooltip width={220}>
|
||||
<P>
|
||||
For details on configuring cash boxes and cassettes, please read
|
||||
the relevant knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/4420839641229-Cash-Boxes-Cassettess"
|
||||
label="Cash Boxes & Cassettes"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}>
|
||||
{!showHistory && (
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Label1 className={classes.cashboxReset}>Cash box resets</Label1>
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
|||
},
|
||||
{
|
||||
name: 'billCount',
|
||||
header: 'Bill Count',
|
||||
header: 'Bill count',
|
||||
width: 115,
|
||||
textAlign: 'left',
|
||||
input: NumberInput,
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
|||
<Item xs>
|
||||
<Container className={classes.row}>
|
||||
<Item xs={2}>
|
||||
<Label>Machine Model</Label>
|
||||
<Label>Machine model</Label>
|
||||
<span>{modelPrettifier[machine.model]}</span>
|
||||
</Item>
|
||||
<Item xs={4}>
|
||||
|
|
@ -126,7 +126,7 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
|||
</span>
|
||||
</Item>
|
||||
<Item xs={2}>
|
||||
<Label>Packet Loss</Label>
|
||||
<Label>Packet loss</Label>
|
||||
<span>
|
||||
{machine.packetLoss
|
||||
? new BigNumber(machine.packetLoss).toFixed(3).toString() +
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const MachineStatus = () => {
|
|||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Machine Name',
|
||||
header: 'Machine name',
|
||||
width: 250,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
|
|
@ -111,7 +111,7 @@ const MachineStatus = () => {
|
|||
: 'unknown'
|
||||
},
|
||||
{
|
||||
header: 'Software Version',
|
||||
header: 'Software version',
|
||||
width: 200,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
|
|
@ -134,7 +134,7 @@ const MachineStatus = () => {
|
|||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>Machine Status</Title>
|
||||
<Title>Machine status</Title>
|
||||
</div>
|
||||
<div className={classes.headerLabels}>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import React from 'react'
|
|||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Stepper from 'src/components/Stepper'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Cashbox } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
||||
|
|
@ -245,12 +245,12 @@ const WizardStep = ({
|
|||
classes.centerAlignment
|
||||
)}>
|
||||
<P>Since previous update</P>
|
||||
<HoverableTooltip width={215}>
|
||||
<HelpTooltip width={215}>
|
||||
<P>
|
||||
Number of bills inside the cash box, since the last
|
||||
cash box changes.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { P } from 'src/components/typography'
|
||||
import FormRenderer from 'src/pages/Services/FormRenderer'
|
||||
|
|
@ -159,7 +161,24 @@ const Notifications = ({
|
|||
!loading && (
|
||||
<>
|
||||
<NotificationsCtx.Provider value={contextValue}>
|
||||
{displayTitle && <TitleSection title="Notifications" />}
|
||||
{displayTitle && (
|
||||
<TitleSection
|
||||
title="Notifications"
|
||||
appendix={
|
||||
<HelpTooltip width={250}>
|
||||
<P>
|
||||
For details on configuring notifications, please read the
|
||||
relevant knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001210592-Enabling-notifications"
|
||||
label="Enabling notifications"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{displayThirdPartyProvider && (
|
||||
<Section
|
||||
title="Third party providers"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const CryptoBalanceAlerts = ({ section, fieldWidth }) => {
|
|||
section={section}
|
||||
decoration={currency}
|
||||
className={classes.cryptoBalanceAlertsForm}
|
||||
title="Default (Low Balance)"
|
||||
title="Default (Low balance)"
|
||||
label="Alert me under"
|
||||
editing={isEditing(LOW_BALANCE_KEY)}
|
||||
disabled={isDisabled(LOW_BALANCE_KEY)}
|
||||
|
|
@ -49,7 +49,7 @@ const CryptoBalanceAlerts = ({ section, fieldWidth }) => {
|
|||
save={save}
|
||||
decoration={currency}
|
||||
className={classes.cryptoBalanceAlertsSecondForm}
|
||||
title="Default (High Balance)"
|
||||
title="Default (High balance)"
|
||||
label="Alert me over"
|
||||
editing={isEditing(HIGH_BALANCE_KEY)}
|
||||
disabled={isDisabled(HIGH_BALANCE_KEY)}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
.nullable()
|
||||
.required(),
|
||||
[LOW_BALANCE_KEY]: Yup.number()
|
||||
.label('Low Balance')
|
||||
.label('Low balance')
|
||||
.when(HIGH_BALANCE_KEY, {
|
||||
is: HIGH_BALANCE_KEY => !HIGH_BALANCE_KEY,
|
||||
then: Yup.number().required()
|
||||
|
|
@ -73,7 +73,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
.max(CURRENCY_MAX)
|
||||
.nullable(),
|
||||
[HIGH_BALANCE_KEY]: Yup.number()
|
||||
.label('High Balance')
|
||||
.label('High balance')
|
||||
.when(LOW_BALANCE_KEY, {
|
||||
is: LOW_BALANCE_KEY => !LOW_BALANCE_KEY,
|
||||
then: Yup.number().required()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from 'src/components/fake-table/Table'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
import { startCase } from 'src/utils/string'
|
||||
import { sentenceCase } from 'src/utils/string'
|
||||
|
||||
import NotificationsCtx from '../NotificationsContext'
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ const Row = ({
|
|||
return (
|
||||
<Tr>
|
||||
<Td width={channelSize}>
|
||||
{shouldUpperCase ? R.toUpper(namespace) : startCase(namespace)}
|
||||
{shouldUpperCase ? R.toUpper(namespace) : sentenceCase(namespace)}
|
||||
</Td>
|
||||
<Cell name="balance" disabled={disabled} />
|
||||
<Cell name="transactions" disabled={disabled} />
|
||||
|
|
@ -127,7 +127,7 @@ const Setup = ({ wizard, forceDisable }) => {
|
|||
<Th width={channelSize - widthAdjust}>Channel</Th>
|
||||
{Object.keys(sizes).map(it => (
|
||||
<Th key={it} width={sizes[it] - widthAdjust} textAlign="center">
|
||||
{startCase(it)}
|
||||
{sentenceCase(it)}
|
||||
</Th>
|
||||
))}
|
||||
</THead>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import gql from 'graphql-tag'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { H4, P, Label2 } from 'src/components/typography'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import { SupportLinkButton } from '../../components/buttons'
|
||||
|
||||
import { global } from './OperatorInfo.styles'
|
||||
|
||||
const useStyles = makeStyles(global)
|
||||
|
|
@ -66,7 +68,7 @@ const CoinATMRadar = memo(({ wizard }) => {
|
|||
<div>
|
||||
<div className={classes.header}>
|
||||
<H4>Coin ATM Radar share settings</H4>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article{' '}
|
||||
|
|
@ -78,7 +80,12 @@ const CoinATMRadar = memo(({ wizard }) => {
|
|||
</a>
|
||||
.
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360023720472-Coin-ATM-Radar"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<Row
|
||||
title={'Share information?'}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import * as Yup from 'yup'
|
|||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Link, IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||
import Switch from 'src/components/inputs/base/Switch'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { P, H4, Info3, Label1, Label2, Label3 } from 'src/components/typography'
|
||||
|
|
@ -136,7 +137,7 @@ const ContactInfo = ({ wizard }) => {
|
|||
const fields = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Full name',
|
||||
label: 'Company name',
|
||||
value: info.name ?? '',
|
||||
component: TextInput
|
||||
},
|
||||
|
|
@ -160,7 +161,7 @@ const ContactInfo = ({ wizard }) => {
|
|||
},
|
||||
{
|
||||
name: 'companyNumber',
|
||||
label: 'Company number',
|
||||
label: 'Company registration number',
|
||||
value: info.companyNumber ?? '',
|
||||
component: TextInput
|
||||
}
|
||||
|
|
@ -189,6 +190,17 @@ const ContactInfo = ({ wizard }) => {
|
|||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>Contact information</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360033051732-Enabling-Operator-Info"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div className={classes.switchRow}>
|
||||
<P>Info card enabled?</P>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import gql from 'graphql-tag'
|
|||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { BooleanPropertiesTable } from 'src/components/booleanPropertiesTable'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { H4, P, Label2 } from 'src/components/typography'
|
||||
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||
|
||||
import { SupportLinkButton } from '../../components/buttons'
|
||||
|
||||
import { global } from './OperatorInfo.styles'
|
||||
|
||||
const useStyles = makeStyles(global)
|
||||
|
|
@ -47,6 +50,17 @@ const ReceiptPrinting = memo(({ wizard }) => {
|
|||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>Receipt options</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360058513951-Receipt-options-printers"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div className={classes.switchRow}>
|
||||
<P>Enable receipt printing</P>
|
||||
|
|
@ -109,7 +123,7 @@ const ReceiptPrinting = memo(({ wizard }) => {
|
|||
},
|
||||
{
|
||||
name: 'companyNumber',
|
||||
display: 'Company number'
|
||||
display: 'Company registration number'
|
||||
},
|
||||
{
|
||||
name: 'machineLocation',
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import gql from 'graphql-tag'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, P, Label3 } from 'src/components/typography'
|
||||
|
|
@ -162,9 +162,9 @@ const SMSNotices = () => {
|
|||
!R.isEmpty(TOOLTIPS[it.event]) ? (
|
||||
<div className={classes.messageWithTooltip}>
|
||||
{R.prop('messageName', it)}
|
||||
<HoverableTooltip width={250}>
|
||||
<HelpTooltip width={250}>
|
||||
<P>{TOOLTIPS[it.event]}</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
) : (
|
||||
R.prop('messageName', it)
|
||||
|
|
@ -237,6 +237,17 @@ const SMSNotices = () => {
|
|||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>SMS notices</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001205591-SMS-Phone-Verification"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
{showModal && (
|
||||
<CustomSMSModal
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import * as Yup from 'yup'
|
|||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Link, IconButton, SupportLinkButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { H4, Info2, Info3, Label2, Label3, P } from 'src/components/typography'
|
||||
|
|
@ -171,6 +172,17 @@ const TermsConditions = () => {
|
|||
<>
|
||||
<div className={classes.header}>
|
||||
<H4>Terms & Conditions</H4>
|
||||
<HelpTooltip width={320}>
|
||||
<P>
|
||||
For details on configuring this panel, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360015982211-Terms-and-Conditions"
|
||||
label="Lamassu Support Article"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div className={classes.switchRow}>
|
||||
<P>Show on screen</P>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ const Services = () => {
|
|||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<TitleSection title="3rd Party Services" />
|
||||
<TitleSection title="Third-Party services" />
|
||||
<Grid container spacing={4}>
|
||||
{R.values(schemas).map(schema => (
|
||||
<Grid item key={schema.code}>
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private Key',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private Key',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'token',
|
||||
display: 'API Token',
|
||||
display: 'API token',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
|
|
@ -47,52 +47,52 @@ export default {
|
|||
},
|
||||
{
|
||||
code: 'BTCWalletId',
|
||||
display: 'BTC Wallet ID',
|
||||
display: 'BTC wallet ID',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'BTCWalletPassphrase',
|
||||
display: 'BTC Wallet Passphrase',
|
||||
display: 'BTC wallet passphrase',
|
||||
component: SecretInput
|
||||
},
|
||||
{
|
||||
code: 'LTCWalletId',
|
||||
display: 'LTC Wallet ID',
|
||||
display: 'LTC wallet ID',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'LTCWalletPassphrase',
|
||||
display: 'LTC Wallet Passphrase',
|
||||
display: 'LTC wallet passphrase',
|
||||
component: SecretInput
|
||||
},
|
||||
{
|
||||
code: 'ZECWalletId',
|
||||
display: 'ZEC Wallet ID',
|
||||
display: 'ZEC wallet ID',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'ZECWalletPassphrase',
|
||||
display: 'ZEC Wallet Passphrase',
|
||||
display: 'ZEC wallet passphrase',
|
||||
component: SecretInput
|
||||
},
|
||||
{
|
||||
code: 'BCHWalletId',
|
||||
display: 'BCH Wallet ID',
|
||||
display: 'BCH wallet ID',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'BCHWalletPassphrase',
|
||||
display: 'BCH Wallet Passphrase',
|
||||
display: 'BCH wallet passphrase',
|
||||
component: SecretInput
|
||||
},
|
||||
{
|
||||
code: 'DASHWalletId',
|
||||
display: 'DASH Wallet ID',
|
||||
display: 'DASH wallet ID',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'DASHWalletPassphrase',
|
||||
display: 'DASH Wallet Passphrase',
|
||||
display: 'DASH wallet passphrase',
|
||||
component: SecretInput
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ export default {
|
|||
},
|
||||
{
|
||||
code: 'key',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'secret',
|
||||
display: 'API Secret',
|
||||
display: 'API secret',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'token',
|
||||
display: 'API Token',
|
||||
display: 'API token',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'confidenceFactor',
|
||||
display: 'Confidence Factor',
|
||||
display: 'Confidence factor',
|
||||
component: NumberInput,
|
||||
face: true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,14 +12,21 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'uid',
|
||||
display: 'User ID',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private Key',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
@ -28,6 +35,9 @@ export default {
|
|||
apiKey: Yup.string('The API key must be a string')
|
||||
.max(100, 'The API key is too long')
|
||||
.required('The API key is required'),
|
||||
uid: Yup.string('The User ID must be a string')
|
||||
.max(100, 'The User ID is too long')
|
||||
.required('The User ID is required'),
|
||||
privateKey: Yup.string('The private key must be a string')
|
||||
.max(100, 'The private key is too long')
|
||||
.test(secretTest(account?.privateKey, 'private key'))
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ export default {
|
|||
},
|
||||
{
|
||||
code: 'clientKey',
|
||||
display: 'Client Key',
|
||||
display: 'Client key',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
code: 'clientSecret',
|
||||
display: 'Client Secret',
|
||||
display: 'Client secret',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private Key',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default {
|
|||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
|
|
@ -19,13 +19,13 @@ export default {
|
|||
},
|
||||
{
|
||||
code: 'fromEmail',
|
||||
display: 'From Email',
|
||||
display: 'From email',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
},
|
||||
{
|
||||
code: 'toEmail',
|
||||
display: 'To Email',
|
||||
display: 'To email',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const singleBitgo = code => ({
|
|||
elements: [
|
||||
{
|
||||
code: 'token',
|
||||
display: 'API Token',
|
||||
display: 'API token',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
|
|
@ -34,12 +34,12 @@ const singleBitgo = code => ({
|
|||
},
|
||||
{
|
||||
code: `${code}WalletId`,
|
||||
display: `${code} Wallet ID`,
|
||||
display: `${code} wallet ID`,
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: `${code}WalletPassphrase`,
|
||||
display: `${code} Wallet Passphrase`,
|
||||
display: `${code} wallet passphrase`,
|
||||
component: SecretInput
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -17,18 +17,18 @@ export default {
|
|||
},
|
||||
{
|
||||
code: 'authToken',
|
||||
display: 'Auth Token',
|
||||
display: 'Auth token',
|
||||
component: SecretInputFormik
|
||||
},
|
||||
{
|
||||
code: 'fromNumber',
|
||||
display: 'Twilio Number (international format)',
|
||||
display: 'Twilio number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
},
|
||||
{
|
||||
code: 'toNumber',
|
||||
display: 'Notifications Number (international format)',
|
||||
display: 'Notifications number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const SessionManagement = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Session Management" />
|
||||
<TitleSection title="Session management" />
|
||||
<DataTable
|
||||
loading={loading}
|
||||
elements={elements}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const CopyToClipboard = ({
|
|||
buttonClassname,
|
||||
children,
|
||||
wrapperClassname,
|
||||
removeSpace = true,
|
||||
...props
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
|
@ -46,7 +47,8 @@ const CopyToClipboard = ({
|
|||
{children}
|
||||
</div>
|
||||
<div className={classnames(classes.buttonWrapper, buttonClassname)}>
|
||||
<ReactCopyToClipboard text={R.replace(/\s/g, '')(children)}>
|
||||
<ReactCopyToClipboard
|
||||
text={removeSpace ? R.replace(/\s/g, '')(children) : children}>
|
||||
<button
|
||||
aria-describedby={id}
|
||||
onClick={event => handleClick(event)}>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import * as R from 'ramda'
|
|||
import React, { memo, useState } from 'react'
|
||||
|
||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { IDButton, ActionButton } from 'src/components/buttons'
|
||||
import { P, Label1 } from 'src/components/typography'
|
||||
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
|
||||
|
|
@ -134,9 +134,9 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
const commissionPercentage = BigNumber(
|
||||
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
||||
).toFixed(2, 1) // ROUND_DOWN
|
||||
const cashInFee = isCashIn ? Number.parseFloat(tx.cashInFee) : 0
|
||||
const fixedFee = Number.parseFloat(tx.fixedFee) || 0
|
||||
const fiat = BigNumber(tx.fiat)
|
||||
.minus(cashInFee)
|
||||
.minus(fixedFee)
|
||||
.toFixed(2, 1) // ROUND_DOWN
|
||||
const crypto = getCryptoAmount(tx)
|
||||
const cryptoFee = tx.fee ? `${getCryptoFeeAmount(tx)} ${tx.fiatCode}` : 'N/A'
|
||||
|
|
@ -192,6 +192,13 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
<>
|
||||
<Label>Transaction status</Label>
|
||||
<span className={classes.bold}>{getStatus(tx)}</span>
|
||||
{getStatusDetails(tx) ? (
|
||||
<CopyToClipboard removeSpace={false} className={classes.errorCopy}>
|
||||
{getStatusDetails(tx)}
|
||||
</CopyToClipboard>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
|
|
@ -351,7 +358,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
</div>
|
||||
<div>
|
||||
<Label>Fixed fee</Label>
|
||||
<div>{isCashIn ? `${cashInFee} ${tx.fiatCode}` : 'N/A'}</div>
|
||||
<div>{`${fixedFee} ${tx.fiatCode}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.secondRow}>
|
||||
|
|
@ -359,9 +366,9 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
<div className={classes.addressHeader}>
|
||||
<Label>Address</Label>
|
||||
{!R.isNil(tx.walletScore) && (
|
||||
<HoverableTooltip parentElements={walletScoreEl}>
|
||||
<HelpTooltip parentElements={walletScoreEl}>
|
||||
{`Chain analysis score: ${tx.walletScore}/10`}
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -393,13 +400,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
</div>
|
||||
<div className={classes.lastRow}>
|
||||
<div className={classes.status}>
|
||||
{getStatusDetails(tx) ? (
|
||||
<HoverableTooltip parentElements={errorElements} width={200}>
|
||||
<P>{getStatusDetails(tx)}</P>
|
||||
</HoverableTooltip>
|
||||
) : (
|
||||
errorElements
|
||||
)}
|
||||
{errorElements}
|
||||
{((tx.txClass === 'cashOut' && getStatus(tx) === 'Pending') ||
|
||||
(tx.txClass === 'cashIn' && getStatus(tx) === 'Batched')) && (
|
||||
<ActionButton
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { offColor, comet, white, tomato } from 'src/styling/variables'
|
||||
|
||||
const { p } = typographyStyles
|
||||
const { p, label3 } = typographyStyles
|
||||
|
||||
export default {
|
||||
wrapper: {
|
||||
|
|
@ -137,5 +137,10 @@ export default {
|
|||
},
|
||||
swept: {
|
||||
width: 250
|
||||
},
|
||||
errorCopy: {
|
||||
extend: label3,
|
||||
lineBreak: 'normal',
|
||||
maxWidth: 180
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
|||
import SearchBox from 'src/components/SearchBox'
|
||||
import SearchFilter from 'src/components/SearchFilter'
|
||||
import Title from 'src/components/Title'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
|
|
@ -105,7 +107,7 @@ const GET_TRANSACTIONS = gql`
|
|||
deviceId
|
||||
fiat
|
||||
fee
|
||||
cashInFee
|
||||
fixedFee
|
||||
fiatCode
|
||||
cryptoAtoms
|
||||
cryptoCode
|
||||
|
|
@ -233,7 +235,22 @@ const Transactions = () => {
|
|||
},
|
||||
{
|
||||
header: 'Status',
|
||||
view: it => getStatus(it),
|
||||
view: it => {
|
||||
if (getStatus(it) === 'Pending')
|
||||
return (
|
||||
<div className={classes.pendingBox}>
|
||||
{'Pending'}
|
||||
<HelpTooltip width={285}>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/115001210452-Cancelling-cash-out-transactions"
|
||||
label="Cancelling cash-out transactions"
|
||||
bottomSpace="0"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
)
|
||||
else return getStatus(it)
|
||||
},
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
width: 80
|
||||
|
|
@ -323,7 +340,7 @@ const Transactions = () => {
|
|||
loading={filtersLoading}
|
||||
filters={filters}
|
||||
options={filterOptions}
|
||||
inputPlaceholder={'Search Transactions'}
|
||||
inputPlaceholder={'Search transactions'}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Link, SupportLinkButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
|
@ -187,13 +187,13 @@ const Triggers = () => {
|
|||
<Label2 className={classes.switchLabel}>
|
||||
{rejectAddressReuse ? 'On' : 'Off'}
|
||||
</Label2>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
This option requires a user to scan a different cryptocurrency
|
||||
address if they attempt to scan one that had been previously
|
||||
used for a transaction in your network
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ const Users = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="User Management" />
|
||||
<TitleSection title="User management" />
|
||||
<Box
|
||||
marginBottom={3}
|
||||
marginTop={-5}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { SupportLinkButton } from 'src/components/buttons'
|
||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import FormRenderer from 'src/pages/Services/FormRenderer'
|
||||
|
|
@ -13,6 +15,8 @@ import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle
|
|||
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
|
||||
import { P } from '../../components/typography'
|
||||
|
||||
import AdvancedWallet from './AdvancedWallet'
|
||||
import styles from './Wallet.styles.js'
|
||||
import Wizard from './Wizard'
|
||||
|
|
@ -115,7 +119,7 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
<>
|
||||
<div className={classes.header}>
|
||||
<TitleSection
|
||||
title="Wallet Settings"
|
||||
title="Wallet settings"
|
||||
buttons={[
|
||||
{
|
||||
text: 'Advanced settings',
|
||||
|
|
@ -124,6 +128,19 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
toggle: setAdvancedSettings
|
||||
}
|
||||
]}
|
||||
appendix={
|
||||
<HelpTooltip width={340}>
|
||||
<P>
|
||||
For details on configuring wallets, please read the relevant
|
||||
knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360000725832-Wallets-Exchange-Linkage-and-Volatility"
|
||||
label="Wallets, Exchange Linkage, and Volatility"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!advancedSettings && (
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ const getAdvancedWalletElements = () => {
|
|||
},
|
||||
{
|
||||
name: 'allowTransactionBatching',
|
||||
header: `Allow BTC Transaction Batching`,
|
||||
header: `Allow BTC transaction batching`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 260,
|
||||
|
|
@ -119,7 +119,7 @@ const getAdvancedWalletElements = () => {
|
|||
},
|
||||
{
|
||||
name: 'feeMultiplier',
|
||||
header: `BTC Miner's Fee`,
|
||||
header: `BTC miner's fee`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 250,
|
||||
|
|
@ -179,7 +179,7 @@ const getAdvancedWalletElementsOverrides = (
|
|||
},
|
||||
{
|
||||
name: 'feeMultiplier',
|
||||
header: `Miner's Fee`,
|
||||
header: `Miner's fee`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 250,
|
||||
|
|
@ -280,7 +280,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
|||
},
|
||||
{
|
||||
name: 'zeroConf',
|
||||
header: 'Confidence Checking',
|
||||
header: 'Confidence checking',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
view: (it, row) => {
|
||||
|
|
@ -304,7 +304,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
|||
},
|
||||
{
|
||||
name: 'zeroConfLimit',
|
||||
header: '0-conf Limit',
|
||||
header: '0-conf limit',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
view: (it, row) =>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import gql from 'graphql-tag'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
import InfoMessage from 'src/components/InfoMessage'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Button, SupportLinkButton } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import { H1, H4, P } from 'src/components/typography'
|
||||
|
|
@ -102,7 +102,7 @@ function Twilio({ doContinue }) {
|
|||
<H4 noMargin className={classnames(titleClasses)}>
|
||||
Will you setup a two way machine or compliance?
|
||||
</H4>
|
||||
<HoverableTooltip width={304}>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
Two-way machines allow your customers not only to buy (cash-in)
|
||||
but also sell cryptocurrencies (cash-out).
|
||||
|
|
@ -111,7 +111,7 @@ function Twilio({ doContinue }) {
|
|||
You’ll need an SMS service for cash-out transactions and for any
|
||||
compliance triggers
|
||||
</P>
|
||||
</HoverableTooltip>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
|
||||
<RadioGroup
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const getLamassuRoutes = () => [
|
|||
children: [
|
||||
{
|
||||
key: 'cash_units',
|
||||
label: 'Cash Units',
|
||||
label: 'Cash units',
|
||||
route: '/maintenance/cash-units',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CashUnits
|
||||
|
|
@ -63,14 +63,14 @@ const getLamassuRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Machine Logs',
|
||||
label: 'Machine logs',
|
||||
route: '/maintenance/logs',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineLogs
|
||||
},
|
||||
{
|
||||
key: 'machine-status',
|
||||
label: 'Machine Status',
|
||||
label: 'Machine status',
|
||||
route: '/maintenance/machine-status',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineStatus
|
||||
|
|
@ -130,7 +130,7 @@ const getLamassuRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: 'services',
|
||||
label: '3rd Party Services',
|
||||
label: 'Third-party services',
|
||||
route: '/settings/3rd-party-services',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Services
|
||||
|
|
@ -144,9 +144,9 @@ const getLamassuRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: namespaces.OPERATOR_INFO,
|
||||
label: 'Operator Info',
|
||||
label: 'Operator info',
|
||||
route: '/settings/operator-info',
|
||||
title: 'Operator Information',
|
||||
title: 'Operator information',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => (
|
||||
|
|
@ -232,7 +232,7 @@ const getLamassuRoutes = () => [
|
|||
key: 'loyalty',
|
||||
label: 'Loyalty',
|
||||
route: '/compliance/loyalty',
|
||||
title: 'Loyalty Panel',
|
||||
title: 'Loyalty panel',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => (
|
||||
|
|
@ -247,14 +247,14 @@ const getLamassuRoutes = () => [
|
|||
children: [
|
||||
{
|
||||
key: 'individual-discounts',
|
||||
label: 'Individual Discounts',
|
||||
label: 'Individual discounts',
|
||||
route: '/compliance/loyalty/individual-discounts',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: IndividualDiscounts
|
||||
},
|
||||
{
|
||||
key: 'promo-codes',
|
||||
label: 'Promo Codes',
|
||||
label: 'Promo codes',
|
||||
route: '/compliance/loyalty/codes',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: PromoCodes
|
||||
|
|
@ -280,14 +280,14 @@ const getLamassuRoutes = () => [
|
|||
children: [
|
||||
{
|
||||
key: 'user-management',
|
||||
label: 'User Management',
|
||||
label: 'User management',
|
||||
route: '/system/user-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: UserManagement
|
||||
},
|
||||
{
|
||||
key: 'session-management',
|
||||
label: 'Session Management',
|
||||
label: 'Session management',
|
||||
route: '/system/session-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: SessionManagement
|
||||
|
|
|
|||
|
|
@ -56,14 +56,14 @@ const getPazuzRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Machine Logs',
|
||||
label: 'Machine logs',
|
||||
route: '/maintenance/logs',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineLogs
|
||||
},
|
||||
{
|
||||
key: 'machine-status',
|
||||
label: 'Machine Status',
|
||||
label: 'Machine status',
|
||||
route: '/maintenance/machine-status',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineStatus
|
||||
|
|
@ -123,9 +123,9 @@ const getPazuzRoutes = () => [
|
|||
},
|
||||
{
|
||||
key: namespaces.OPERATOR_INFO,
|
||||
label: 'Operator Info',
|
||||
label: 'Operator info',
|
||||
route: '/settings/operator-info',
|
||||
title: 'Operator Information',
|
||||
title: 'Operator information',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => (
|
||||
|
|
@ -225,14 +225,14 @@ const getPazuzRoutes = () => [
|
|||
children: [
|
||||
{
|
||||
key: 'individual-discounts',
|
||||
label: 'Individual Discounts',
|
||||
label: 'Individual discounts',
|
||||
route: '/compliance/loyalty/individual-discounts',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: IndividualDiscounts
|
||||
},
|
||||
{
|
||||
key: 'promo-codes',
|
||||
label: 'Promo Codes',
|
||||
label: 'Promo codes',
|
||||
route: '/compliance/loyalty/codes',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: PromoCodes
|
||||
|
|
@ -290,14 +290,14 @@ const getPazuzRoutes = () => [
|
|||
children: [
|
||||
{
|
||||
key: 'user-management',
|
||||
label: 'User Management',
|
||||
label: 'User management',
|
||||
route: '/system/user-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: UserManagement
|
||||
},
|
||||
{
|
||||
key: 'session-management',
|
||||
label: 'Session Management',
|
||||
label: 'Session management',
|
||||
route: '/system/session-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: SessionManagement
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ export default {
|
|||
// forcing styling onto inner container
|
||||
'.ReactVirtualized__Grid__innerScrollContainer': {
|
||||
overflow: 'inherit !important'
|
||||
},
|
||||
'.ReactVirtualized__Grid.ReactVirtualized__List': {
|
||||
overflowY: 'overlay !important'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,15 @@ const startCase = R.compose(
|
|||
splitOnUpper
|
||||
)
|
||||
|
||||
const sentenceCase = R.compose(onlyFirstToUpper, S.joinWith(' '), splitOnUpper)
|
||||
|
||||
const singularOrPlural = (amount, singularStr, pluralStr) =>
|
||||
parseInt(amount) === 1 ? singularStr : pluralStr
|
||||
|
||||
export { startCase, onlyFirstToUpper, formatLong, singularOrPlural }
|
||||
export {
|
||||
startCase,
|
||||
onlyFirstToUpper,
|
||||
formatLong,
|
||||
singularOrPlural,
|
||||
sentenceCase
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue