Merge branch 'dev' into fix/machine_upairing

This commit is contained in:
Nikola Ubavić 2021-12-24 23:49:55 +01:00 committed by GitHub
commit 29dc519a52
126 changed files with 2007 additions and 1281 deletions

View file

@ -45,6 +45,7 @@ keypool=10000
prune=4000 prune=4000
daemon=0 daemon=0
addresstype=p2sh-segwit addresstype=p2sh-segwit
changetype=bech32
walletrbf=1 walletrbf=1
bind=0.0.0.0:8332 bind=0.0.0.0:8332
rpcport=8333` rpcport=8333`

View file

@ -29,8 +29,8 @@ const BINARIES = {
dir: 'bitcoin-22.0/bin' dir: 'bitcoin-22.0/bin'
}, },
ETH: { ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.12-6c4dc6c3.tar.gz', url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.13-7a0c19f8.tar.gz',
dir: 'geth-linux-amd64-1.10.12-6c4dc6c3' dir: 'geth-linux-amd64-1.10.13-7a0c19f8'
}, },
ZEC: { ZEC: {
url: 'https://z.cash/downloads/zcash-4.5.1-1-linux64-debian-stretch.tar.gz', url: 'https://z.cash/downloads/zcash-4.5.1-1-linux64-debian-stretch.tar.gz',
@ -45,8 +45,8 @@ const BINARIES = {
dir: 'litecoin-0.18.1/bin' dir: 'litecoin-0.18.1/bin'
}, },
BCH: { BCH: {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v23.1.0/bitcoin-cash-node-23.1.0-x86_64-linux-gnu.tar.gz', url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v24.0.0/bitcoin-cash-node-24.0.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-cash-node-23.1.0/bin', dir: 'bitcoin-cash-node-24.0.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']] files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
}, },
XMR: { XMR: {

View file

@ -44,5 +44,6 @@ connections=40
keypool=10000 keypool=10000
prune=4000 prune=4000
daemon=0 daemon=0
addresstype=p2sh-segwit` addresstype=p2sh-segwit
changetype=bech32`
} }

View file

@ -4,8 +4,8 @@ const _ = require('lodash/fp')
const uuid = require('uuid') const uuid = require('uuid')
function createCashboxBatch (deviceId, cashboxCount) { function createCashboxBatch (deviceId, cashboxCount) {
if (_.isEqual(0, cashboxCount)) throw new Error('Cashbox is empty. Cashbox batch could not be created.') if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
const sql = `INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-in-empty') RETURNING *` const sql = `INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
const sql2 = ` const sql2 = `
UPDATE bills SET cashbox_batch_id=$1 UPDATE bills SET cashbox_batch_id=$1
FROM cash_in_txs FROM cash_in_txs
@ -24,12 +24,12 @@ function updateMachineWithBatch (machineContext, oldCashboxCount) {
const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(machineContext.cassettes)) const isCassetteAmountWithinRange = _.inRange(constants.CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, constants.CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES + 1, _.size(machineContext.cassettes))
if (!isValidContext && !isCassetteAmountWithinRange) if (!isValidContext && !isCassetteAmountWithinRange)
throw new Error('Insufficient info to create a new cashbox batch') throw new Error('Insufficient info to create a new cashbox batch')
if (_.isEqual(0, oldCashboxCount)) throw new Error('Cashbox is empty. Cashbox batch could not be created.') if (_.isEqual(0, oldCashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
return db.tx(t => { return db.tx(t => {
const deviceId = machineContext.deviceId const deviceId = machineContext.deviceId
const batchId = uuid.v4() const batchId = uuid.v4()
const q1 = t.none(`INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-in-empty')`, [batchId, deviceId]) const q1 = t.none(`INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`, [batchId, deviceId])
const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs const q2 = t.none(`UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs
WHERE bills.cash_in_txs_id = cash_in_txs.id AND WHERE bills.cash_in_txs_id = cash_in_txs.id AND
cash_in_txs.device_id = $2 AND cash_in_txs.device_id = $2 AND
@ -68,4 +68,10 @@ function getBillsByBatchId (id) {
return db.any(sql, [id]) return db.any(sql, [id])
} }
module.exports = { createCashboxBatch, updateMachineWithBatch, getBatches, getBillsByBatchId, editBatchById } module.exports = {
createCashboxBatch,
updateMachineWithBatch,
getBatches,
getBillsByBatchId,
editBatchById
}

View file

@ -683,18 +683,18 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
*/ */
function getCustomerById (id) { function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',') const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override, const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration, phone, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at, id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat, sanctions_override, total_txs, total_spent, created AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
FROM ( FROM (
SELECT c.id, c.authorized_override, SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended, greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended, c.suspended_until > now() AS is_suspended,
c.front_camera_path, c.front_camera_override, c.front_camera_path, c.front_camera_override, c.front_camera_at,
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration, c.phone, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes, c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created, cn.notes,
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn, row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs, sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (PARTITION BY c.id) AS total_txs,

View file

@ -66,7 +66,7 @@ function addName (pings, events, config) {
const statuses = [ const statuses = [
getStatus( getStatus(
_.first(pings[machine.deviceId]), _.first(pings[machine.deviceId]),
_.first(checkStuckScreen(events, machine.name)) _.first(checkStuckScreen(events, machine))
) )
] ]

View file

@ -2,7 +2,7 @@ const bills = require('../../services/bills')
const resolvers = { const resolvers = {
Query: { Query: {
bills: () => bills.getBills() bills: (...[, { filters }]) => bills.getBills(filters)
} }
} }

View file

@ -2,14 +2,15 @@ const { gql } = require('apollo-server-express')
const typeDef = gql` const typeDef = gql`
type Bill { type Bill {
id: ID
fiat: Int fiat: Int
deviceId: ID deviceId: ID
created: Date created: Date
cashbox: Int cashboxBatchId: ID
} }
type Query { type Query {
bills: [Bill] @auth bills(filters: JSONObject): [Bill] @auth
} }
` `

View file

@ -1,23 +1,27 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('../../db') const db = require('../../db')
// Get all bills with device id const getBills = filters => {
const getBills = () => { const deviceStatement = !_.isNil(filters.deviceId) ? `WHERE device_id = ${pgp.as.text(filters.deviceId)}` : ``
return Promise.reject(new Error('This functionality hasn\'t been implemented yet')) const batchStatement = filter => {
/* return db.any(` switch (filter) {
SELECT d.device_id, b.fiat, b.created, d.cashbox case 'none':
FROM cash_in_txs return `WHERE b.cashbox_batch_id IS NULL`
INNER JOIN bills AS b ON b.cash_in_txs_id = cash_in_txs.id case 'any':
INNER JOIN devices as d ON d.device_id = cash_in_txs.device_id return `WHERE b.cashbox_batch_id IS NOT NULL`
ORDER BY device_id, created DESC` default:
) return _.isNil(filter) ? `` : `WHERE b.cashbox_batch_id = ${pgp.as.text(filter)}`
.then(res => { }
return res.map(item => ({ }
fiat: item.fiat,
deviceId: item.device_id, const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
cashbox: item.cashbox, SELECT id, device_id FROM cash_in_txs ${deviceStatement}
created: item.created ) AS cit ON cit.id = b.cash_in_txs_id ${batchStatement(filters.batch)}`
}))
}) */ return db.any(sql)
.then(res => _.map(_.mapKeys(_.camelCase), res))
} }
module.exports = { module.exports = {

View file

@ -56,7 +56,7 @@ function batch (
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired ((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id LEFT OUTER JOIN customers c ON txs.customer_id = c.id
INNER JOIN devices d ON txs.device_id = d.device_id LEFT JOIN devices d ON txs.device_id = d.device_id
WHERE txs.created >= $2 AND txs.created <= $3 ${ WHERE txs.created >= $2 AND txs.created <= $3 ${
id !== null ? `AND txs.device_id = $6` : `` id !== null ? `AND txs.device_id = $6` : ``
} }
@ -87,7 +87,7 @@ function batch (
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress' AND actions.action = 'provisionAddress'
LEFT OUTER JOIN customers c ON txs.customer_id = c.id LEFT OUTER JOIN customers c ON txs.customer_id = c.id
INNER JOIN devices d ON txs.device_id = d.device_id LEFT JOIN devices d ON txs.device_id = d.device_id
WHERE txs.created >= $2 AND txs.created <= $3 ${ WHERE txs.created >= $2 AND txs.created <= $3 ${
id !== null ? `AND txs.device_id = $6` : `` id !== null ? `AND txs.device_id = $6` : ``
} }
@ -130,7 +130,10 @@ function simplifiedBatch (data) {
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString() const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
const getProfit = it => { const getProfit = it => {
const getCommissionFee = it => BN(it.commissionPercentage).times(BN(it.fiat)) const discountValue = _.isNil(it.discount) ? BN(100) : BN(100).minus(it.discount)
const discountPercentage = BN(discountValue).div(100)
const commissionPercentage = BN(it.commissionPercentage).times(discountPercentage)
const getCommissionFee = it => BN(commissionPercentage).times(BN(it.fiat))
if (!it.cashInFee) return getCommissionFee(it) if (!it.cashInFee) return getCommissionFee(it)
return getCommissionFee(it).plus(BN(it.cashInFee)) return getCommissionFee(it).plus(BN(it.cashInFee))
} }

View file

@ -85,12 +85,8 @@ function buildAlerts (pings, balances, events, devices) {
alerts.general = _.filter(r => !r.deviceId, balances) alerts.general = _.filter(r => !r.deviceId, balances)
_.forEach(device => { _.forEach(device => {
const deviceId = device.deviceId const deviceId = device.deviceId
const deviceName = device.name
const deviceEvents = events.filter(function (eventRow) {
return eventRow.device_id === deviceId
})
const ping = pings[deviceId] || [] const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents, deviceName) const stuckScreen = checkStuckScreen(events, device)
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter( alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
['deviceId', deviceId], ['deviceId', deviceId],
@ -98,7 +94,7 @@ function buildAlerts (pings, balances, events, devices) {
), alerts.devices) ), alerts.devices)
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.deviceNames[deviceId] = deviceName alerts.deviceNames[deviceId] = device.name
}, devices) }, devices)
return alerts return alerts
@ -110,12 +106,13 @@ function checkPings (devices) {
return _.zipObject(deviceIds)(pings) return _.zipObject(deviceIds)(pings)
} }
function checkStuckScreen (deviceEvents, machineName) { function checkStuckScreen (deviceEvents, machine) {
const sortedEvents = _.sortBy( const lastEvent = _.pipe(
utils.getDeviceTime, _.filter(e => e.device_id === machine.deviceId),
_.map(utils.parseEventNote, deviceEvents) _.sortBy(utils.getDeviceTime),
) _.map(utils.parseEventNote),
const lastEvent = _.last(sortedEvents) _.last
)(deviceEvents)
if (!lastEvent) return [] if (!lastEvent) return []
@ -125,6 +122,7 @@ function checkStuckScreen (deviceEvents, machineName) {
if (isIdle) return [] if (isIdle) return []
const age = Math.floor(lastEvent.age) const age = Math.floor(lastEvent.age)
const machineName = machine.name
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }] if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
return [] return []

View file

@ -18,27 +18,10 @@ module.exports.up = function (next) {
} }
loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION) loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
.then(async settings => { .then(settings => _.isEmpty(settings.config)
if (_.isEmpty(settings.config)) { ? next()
return { : migrateConfig(settings)
settings, )
machines: []
}
}
return {
settings,
machines: await machineLoader.getMachineNames(settings.config)
}
})
.then(({ settings, machines }) => {
if (_.isEmpty(settings.config)) {
return next()
}
const sql = machines
? machines.map(m => `update devices set name = '${m.name}' where device_id = '${m.deviceId}'`)
: []
return db.multi(sql, () => migrateConfig(settings))
})
.catch(err => { .catch(err => {
if (err.message === 'lamassu-server is not configured') { if (err.message === 'lamassu-server is not configured') {
return next() return next()

View file

@ -3,15 +3,15 @@ var db = require('./db')
exports.up = function (next) { exports.up = function (next) {
var sqls = [ var sqls = [
`CREATE TYPE cashbox_batch_type AS ENUM( `CREATE TYPE cashbox_batch_type AS ENUM(
'cash-in-empty', 'cash-box-empty',
'cash-out-1-refill', 'cash-cassette-1-refill',
'cash-out-1-empty', 'cash-cassette-1-empty',
'cash-out-2-refill', 'cash-cassette-2-refill',
'cash-out-2-empty', 'cash-cassette-2-empty',
'cash-out-3-refill', 'cash-cassette-3-refill',
'cash-out-3-empty', 'cash-cassette-3-empty',
'cash-out-4-refill', 'cash-cassette-4-refill',
'cash-out-4-empty' 'cash-cassette-4-empty'
)`, )`,
`ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`, `ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`,
`ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`, `ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`,

View file

@ -6889,11 +6889,14 @@
} }
}, },
"apollo-upload-client": { "apollo-upload-client": {
"version": "16.0.0", "version": "13.0.0",
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz", "resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz",
"integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==", "integrity": "sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==",
"requires": { "requires": {
"extract-files": "^11.0.0" "@babel/runtime": "^7.9.2",
"apollo-link": "^1.2.12",
"apollo-link-http-common": "^0.2.14",
"extract-files": "^8.0.0"
} }
}, },
"apollo-utilities": { "apollo-utilities": {
@ -12617,9 +12620,9 @@
} }
}, },
"extract-files": { "extract-files": {
"version": "11.0.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz",
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==" "integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ=="
}, },
"extsprintf": { "extsprintf": {
"version": "1.3.0", "version": "1.3.0",
@ -27096,6 +27099,11 @@
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
}, },
"ua-parser-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
"integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg=="
},
"unfetch": { "unfetch": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",

View file

@ -14,7 +14,7 @@
"apollo-link": "^1.2.14", "apollo-link": "^1.2.14",
"apollo-link-error": "^1.1.13", "apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17", "apollo-link-http": "^1.5.17",
"apollo-upload-client": "^16.0.0", "apollo-upload-client": "^13.0.0",
"axios": "0.21.1", "axios": "0.21.1",
"base-64": "^1.0.0", "base-64": "^1.0.0",
"bignumber.js": "9.0.0", "bignumber.js": "9.0.0",
@ -47,6 +47,7 @@
"react-use": "15.3.2", "react-use": "15.3.2",
"react-virtualized": "^9.21.2", "react-virtualized": "^9.21.2",
"sanctuary": "^2.0.1", "sanctuary": "^2.0.1",
"ua-parser-js": "^1.0.2",
"uuid": "^7.0.2", "uuid": "^7.0.2",
"yup": "0.32.9" "yup": "0.32.9"
}, },

View file

@ -153,7 +153,6 @@ const NotificationCenter = ({
{!loading && buildNotifications()} {!loading && buildNotifications()}
</div> </div>
</div> </div>
<div className={classes.background} />
</> </>
) )
} }

View file

@ -8,22 +8,16 @@ import {
} from 'src/styling/variables' } from 'src/styling/variables'
const styles = { const styles = {
background: { container: {
position: 'absolute', '@media only screen and (max-width: 1920px)': {
width: '100vw', width: '30vw'
height: '100vh', },
left: 0, width: '40vw',
top: 0, height: '110vh',
zIndex: -1, right: 0,
backgroundColor: white, backgroundColor: white,
boxShadow: '0 0 14px 0 rgba(0, 0, 0, 0.24)' boxShadow: '0 0 14px 0 rgba(0, 0, 0, 0.24)'
}, },
container: {
left: -200,
top: -42,
backgroundColor: white,
height: '110vh'
},
header: { header: {
display: 'flex', display: 'flex',
justifyContent: 'space-between' justifyContent: 'space-between'
@ -39,7 +33,7 @@ const styles = {
}, },
notificationIcon: ({ buttonCoords, xOffset }) => ({ notificationIcon: ({ buttonCoords, xOffset }) => ({
position: 'absolute', position: 'absolute',
top: buttonCoords ? buttonCoords.y - 1 : 0, top: buttonCoords ? buttonCoords.y : 0,
left: buttonCoords ? buttonCoords.x - xOffset : 0, left: buttonCoords ? buttonCoords.x - xOffset : 0,
cursor: 'pointer', cursor: 'pointer',
background: 'transparent', background: 'transparent',
@ -54,21 +48,33 @@ const styles = {
backgroundColor: zircon backgroundColor: zircon
}, },
notificationsList: { notificationsList: {
width: 440,
height: '90vh', height: '90vh',
maxHeight: '100vh', maxHeight: '100vh',
marginTop: spacer * 3, marginTop: spacer * 3,
marginLeft: 0, marginLeft: 0,
marginRight: -50,
overflowY: 'auto', overflowY: 'auto',
overflowX: 'hidden', overflowX: 'hidden',
backgroundColor: white, backgroundColor: white,
zIndex: 10 zIndex: 10
}, },
notificationRow: { notificationRow: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
position: 'relative', position: 'relative',
marginBottom: spacer / 2, marginBottom: spacer / 2,
paddingTop: spacer * 1.5 paddingTop: spacer * 1.5,
'& > *': {
marginRight: 10
},
'& > *:last-child': {
marginRight: 0
}
},
notificationContent: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}, },
unread: { unread: {
backgroundColor: spring3 backgroundColor: spring3
@ -79,6 +85,9 @@ const styles = {
marginLeft: spacer * 3 marginLeft: spacer * 3
} }
}, },
readIconWrapper: {
flexGrow: 1
},
unreadIcon: { unreadIcon: {
marginLeft: spacer, marginLeft: spacer,
marginTop: 5, marginTop: 5,

View file

@ -1,4 +1,3 @@
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import prettyMs from 'pretty-ms' import prettyMs from 'pretty-ms'
@ -8,7 +7,6 @@ import React from 'react'
import { Label1, Label2, TL2 } from 'src/components/typography' import { Label1, Label2, TL2 } from 'src/components/typography'
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg' import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg' import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg'
import { ReactComponent as StripesSvg } from 'src/styling/icons/stripes.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg' import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
import styles from './NotificationCenter.styles' import styles from './NotificationCenter.styles'
@ -54,36 +52,26 @@ const NotificationRow = ({
[classes.unreadIcon]: !read [classes.unreadIcon]: !read
} }
return ( return (
<Grid <div
container
className={classnames( className={classnames(
classes.notificationRow, classes.notificationRow,
!read && valid ? classes.unread : '' !read && valid ? classes.unread : ''
)}> )}>
<Grid item xs={2} className={classes.notificationRowIcon}> <div className={classes.notificationRowIcon}>{icon}</div>
{icon} <div className={classes.notificationContent}>
</Grid>
<Grid item container xs={7} direction="row">
<Grid item xs={12}>
<Label2 className={classes.notificationTitle}> <Label2 className={classes.notificationTitle}>
{notificationTitle} {notificationTitle}
</Label2> </Label2>
</Grid>
<Grid item xs={12}>
<TL2 className={classes.notificationBody}>{message}</TL2> <TL2 className={classes.notificationBody}>{message}</TL2>
</Grid>
<Grid item xs={12}>
<Label1 className={classes.notificationSubtitle}>{age}</Label1> <Label1 className={classes.notificationSubtitle}>{age}</Label1>
</Grid> </div>
</Grid> <div className={classes.readIconWrapper}>
<Grid item xs={3} style={{ zIndex: 1 }}>
<div <div
onClick={() => toggleClear(id)} onClick={() => toggleClear(id)}
className={classnames(iconClass)} className={classnames(iconClass)}
/> />
</Grid> </div>
{!valid && <StripesSvg className={classes.stripes} />} </div>
</Grid>
) )
} }

View file

@ -79,6 +79,7 @@ const Tr = ({
onClick, onClick,
error, error,
errorMessage, errorMessage,
shouldShowError,
children, children,
className, className,
size, size,
@ -99,7 +100,9 @@ const Tr = ({
<Card className={classnames(classNames, className)} onClick={onClick}> <Card className={classnames(classNames, className)} onClick={onClick}>
<CardContent classes={cardClasses}> <CardContent classes={cardClasses}>
<div className={classes.mainContent}>{children}</div> <div className={classes.mainContent}>{children}</div>
{error && <div className={classes.errorContent}>{errorMessage}</div>} {error && shouldShowError && (
<div className={classes.errorContent}>{errorMessage}</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</> </>

View file

@ -9,10 +9,12 @@ const styles = {
borderRadius: '4px' borderRadius: '4px'
}, },
focus: { focus: {
color: primaryColor,
border: '2px solid', border: '2px solid',
borderColor: primaryColor, borderColor: primaryColor,
borderRadius: '4px' borderRadius: '4px',
'&:focus': {
outline: 'none'
}
}, },
error: { error: {
borderColor: errorColor borderColor: errorColor

View file

@ -4,6 +4,7 @@ import React from 'react'
import Chip from 'src/components/Chip' import Chip from 'src/components/Chip'
import { Info2, Label1, Label2 } from 'src/components/typography' import { Info2, Label1, Label2 } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import { cashboxStyles, gridStyles } from './Cashbox.styles' import { cashboxStyles, gridStyles } from './Cashbox.styles'
@ -64,11 +65,9 @@ const CashIn = ({ currency, notes, total }) => {
<Info2 className={classes.noMarginText}>{notes} notes</Info2> <Info2 className={classes.noMarginText}>{notes} notes</Info2>
</div> </div>
<div className={classes.innerRow}> <div className={classes.innerRow}>
{/* Feature on hold until this can be calculated
<Label1 className={classes.noMarginText}> <Label1 className={classes.noMarginText}>
{total} {currency.code} {total} {currency.code}
</Label1> </Label1>
*/}
</div> </div>
</div> </div>
</div> </div>
@ -112,7 +111,7 @@ const CashOut = ({
</div> </div>
<div className={classes.innerRow}> <div className={classes.innerRow}>
<Label1 className={classes.noMarginText}> <Label1 className={classes.noMarginText}>
{notes * denomination} {currency.code} {numberToFiatAmount(notes * denomination)} {currency.code}
</Label1> </Label1>
</div> </div>
</div> </div>

View file

@ -103,7 +103,7 @@ const Header = memo(({ tree, user }) => {
const handleClick = event => { const handleClick = event => {
const coords = notifCenterButtonRef.current.getBoundingClientRect() const coords = notifCenterButtonRef.current.getBoundingClientRect()
setNotifButtonCoords({ x: coords.x, y: coords.y }) setNotifButtonCoords({ x: coords.x, y: coords.y + 5 })
setAnchorEl(anchorEl ? null : event.currentTarget) setAnchorEl(anchorEl ? null : event.currentTarget)
document.querySelector('#root').classList.add('root-notifcenter-open') document.querySelector('#root').classList.add('root-notifcenter-open')
@ -132,7 +132,7 @@ const Header = memo(({ tree, user }) => {
return ( return (
<NavLink <NavLink
key={idx} key={idx}
to={it.route || it.children[0].route} to={!R.isNil(it.children) ? it.children[0].route : it.route}
isActive={match => { isActive={match => {
if (!match) return false if (!match) return false
setActive(it) setActive(it)
@ -173,10 +173,16 @@ const Header = memo(({ tree, user }) => {
anchorEl={anchorEl} anchorEl={anchorEl}
className={classes.popper} className={classes.popper}
disablePortal={false} disablePortal={false}
placement="bottom-end"
modifiers={{ modifiers={{
offset: {
enabled: true,
offset: '100vw'
},
preventOverflow: { preventOverflow: {
enabled: true, enabled: true,
boundariesElement: 'viewport' boundariesElement: 'viewport',
padding: 0
} }
}}> }}>
<NotificationCenter <NotificationCenter

View file

@ -171,7 +171,7 @@ const styles = {
hasUnread: { hasUnread: {
position: 'absolute', position: 'absolute',
top: 4, top: 4,
left: 182, left: 186,
width: '9px', width: '9px',
height: '9px', height: '9px',
backgroundColor: secondaryColor, backgroundColor: secondaryColor,

View file

@ -28,6 +28,7 @@ const useStyles = makeStyles(styles)
const Row = ({ const Row = ({
id, id,
index,
elements, elements,
data, data,
width, width,
@ -48,9 +49,11 @@ const Row = ({
[classes.row]: true, [classes.row]: true,
[classes.expanded]: expanded [classes.expanded]: expanded
} }
return ( return (
<div className={classes.rowWrapper}> <div className={classes.rowWrapper}>
<div className={classnames({ [classes.before]: expanded && id !== 0 })}> <div
className={classnames({ [classes.before]: expanded && index !== 0 })}>
<Tr <Tr
size={size} size={size}
className={classnames(trClasses)} className={classnames(trClasses)}
@ -58,8 +61,9 @@ const Row = ({
expandable && expandRow(id, data) expandable && expandRow(id, data)
onClick && onClick(data) onClick && onClick(data)
}} }}
error={data.error} error={data.error || data.hasError}
errorMessage={data.errorMessage}> shouldShowError={false}
errorMessage={data.errorMessage || data.hasError}>
{elements.map(({ view = it => it?.toString(), ...props }, idx) => ( {elements.map(({ view = it => it?.toString(), ...props }, idx) => (
<Td key={idx} {...props}> <Td key={idx} {...props}>
{view(data)} {view(data)}
@ -142,6 +146,7 @@ const DataTable = ({
width={width} width={width}
size={rowSize} size={rowSize}
id={data[index].id ? data[index].id : index} id={data[index].id ? data[index].id : index}
index={index}
expWidth={expWidth} expWidth={expWidth}
elements={elements} elements={elements}
data={data[index]} data={data[index]}

View file

@ -129,7 +129,7 @@ export default {
confirmationCode: { confirmationCode: {
extend: base, extend: base,
fontSize: codeInputFontSize, fontSize: codeInputFontSize,
fontFamily: fontPrimary, fontFamily: fontSecondary,
fontWeight: 900 fontWeight: 900
}, },
inline: { inline: {

View file

@ -15,6 +15,7 @@ import { ReactComponent as DashLogo } from 'src/styling/logos/icon-dash-colour.s
import { ReactComponent as EthereumLogo } from 'src/styling/logos/icon-ethereum-colour.svg' import { ReactComponent as EthereumLogo } from 'src/styling/logos/icon-ethereum-colour.svg'
import { ReactComponent as LitecoinLogo } from 'src/styling/logos/icon-litecoin-colour.svg' import { ReactComponent as LitecoinLogo } from 'src/styling/logos/icon-litecoin-colour.svg'
import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg' import { ReactComponent as ZCashLogo } from 'src/styling/logos/icon-zcash-colour.svg'
import { numberToFiatAmount } from 'src/utils/number'
import styles from './ATMWallet.styles' import styles from './ATMWallet.styles'
@ -51,9 +52,6 @@ const GET_OPERATOR_BY_USERNAME = gql`
} }
` `
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const CHIPS_PER_ROW = 6 const CHIPS_PER_ROW = 6
const Assets = ({ balance, wallets, currency }) => { const Assets = ({ balance, wallets, currency }) => {
@ -69,7 +67,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Available balance</P> <P className={classes.fieldHeader}>Available balance</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)} {numberToFiatAmount(balance)}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -81,7 +79,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Total balance in wallets</P> <P className={classes.fieldHeader}>Total balance in wallets</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(walletFiatSum())} {numberToFiatAmount(walletFiatSum())}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -93,7 +91,7 @@ const Assets = ({ balance, wallets, currency }) => {
<P className={classes.fieldHeader}>Total assets</P> <P className={classes.fieldHeader}>Total assets</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)} {numberToFiatAmount(balance)}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -144,17 +142,11 @@ const WalletInfoChip = ({ wallet, currency }) => {
<div className={classes.walletValueWrapper}> <div className={classes.walletValueWrapper}>
<Label2 className={classes.fieldHeader}>{wallet.name} value</Label2> <Label2 className={classes.fieldHeader}>{wallet.name} value</Label2>
<Label2 className={classes.walletValue}> <Label2 className={classes.walletValue}>
{wallet.amount.toFixed(1).toLocaleString('en-US', { {numberToFiatAmount(wallet.amount.toFixed(1))} {wallet.cryptoCode}
maximumFractionDigits: 2
})}{' '}
{wallet.cryptoCode}
</Label2> </Label2>
<Label2 className={classes.fieldHeader}>Hedged value</Label2> <Label2 className={classes.fieldHeader}>Hedged value</Label2>
<Label2 className={classes.walletValue}> <Label2 className={classes.walletValue}>
{wallet.fiatValue.toLocaleString('en-US', { {numberToFiatAmount(wallet.fiatValue)} {currency}
maximumFractionDigits: 2
})}{' '}
{currency}
</Label2> </Label2>
</div> </div>
</Paper> </Paper>

View file

@ -9,13 +9,11 @@ import { Tooltip } from 'src/components/Tooltip'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { H4, Info2, P } from 'src/components/typography' import { H4, Info2, P } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import { formatDate } from 'src/utils/timezones' import { formatDate } from 'src/utils/timezones'
import styles from './Accounting.styles' import styles from './Accounting.styles'
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const GET_OPERATOR_BY_USERNAME = gql` const GET_OPERATOR_BY_USERNAME = gql`
@ -64,7 +62,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Pazuz fiat balance</P> <P className={classes.fieldHeader}>Pazuz fiat balance</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance)} {numberToFiatAmount(balance)}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -76,7 +74,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Hedging reserve</P> <P className={classes.fieldHeader}>Hedging reserve</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(hedgingReserve)} {numberToFiatAmount(hedgingReserve)}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -88,7 +86,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
<P className={classes.fieldHeader}>Available balance</P> <P className={classes.fieldHeader}>Available balance</P>
<div className={classes.totalAssetWrapper}> <div className={classes.totalAssetWrapper}>
<Info2 noMargin className={classes.fieldValue}> <Info2 noMargin className={classes.fieldValue}>
{formatCurrency(balance - hedgingReserve)} {numberToFiatAmount(balance - hedgingReserve)}
</Info2> </Info2>
<Info2 noMargin className={classes.fieldCurrency}> <Info2 noMargin className={classes.fieldCurrency}>
{R.toUpper(currency)} {R.toUpper(currency)}
@ -114,7 +112,7 @@ const Accounting = () => {
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const loading = operatorLoading && configLoading const loading = operatorLoading || configLoading
const operatorData = R.path(['operatorByUsername'], opData) const operatorData = R.path(['operatorByUsername'], opData)
@ -143,7 +141,7 @@ const Accounting = () => {
size: 'sm', size: 'sm',
textAlign: 'right', textAlign: 'right',
view: it => view: it =>
`${formatCurrency(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}` `${numberToFiatAmount(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}`
}, },
{ {
header: 'Balance after operation', header: 'Balance after operation',
@ -151,7 +149,9 @@ const Accounting = () => {
size: 'sm', size: 'sm',
textAlign: 'right', textAlign: 'right',
view: it => view: it =>
`${formatCurrency(it.fiatBalanceAfter)} ${R.toUpper(it.fiatCurrency)}` `${numberToFiatAmount(it.fiatBalanceAfter)} ${R.toUpper(
it.fiatCurrency
)}`
}, },
{ {
header: 'Date', header: 'Date',
@ -170,19 +170,16 @@ const Accounting = () => {
] ]
return ( return (
!loading && (
<> <>
<TitleSection title="Accounting" /> <TitleSection title="Accounting" />
<Assets <Assets
balance={ balance={operatorData.fiatBalances[operatorData.preferredFiatCurrency]}
operatorData.fiatBalances[operatorData.preferredFiatCurrency]
}
hedgingReserve={operatorData.hedgingReserve ?? 0} hedgingReserve={operatorData.hedgingReserve ?? 0}
currency={operatorData.preferredFiatCurrency} currency={operatorData.preferredFiatCurrency}
/> />
<H4 className={classes.tableTitle}>Fiat balance history</H4> <H4 className={classes.tableTitle}>Fiat balance history</H4>
<DataTable <DataTable
loading={false} loading={loading}
emptyText="No transactions so far" emptyText="No transactions so far"
elements={elements} elements={elements}
data={operatorData.fundings ?? []} data={operatorData.fundings ?? []}
@ -190,7 +187,6 @@ const Accounting = () => {
/> />
</> </>
) )
)
} }
export default Accounting export default Accounting

View file

@ -14,6 +14,7 @@ import { Button } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik' import { TextInput } from 'src/components/inputs/formik'
import Sidebar from 'src/components/layout/Sidebar' import Sidebar from 'src/components/layout/Sidebar'
import { Info2, P } from 'src/components/typography' import { Info2, P } from 'src/components/typography'
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg' import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg' import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
@ -70,8 +71,18 @@ const QrCodeComponent = ({ classes, qrCode, name, count, onPaired }) => {
Scan QR code with your new cryptomat Scan QR code with your new cryptomat
</Info2> </Info2>
<div className={classes.qrCodeWrapper}> <div className={classes.qrCodeWrapper}>
<div> <div className={classes.qrCodeImageWrapper}>
<QRCode size={240} fgColor={primaryColor} value={qrCode} /> <QRCode
size={280}
fgColor={primaryColor}
includeMargin
value={qrCode}
className={classes.qrCodeBorder}
/>
<div className={classes.qrCodeScanMessage}>
<CameraIcon />
<P noMargin>Snap a picture and scan</P>
</div>
</div> </div>
<div className={classes.qrTextWrapper}> <div className={classes.qrTextWrapper}>
<div className={classes.qrTextInfoWrapper}> <div className={classes.qrTextInfoWrapper}>

View file

@ -126,6 +126,23 @@ const styles = {
}, },
errorMessage: { errorMessage: {
color: errorColor color: errorColor
},
qrCodeImageWrapper: {
display: 'flex',
flexDirection: 'column',
backgroundColor: 'white',
border: `5px solid ${primaryColor}`,
padding: 5,
borderRadius: 15
},
qrCodeScanMessage: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
margin: [[0, 0, 20, 20]],
'& > p': {
marginLeft: 10
}
} }
} }

View file

@ -13,6 +13,7 @@ import { ReactComponent as DownIcon } from 'src/styling/icons/dashboard/down.svg
import { ReactComponent as EqualIcon } from 'src/styling/icons/dashboard/equal.svg' import { ReactComponent as EqualIcon } from 'src/styling/icons/dashboard/equal.svg'
import { ReactComponent as UpIcon } from 'src/styling/icons/dashboard/up.svg' import { ReactComponent as UpIcon } from 'src/styling/icons/dashboard/up.svg'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import { numberToFiatAmount } from 'src/utils/number'
import { DAY, WEEK, MONTH } from 'src/utils/time' import { DAY, WEEK, MONTH } from 'src/utils/time'
import styles from './Analytics.styles' import styles from './Analytics.styles'
@ -97,9 +98,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
<div className={classes.overviewEntry}> <div className={classes.overviewEntry}>
<P noMargin>{label}</P> <P noMargin>{label}</P>
<Info2 noMargin className={classes.overviewFieldWrapper}> <Info2 noMargin className={classes.overviewFieldWrapper}>
<span> <span>{numberToFiatAmount(value)}</span>
{value.toLocaleString('en-US', { maximumFractionDigits: 2 })}
</span>
{!!currency && ` ${currency}`} {!!currency && ` ${currency}`}
</Info2> </Info2>
<span className={classes.overviewGrowth}> <span className={classes.overviewGrowth}>
@ -107,7 +106,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
{R.lt(growthRate, 0) && <DownIcon height={10} />} {R.lt(growthRate, 0) && <DownIcon height={10} />}
{R.equals(growthRate, 0) && <EqualIcon height={10} />} {R.equals(growthRate, 0) && <EqualIcon height={10} />}
<P noMargin className={classnames(growthClasses)}> <P noMargin className={classnames(growthClasses)}>
{growthRate.toLocaleString('en-US', { maximumFractionDigits: 2 })}% {numberToFiatAmount(growthRate)}%
</P> </P>
</span> </span>
</div> </div>

View file

@ -6,6 +6,7 @@ import React, { memo } from 'react'
import { Info2, Label3, P } from 'src/components/typography' import { Info2, Label3, P } from 'src/components/typography'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { numberToFiatAmount } from 'src/utils/number'
import { singularOrPlural } from 'src/utils/string' import { singularOrPlural } from 'src/utils/string'
import { formatDate, formatDateNonUtc } from 'src/utils/timezones' import { formatDate, formatDateNonUtc } from 'src/utils/timezones'
@ -13,9 +14,6 @@ import styles from './GraphTooltip.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const GraphTooltip = ({ const GraphTooltip = ({
coords, coords,
data, data,
@ -67,7 +65,7 @@ const GraphTooltip = ({
{singularOrPlural(R.length(data), 'transaction', 'transactions')} {singularOrPlural(R.length(data), 'transaction', 'transactions')}
</P> </P>
<P noMargin className={classes.dotOtTransactionVolume}> <P noMargin className={classes.dotOtTransactionVolume}>
{formatCurrency(transactions.volume)} {currency} in volume {numberToFiatAmount(transactions.volume)} {currency} in volume
</P> </P>
<div className={classes.dotOtTransactionClasses}> <div className={classes.dotOtTransactionClasses}>
<Label3 noMargin> <Label3 noMargin>

View file

@ -14,8 +14,10 @@ import React, { useContext } from 'react'
import AppContext from 'src/AppContext' import AppContext from 'src/AppContext'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { H4, Label2, P, Info2 } from 'src/components/typography' import { H4, Label2, P, Info2 } from 'src/components/typography'
import { numberToFiatAmount } from 'src/utils/number'
import styles from './Assets.styles' import styles from './Assets.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const GET_OPERATOR_BY_USERNAME = gql` const GET_OPERATOR_BY_USERNAME = gql`
@ -105,7 +107,7 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
</Cell> </Cell>
<Cell align="right"> <Cell align="right">
<P>{`${selectAmountPrefix(asset)} <P>{`${selectAmountPrefix(asset)}
${formatCurrency(Math.abs(asset.amount))} ${ ${numberToFiatAmount(Math.abs(asset.amount))} ${
asset.currency asset.currency
}`}</P> }`}</P>
</Cell> </Cell>
@ -117,7 +119,9 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
<Info2>{`Total ${R.toLower(title)}`}</Info2> <Info2>{`Total ${R.toLower(title)}`}</Info2>
</Cell> </Cell>
<Cell align="right"> <Cell align="right">
<Info2>{`${formatCurrency(totalAmount)} ${currency}`}</Info2> <Info2>{`${numberToFiatAmount(
totalAmount
)} ${currency}`}</Info2>
</Cell> </Cell>
</TableRow> </TableRow>
</TableBody> </TableBody>
@ -128,9 +132,6 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
) )
} }
const formatCurrency = amount =>
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
const Assets = () => { const Assets = () => {
const classes = useStyles() const classes = useStyles()
const { userData } = useContext(AppContext) const { userData } = useContext(AppContext)

View file

@ -1,6 +1,7 @@
import { useMutation, useLazyQuery } from '@apollo/react-hooks' import { useMutation, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import base64 from 'base-64' import base64 from 'base-64'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
@ -120,6 +121,9 @@ const Input2FAState = ({ state, dispatch }) => {
<TL1 className={classes.info}> <TL1 className={classes.info}>
Enter your two-factor authentication code Enter your two-factor authentication code
</TL1> </TL1>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput <CodeInput
name="2fa" name="2fa"
value={state.twoFAField} value={state.twoFAField}
@ -128,6 +132,9 @@ const Input2FAState = ({ state, dispatch }) => {
error={invalidToken} error={invalidToken}
shouldAutoFocus shouldAutoFocus
/> />
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
<div className={classes.twofaFooter}> <div className={classes.twofaFooter}>
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>} {errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}> <Button onClick={handleSubmit} buttonClassName={classes.loginButton}>

View file

@ -1,6 +1,7 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles, Grid } from '@material-ui/core' import { makeStyles, Grid } from '@material-ui/core'
import Paper from '@material-ui/core/Paper' import Paper from '@material-ui/core/Paper'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import React, { useReducer, useState } from 'react' import React, { useReducer, useState } from 'react'
@ -101,6 +102,20 @@ const Reset2FA = () => {
return null return null
} }
const handleSubmit = () => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
token: token,
userID: state.userID,
code: twoFAConfirmation
}
})
}
return ( return (
<Grid <Grid
container container
@ -152,6 +167,9 @@ const Reset2FA = () => {
</ActionButton> </ActionButton>
</div> </div>
<div className={classes.confirm2FAInput}> <div className={classes.confirm2FAInput}>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput <CodeInput
name="2fa" name="2fa"
value={twoFAConfirmation} value={twoFAConfirmation}
@ -160,25 +178,19 @@ const Reset2FA = () => {
error={invalidToken} error={invalidToken}
shouldAutoFocus shouldAutoFocus
/> />
<button
onClick={handleSubmit}
className={classes.enterButton}
/>
</Form>
</Formik>
</div> </div>
<div className={classes.twofaFooter}> <div className={classes.twofaFooter}>
{getErrorMsg() && ( {getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P> <P className={classes.errorMessage}>{getErrorMsg()}</P>
)} )}
<Button <Button
onClick={() => { onClick={handleSubmit}
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
reset2FA({
variables: {
token: token,
userID: state.userID,
code: twoFAConfirmation
}
})
}}
buttonClassName={classes.loginButton}> buttonClassName={classes.loginButton}>
Done Done
</Button> </Button>

View file

@ -1,6 +1,7 @@
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks' import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import base64 from 'base-64' import base64 from 'base-64'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import React, { useContext, useState } from 'react' import React, { useContext, useState } from 'react'
@ -125,6 +126,14 @@ const Setup2FAState = ({ state, dispatch }) => {
return null return null
} }
const handleSubmit = () => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
setup2FA(mutationOptions)
}
return ( return (
secret && secret &&
otpauth && ( otpauth && (
@ -159,6 +168,9 @@ const Setup2FAState = ({ state, dispatch }) => {
</ActionButton> </ActionButton>
</div> </div>
<div className={classes.confirm2FAInput}> <div className={classes.confirm2FAInput}>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput <CodeInput
name="2fa" name="2fa"
value={twoFAConfirmation} value={twoFAConfirmation}
@ -167,20 +179,15 @@ const Setup2FAState = ({ state, dispatch }) => {
error={invalidToken} error={invalidToken}
shouldAutoFocus shouldAutoFocus
/> />
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
</div> </div>
<div className={classes.twofaFooter}> <div className={classes.twofaFooter}>
{getErrorMsg() && ( {getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P> <P className={classes.errorMessage}>{getErrorMsg()}</P>
)} )}
<Button <Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
onClick={() => {
if (twoFAConfirmation.length !== 6) {
setInvalidToken(true)
return
}
setup2FA(mutationOptions)
}}
buttonClassName={classes.loginButton}>
Done Done
</Button> </Button>
</div> </div>

View file

@ -100,6 +100,9 @@ const styles = {
}, },
error: { error: {
color: errorColor color: errorColor
},
enterButton: {
display: 'none'
} }
} }

View file

@ -1,4 +1,4 @@
import { spacer, white, errorColor } from 'src/styling/variables' import { spacer, white } from 'src/styling/variables'
const styles = { const styles = {
grid: { grid: {
flex: 1, flex: 1,
@ -32,7 +32,7 @@ const styles = {
marginLeft: 8 marginLeft: 8
}, },
error: { error: {
color: errorColor marginTop: 20
} }
} }

View file

@ -5,6 +5,7 @@ import * as R from 'ramda'
import React from 'react' import React from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import ErrorMessage from 'src/components/ErrorMessage'
import Modal from 'src/components/Modal' import Modal from 'src/components/Modal'
import { Link } from 'src/components/buttons' import { Link } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs/formik' import { TextInput } from 'src/components/inputs/formik'
@ -32,7 +33,10 @@ const BlackListModal = ({
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa', LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn', DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR', ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm' BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
XMR:
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
} }
return ( return (
@ -53,9 +57,8 @@ const BlackListModal = ({
.trim() .trim()
.required('An address is required') .required('An address is required')
})} })}
onSubmit={({ address }, { resetForm }) => { onSubmit={({ address }) => {
handleAddToBlacklist(address.trim()) handleAddToBlacklist(address.trim())
resetForm()
}}> }}>
<Form id="address-form"> <Form id="address-form">
<H3 className={classes.modalTitle}> <H3 className={classes.modalTitle}>
@ -63,7 +66,6 @@ const BlackListModal = ({
? `Blacklist ${R.toLower(selectedCoin.display)} address` ? `Blacklist ${R.toLower(selectedCoin.display)} address`
: ''} : ''}
</H3> </H3>
<span className={classes.error}>{errorMsg}</span>
<Field <Field
name="address" name="address"
fullWidth fullWidth
@ -72,6 +74,9 @@ const BlackListModal = ({
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`} placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
component={TextInput} component={TextInput}
/> />
{!R.isNil(errorMsg) && (
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
)}
</Form> </Form>
</Formik> </Formik>
<div className={classes.footer}> <div className={classes.footer}>

View file

@ -162,14 +162,14 @@ const WizardStep = ({
{lastStep && ( {lastStep && (
<div className={classes.disclaimer}> <div className={classes.disclaimer}>
<Info2 className={classes.title}>Cash-out Bill Count</Info2> <Info2 className={classes.title}>Cash Cassette Bill Count</Info2>
<P> <P>
<WarningIcon className={classes.disclaimerIcon} /> <WarningIcon className={classes.disclaimerIcon} />
When enabling cash-out, your bill count will be automatically set to When enabling cash-out, your bill count will be automatically set to
zero. Make sure you physically put cash inside the cash cassettes 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, allow the machine to dispense it to your users. If you already did,
make sure you set the correct cash-out bill count for this machine make sure you set the correct cash cassette bill count for this
on your Cash Cassettes tab under Maintenance. machine on your Cash Boxes & Cassettes tab under Maintenance.
</P> </P>
<Info2 className={classes.title}>Default Commissions</Info2> <Info2 className={classes.title}>Default Commissions</Info2>

View file

@ -0,0 +1,81 @@
import { makeStyles, Paper } from '@material-ui/core'
import { format } from 'date-fns/fp'
import * as R from 'ramda'
import { React, useState } from 'react'
import { InformativeDialog } from 'src/components/InformativeDialog'
import { Label2, H3 } from 'src/components/typography'
import { ReactComponent as CameraIcon } from 'src/styling/icons/ID/photo/comet.svg'
import { URI } from 'src/utils/apollo'
import styles from './CustomerPhotos.styles'
import PhotosCarousel from './components/PhotosCarousel'
const useStyles = makeStyles(styles)
const CustomerPhotos = ({ photosData }) => {
const classes = useStyles()
const [photosDialog, setPhotosDialog] = useState(false)
const [photoClickedIndex, setPhotoClickIndex] = useState(null)
const orderedPhotosData = !R.isNil(photoClickedIndex)
? R.compose(R.flatten, R.reverse, R.splitAt(photoClickedIndex))(photosData)
: photosData
return (
<div>
<div className={classes.header}>
<H3 className={classes.title}>{'Photos & files'}</H3>
</div>
<div className={classes.photosChipList}>
{photosData.map((elem, idx) => (
<PhotoCard
idx={idx}
date={elem.date}
src={`${URI}/${elem.photoDir}/${elem.path}`}
setPhotosDialog={setPhotosDialog}
setPhotoClickIndex={setPhotoClickIndex}
/>
))}
</div>
<InformativeDialog
open={photosDialog}
title={`Photo roll`}
data={<PhotosCarousel photosData={orderedPhotosData} />}
onDissmised={() => {
setPhotosDialog(false)
setPhotoClickIndex(null)
}}
/>
</div>
)
}
export const PhotoCard = ({
idx,
date,
src,
setPhotosDialog,
setPhotoClickIndex
}) => {
const classes = useStyles()
return (
<Paper
className={classes.photoCardChip}
onClick={() => {
setPhotoClickIndex(idx)
setPhotosDialog(true)
}}>
<img className={classes.image} src={src} alt="" />
<div className={classes.footer}>
<CameraIcon />
<Label2 className={classes.date}>
{format('yyyy-MM-dd', new Date(date))}
</Label2>
</div>
</Paper>
)
}
export default CustomerPhotos

View file

@ -0,0 +1,37 @@
const styles = {
header: {
display: 'flex',
flexDirection: 'row'
},
title: {
marginTop: 7,
marginRight: 24,
marginBottom: 32
},
photosChipList: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap'
},
image: {
objectFit: 'cover',
objectPosition: 'center',
width: 224,
height: 200,
borderTopLeftRadius: 4,
borderTopRightRadius: 4
},
photoCardChip: {
margin: [[0, 16, 0, 0]]
},
footer: {
display: 'flex',
flexDirection: 'row',
margin: [[8, 0, 0, 8]]
},
date: {
margin: [[0, 0, 8, 12]]
}
}
export default styles

View file

@ -24,6 +24,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
import CustomerData from './CustomerData' import CustomerData from './CustomerData'
import CustomerNotes from './CustomerNotes' import CustomerNotes from './CustomerNotes'
import CustomerPhotos from './CustomerPhotos'
import styles from './CustomerProfile.styles' import styles from './CustomerProfile.styles'
import { import {
CustomerDetails, CustomerDetails,
@ -31,7 +32,7 @@ import {
CustomerSidebar, CustomerSidebar,
Wizard Wizard
} from './components' } from './components'
import { getFormattedPhone, getName } from './helper' import { getFormattedPhone, getName, formatPhotosData } from './helper'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -367,12 +368,24 @@ const CustomerProfile = memo(() => {
const isCustomerData = clickedItem === 'customerData' const isCustomerData = clickedItem === 'customerData'
const isOverview = clickedItem === 'overview' const isOverview = clickedItem === 'overview'
const isNotes = clickedItem === 'notes' const isNotes = clickedItem === 'notes'
const isPhotos = clickedItem === 'photos'
const loading = customerLoading && configLoading const frontCameraData = R.pick(['frontCameraPath', 'frontCameraAt'])(
customerData
)
const txPhotosData =
sortedTransactions &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
sortedTransactions
)
const photosData = formatPhotosData(R.append(frontCameraData, txPhotosData))
const loading = customerLoading || configLoading
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const classes = useStyles({ blocked }) const classes = useStyles()
return ( return (
<> <>
@ -406,29 +419,26 @@ const CustomerProfile = memo(() => {
/> />
</div> </div>
<Label1 className={classes.actionLabel}>Actions</Label1> <Label1 className={classes.actionLabel}>Actions</Label1>
<div> <div className={classes.actionBar}>
<ActionButton <ActionButton
className={classes.customerManualDataEntry} className={classes.actionButton}
color="primary" color="primary"
Icon={DataIcon} Icon={DataIcon}
InverseIcon={DataReversedIcon} InverseIcon={DataReversedIcon}
onClick={() => setWizard(true)}> onClick={() => setWizard(true)}>
{`Manual data entry`} {`Manual data entry`}
</ActionButton> </ActionButton>
</div>
<div>
<ActionButton <ActionButton
className={classes.customerDiscount} className={classes.actionButton}
color="primary" color="primary"
Icon={Discount} Icon={Discount}
InverseIcon={DiscountReversedIcon} InverseIcon={DiscountReversedIcon}
onClick={() => {}}> onClick={() => {}}>
{`Add individual discount`} {`Add individual discount`}
</ActionButton> </ActionButton>
</div>
<div>
{isSuspended && ( {isSuspended && (
<ActionButton <ActionButton
className={classes.actionButton}
color="primary" color="primary"
Icon={AuthorizeIcon} Icon={AuthorizeIcon}
InverseIcon={AuthorizeReversedIcon} InverseIcon={AuthorizeReversedIcon}
@ -442,7 +452,7 @@ const CustomerProfile = memo(() => {
)} )}
<ActionButton <ActionButton
color="primary" color="primary"
className={classes.customerBlock} className={classes.actionButton}
Icon={blocked ? AuthorizeIcon : BlockIcon} Icon={blocked ? AuthorizeIcon : BlockIcon}
InverseIcon={ InverseIcon={
blocked ? AuthorizeReversedIcon : BlockReversedIcon blocked ? AuthorizeReversedIcon : BlockReversedIcon
@ -458,7 +468,7 @@ const CustomerProfile = memo(() => {
</ActionButton> </ActionButton>
<ActionButton <ActionButton
color="primary" color="primary"
className={classes.retrieveInformation} className={classes.actionButton}
Icon={blocked ? AuthorizeIcon : BlockIcon} Icon={blocked ? AuthorizeIcon : BlockIcon}
InverseIcon={ InverseIcon={
blocked ? AuthorizeReversedIcon : BlockReversedIcon blocked ? AuthorizeReversedIcon : BlockReversedIcon
@ -488,6 +498,7 @@ const CustomerProfile = memo(() => {
justifyContent="space-between"> justifyContent="space-between">
<CustomerDetails <CustomerDetails
customer={customerData} customer={customerData}
photosData={photosData}
locale={locale} locale={locale}
setShowCompliance={() => setShowCompliance(!showCompliance)} setShowCompliance={() => setShowCompliance(!showCompliance)}
/> />
@ -524,6 +535,11 @@ const CustomerProfile = memo(() => {
timezone={timezone}></CustomerNotes> timezone={timezone}></CustomerNotes>
</div> </div>
)} )}
{isPhotos && (
<div>
<CustomerPhotos photosData={photosData} />
</div>
)}
</div> </div>
{wizard && ( {wizard && (
<Wizard <Wizard

View file

@ -15,29 +15,16 @@ export default {
customerDetails: { customerDetails: {
marginBottom: 18 marginBottom: 18
}, },
customerBlock: props => ({ actionButton: {
margin: [[0, 0, 4, 0]],
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
margin: [[0, 0, 4, 0]], justifyContent: 'center'
padding: [[0, props.blocked ? 35 : 48, 0]]
}),
customerDiscount: {
display: 'flex',
flexDirection: 'row',
margin: [[0, 0, 4, 0]],
padding: [[0, 23.5, 0]]
}, },
customerManualDataEntry: { actionBar: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'column',
margin: [[8, 0, 4, 0]], width: 219
padding: [[0, 40.5, 0]]
},
retrieveInformation: {
display: 'flex',
flexDirection: 'row',
margin: [[0, 0, 4, 0]],
padding: [[0, 32.5, 0]]
}, },
panels: { panels: {
display: 'flex' display: 'flex'

View file

@ -12,8 +12,7 @@ import PhotosCard from './PhotosCard'
const useStyles = makeStyles(mainStyles) const useStyles = makeStyles(mainStyles)
const CustomerDetails = memo( const CustomerDetails = memo(({ customer, photosData, locale }) => {
({ txData, customer, locale, setShowCompliance }) => {
const classes = useStyles() const classes = useStyles()
const idNumber = R.path(['idCardData', 'documentNumber'])(customer) const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
@ -45,27 +44,14 @@ const CustomerDetails = memo(
return ( return (
<Box display="flex"> <Box display="flex">
<PhotosCard <PhotosCard photosData={photosData} />
frontCameraData={R.pick(['frontCameraPath', 'frontCameraAt'])(
customer
)}
txPhotosData={
txData &&
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
txData
)
}
/>
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
<div className={classes.name}> <div className={classes.name}>
<IdIcon className={classes.idIcon} /> <IdIcon className={classes.idIcon} />
<H2 noMargin> <H2 noMargin>
{name.length {name.length
? name ? name
: getFormattedPhone( : getFormattedPhone(R.path(['phone'])(customer), locale.country)}
R.path(['phone'])(customer),
locale.country
)}
</H2> </H2>
</div> </div>
<Box display="flex" mt="auto"> <Box display="flex" mt="auto">
@ -93,7 +79,6 @@ const CustomerDetails = memo(
</Box> </Box>
</Box> </Box>
) )
} })
)
export default CustomerDetails export default CustomerDetails

View file

@ -8,6 +8,8 @@ import { ReactComponent as NoteReversedIcon } from 'src/styling/icons/customer-n
import { ReactComponent as NoteIcon } from 'src/styling/icons/customer-nav/note/white.svg' import { ReactComponent as NoteIcon } from 'src/styling/icons/customer-nav/note/white.svg'
import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/customer-nav/overview/comet.svg' import { ReactComponent as OverviewReversedIcon } from 'src/styling/icons/customer-nav/overview/comet.svg'
import { ReactComponent as OverviewIcon } from 'src/styling/icons/customer-nav/overview/white.svg' import { ReactComponent as OverviewIcon } from 'src/styling/icons/customer-nav/overview/white.svg'
import { ReactComponent as PhotosReversedIcon } from 'src/styling/icons/customer-nav/photos/comet.svg'
import { ReactComponent as Photos } from 'src/styling/icons/customer-nav/photos/white.svg'
import styles from './CustomerSidebar.styles.js' import styles from './CustomerSidebar.styles.js'
@ -33,6 +35,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
display: 'Notes', display: 'Notes',
Icon: NoteIcon, Icon: NoteIcon,
InverseIcon: NoteReversedIcon InverseIcon: NoteReversedIcon
},
{
code: 'photos',
display: 'Photos & files',
Icon: Photos,
InverseIcon: PhotosReversedIcon
} }
] ]

View file

@ -4,58 +4,21 @@ import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda' import * as R from 'ramda'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { Carousel } from 'src/components/Carousel'
import { InformativeDialog } from 'src/components/InformativeDialog' import { InformativeDialog } from 'src/components/InformativeDialog'
import { Info2, Label1 } from 'src/components/typography' import { Info2 } from 'src/components/typography'
import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg' import { ReactComponent as CrossedCameraIcon } from 'src/styling/icons/ID/photo/crossed-camera.svg'
import { URI } from 'src/utils/apollo' import { URI } from 'src/utils/apollo'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import styles from './PhotosCard.styles' import styles from './PhotosCard.styles'
import PhotosCarousel from './PhotosCarousel'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Label = ({ children }) => { const PhotosCard = memo(({ photosData }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
const classes = useStyles() const classes = useStyles()
const [photosDialog, setPhotosDialog] = useState(false) const [photosDialog, setPhotosDialog] = useState(false)
const mapKeys = pair => {
const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
return ['path', value]
}
if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') {
return ['date', value]
}
return pair
}
const addPhotoDir = R.map(it => {
const hasFrontCameraData = R.has('id')(it)
return hasFrontCameraData
? { ...it, photoDir: 'operator-data/customersphotos' }
: { ...it, photoDir: 'front-camera-photo' }
})
const standardizeKeys = R.map(
R.compose(R.fromPairs, R.map(mapKeys), R.toPairs)
)
const filterByPhotoAvailable = R.filter(
tx => !R.isNil(tx.date) && !R.isNil(tx.path)
)
const photosData = filterByPhotoAvailable(
addPhotoDir(standardizeKeys(R.append(frontCameraData, txPhotosData)))
)
const singlePhoto = R.head(photosData) const singlePhoto = R.head(photosData)
return ( return (
@ -97,41 +60,4 @@ const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
) )
}) })
export const PhotosCarousel = memo(({ photosData }) => {
const classes = useStyles()
const [currentIndex, setCurrentIndex] = useState(0)
const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex])
const slidePhoto = index => setCurrentIndex(index)
return (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCard export default PhotosCard

View file

@ -1,7 +1,4 @@
import typographyStyles from 'src/components/typography/styles' import { zircon, backgroundColor } from 'src/styling/variables'
import { zircon, backgroundColor, offColor } from 'src/styling/variables'
const { p } = typographyStyles
export default { export default {
photo: { photo: {
@ -41,43 +38,5 @@ export default {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
display: 'flex' display: 'flex'
},
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
firstRow: {
padding: [[8]],
display: 'flex',
flexDirection: 'column'
},
secondRow: {
extend: p,
display: 'flex',
padding: [[8]],
'& > div': {
display: 'flex',
flexDirection: 'column',
'& > div': {
width: 144,
height: 37,
marginBottom: 15,
marginRight: 55
}
}
},
imgWrapper: {
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
width: 550,
height: 550
},
imgInner: {
objectFit: 'cover',
objectPosition: 'center',
width: 550,
height: 550,
marginBottom: 40
} }
} }

View file

@ -0,0 +1,56 @@
import { makeStyles } from '@material-ui/core/styles'
import * as R from 'ramda'
import React, { memo, useState } from 'react'
import { Carousel } from 'src/components/Carousel'
import { Label1 } from 'src/components/typography'
import CopyToClipboard from '../../Transactions/CopyToClipboard'
import styles from './PhotosCarousel.styles'
const useStyles = makeStyles(styles)
const PhotosCarousel = memo(({ photosData }) => {
const classes = useStyles()
const [currentIndex, setCurrentIndex] = useState(0)
const Label = ({ children }) => {
const classes = useStyles()
return <Label1 className={classes.label}>{children}</Label1>
}
const isFaceCustomerPhoto = !R.has('id')(photosData[currentIndex])
const slidePhoto = index => setCurrentIndex(index)
return (
<>
<Carousel photosData={photosData} slidePhoto={slidePhoto} />
{!isFaceCustomerPhoto && (
<div className={classes.firstRow}>
<Label>Session ID</Label>
<CopyToClipboard>
{photosData && photosData[currentIndex]?.id}
</CopyToClipboard>
</div>
)}
<div className={classes.secondRow}>
<div>
<div>
<Label>Date</Label>
<div>{photosData && photosData[currentIndex]?.date}</div>
</div>
</div>
<div>
<Label>Taken by</Label>
<div>
{!isFaceCustomerPhoto ? 'Acceptance of T&C' : 'Compliance scan'}
</div>
</div>
</div>
</>
)
})
export default PhotosCarousel

View file

@ -0,0 +1,31 @@
import typographyStyles from 'src/components/typography/styles'
import { offColor } from 'src/styling/variables'
const { p } = typographyStyles
export default {
label: {
color: offColor,
margin: [[0, 0, 6, 0]]
},
firstRow: {
padding: [[8]],
display: 'flex',
flexDirection: 'column'
},
secondRow: {
extend: p,
display: 'flex',
padding: [[8]],
'& > div': {
display: 'flex',
flexDirection: 'column',
'& > div': {
width: 144,
height: 37,
marginBottom: 15,
marginRight: 55
}
}
}
}

View file

@ -70,12 +70,7 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
const tableElements = [ const tableElements = [
{ {
header: 'Machine', width: 40,
width: 160,
view: R.path(['machineName'])
},
{
width: 125,
view: it => ( view: it => (
<> <>
{it.txClass === 'cashOut' ? ( {it.txClass === 'cashOut' ? (
@ -86,6 +81,11 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
</> </>
) )
}, },
{
header: 'Machine',
width: 160,
view: R.path(['machineName'])
},
{ {
header: 'Transaction ID', header: 'Transaction ID',
width: 145, width: 145,

View file

@ -5,10 +5,12 @@ import CustomerSidebar from './CustomerSidebar'
import EditableCard from './EditableCard' import EditableCard from './EditableCard'
import Field from './Field' import Field from './Field'
import IdDataCard from './IdDataCard' import IdDataCard from './IdDataCard'
import PhotosCarousel from './PhotosCarousel'
import TransactionsList from './TransactionsList' import TransactionsList from './TransactionsList'
import Upload from './Upload' import Upload from './Upload'
export { export {
PhotosCarousel,
CustomerDetails, CustomerDetails,
IdDataCard, IdDataCard,
TransactionsList, TransactionsList,

View file

@ -209,10 +209,41 @@ const entryType = {
initialValues: { entryType: '' } initialValues: { entryType: '' }
} }
const mapKeys = pair => {
const [key, value] = pair
if (key === 'txCustomerPhotoPath' || key === 'frontCameraPath') {
return ['path', value]
}
if (key === 'txCustomerPhotoAt' || key === 'frontCameraAt') {
return ['date', value]
}
return pair
}
const addPhotoDir = R.map(it => {
const hasFrontCameraData = R.has('id')(it)
return hasFrontCameraData
? { ...it, photoDir: 'operator-data/customersphotos' }
: { ...it, photoDir: 'front-camera-photo' }
})
const standardizeKeys = R.map(R.compose(R.fromPairs, R.map(mapKeys), R.toPairs))
const filterByPhotoAvailable = R.filter(
tx => !R.isNil(tx.date) && !R.isNil(tx.path)
)
const formatPhotosData = R.compose(
filterByPhotoAvailable,
addPhotoDir,
standardizeKeys
)
export { export {
getAuthorizedStatus, getAuthorizedStatus,
getFormattedPhone, getFormattedPhone,
getName, getName,
entryType, entryType,
customElements customElements,
formatPhotosData
} }

View file

@ -7,8 +7,8 @@ import { useHistory } from 'react-router-dom'
import { P } from 'src/components/typography/index' import { P } from 'src/components/typography/index'
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg' import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
import { ReactComponent as CashBoxEmpty } from 'src/styling/icons/cassettes/cashbox-empty.svg' import { ReactComponent as CashBoxEmpty } from 'src/styling/icons/cassettes/cashbox-empty.svg'
import { ReactComponent as AlertLinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg' import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
import styles from './Alerts.styles' import styles from './Alerts.styles'
@ -49,7 +49,7 @@ const AlertsTable = ({ numToRender, alerts, machines }) => {
<Wrench style={{ height: 23, width: 23, marginRight: 8 }} /> <Wrench style={{ height: 23, width: 23, marginRight: 8 }} />
)} )}
<P className={classes.listItemText}>{alertMessage(alert)}</P> <P className={classes.listItemText}>{alertMessage(alert)}</P>
<LinkIcon <AlertLinkIcon
className={classes.linkIcon} className={classes.linkIcon}
onClick={() => history.push(links[alert.type] || '/dashboard')} onClick={() => history.push(links[alert.type] || '/dashboard')}
/> />

View file

@ -1,7 +1,6 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
@ -13,6 +12,7 @@ import { H1, Info2, TL2, Label1 } from 'src/components/typography'
import AddMachine from 'src/pages/AddMachine' import AddMachine from 'src/pages/AddMachine'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { errorColor } from 'src/styling/variables'
import styles from './Dashboard.styles' import styles from './Dashboard.styles'
import Footer from './Footer' import Footer from './Footer'
@ -46,20 +46,20 @@ const Dashboard = () => {
<> <>
<TitleSection title="Dashboard"> <TitleSection title="Dashboard">
<div className={classes.headerLabels}> <div className={classes.headerLabels}>
<> <div>
<div
className={classnames(
classes.headerLabelContainer,
classes.headerLabelContainerMargin
)}>
<TxOutIcon />
<span className={classes.headerLabelSpan}>Cash-out</span>
</div>
<div className={classes.headerLabelContainer}>
<TxInIcon /> <TxInIcon />
<span className={classes.headerLabelSpan}>Cash-in</span> <span>Cash-in</span>
</div>
<div>
<TxOutIcon />
<span>Cash-out</span>
</div>
<div>
<svg width={12} height={12}>
<rect width={12} height={12} rx={3} fill={errorColor} />
</svg>
<span>Action Required</span>
</div> </div>
</>
</div> </div>
</TitleSection> </TitleSection>
<div className={classes.root}> <div className={classes.root}>

View file

@ -12,18 +12,26 @@ const { label1 } = typographyStyles
const styles = { const styles = {
headerLabels: { headerLabels: {
display: 'flex', display: 'flex',
flexDirection: 'row' flexDirection: 'row',
}, '& > div:first-child': {
headerLabelContainerMargin: {
marginRight: 24
},
headerLabelContainer: {
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
marginLeft: 0
}, },
headerLabelSpan: { '& > div': {
display: 'flex',
alignItems: 'center',
marginLeft: 25
},
'& > div:last-child': {
display: 'flex',
alignItems: 'center',
marginLeft: 64
},
'& > div > span': {
extend: label1, extend: label1,
marginLeft: 6 marginLeft: 7
}
}, },
root: { root: {
flexGrow: 1, flexGrow: 1,

View file

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
@ -27,19 +28,15 @@ const GET_DATA = gql`
} }
} }
` `
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP }) BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const Footer = () => { const Footer = () => {
const { data } = useQuery(GET_DATA) const { data } = useQuery(GET_DATA)
const [expanded, setExpanded] = useState(false)
const [delayedExpand, setDelayedExpand] = useState(null)
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {} const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
const classes = useStyles({ const classes = useStyles()
bigFooter: R.keys(withCommissions).length > 8,
expanded
})
const config = R.path(['config'])(data) ?? {} const config = R.path(['config'])(data) ?? {}
const canExpand = R.keys(withCommissions).length > 4 const canExpand = R.keys(withCommissions).length > 4
@ -99,31 +96,16 @@ const Footer = () => {
) )
} }
const handleMouseEnter = () => {
setDelayedExpand(setTimeout(() => canExpand && setExpanded(true), 300))
}
const handleMouseLeave = () => {
clearTimeout(delayedExpand)
setExpanded(false)
}
return ( return (
<> <div className={classes.footer1}>
<div <div className={classes.content1}>
className={classes.mouseWatcher} <Grid container>
onMouseLeave={handleMouseLeave} <Grid container className={classes.footerContainer1}>
onMouseEnter={handleMouseEnter}
/>
<div className={classes.content}>
<Grid container spacing={1}>
<Grid container className={classes.footerContainer}>
{R.keys(withCommissions).map(key => renderFooterItem(key))} {R.keys(withCommissions).map(key => renderFooterItem(key))}
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
<div className={classes.footer} /> </div>
</>
) )
} }

View file

@ -17,52 +17,34 @@ const styles = {
txOutMargin: { txOutMargin: {
marginLeft: spacer * 3 marginLeft: spacer * 3
}, },
footer: ({ expanded, bigFooter }) => ({ tickerLabel: {
height: color: offColor,
expanded && bigFooter marginTop: -5
? spacer * 12 * 3 + spacer * 3 },
: expanded footer1: {
? spacer * 12 * 2 + spacer * 2
: spacer * 12,
left: 0, left: 0,
bottom: 0, bottom: 0,
position: 'fixed', position: 'fixed',
width: '100vw', width: '100vw',
backgroundColor: white, backgroundColor: white,
textAlign: 'left', textAlign: 'left',
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)'
}),
tickerLabel: {
color: offColor,
marginTop: -5
},
content: {
width: 1200,
backgroundColor: white,
zIndex: 1, zIndex: 1,
position: 'fixed', boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)',
bottom: -spacer, minHeight: spacer * 12,
transform: 'translateY(-100%)' transition: 'min-height 0.5s ease-out',
'&:hover': {
transition: 'min-height 0.5s ease-in',
minHeight: 200
}
}, },
footerContainer: ({ expanded, bigFooter }) => ({ content1: {
marginLeft: spacer * 5, width: 1200,
height: 100, maxHeight: 100,
marginTop: expanded && bigFooter ? -300 : expanded ? -200 : -100, backgroundColor: white,
overflow: !expanded && 'hidden' zIndex: 2,
}), bottom: -spacer,
mouseWatcher: ({ expanded, bigFooter }) => ({ margin: '0 auto'
position: 'fixed', }
bottom: 0,
left: 0,
width: '100vw',
height:
expanded && bigFooter
? spacer * 12 * 3 + spacer * 3
: expanded
? spacer * 12 * 2 + spacer * 2
: spacer * 12,
zIndex: 2
})
} }
export default styles export default styles

View file

@ -8,11 +8,10 @@ import { java, neon, white } from 'src/styling/variables'
const styles = { const styles = {
wrapper: { wrapper: {
display: 'flex', display: 'flex',
height: 130, height: 142
marginTop: -8
}, },
percentageBox: { percentageBox: {
height: 130, height: 142,
borderRadius: 4, borderRadius: 4,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -33,11 +32,11 @@ const styles = {
borderRadius: 2 borderRadius: 2
}, },
inWidth: { inWidth: {
width: value => `${value}%` width: value => `${value}%`,
marginRight: value => (value === 100 ? 0 : 4)
}, },
outWidth: { outWidth: {
width: value => `${100 - value}%`, width: value => `${100 - value}%`
marginRight: 4
} }
} }
@ -59,14 +58,6 @@ const PercentageChart = ({ cashIn, cashOut }) => {
return ( return (
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div
className={classnames(
percentageClasses,
classes.outColor,
classes.outWidth
)}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
<div <div
className={classnames( className={classnames(
percentageClasses, percentageClasses,
@ -75,6 +66,14 @@ const PercentageChart = ({ cashIn, cashOut }) => {
)}> )}>
{buildPercentageView(value, 'cashIn')} {buildPercentageView(value, 'cashIn')}
</div> </div>
<div
className={classnames(
percentageClasses,
classes.outColor,
classes.outWidth
)}>
{buildPercentageView(100 - value, 'cashOut')}
</div>
</div> </div>
) )
} }

View file

@ -45,7 +45,7 @@ const RefLineChart = ({
const svg = d3.select(svgRef.current) const svg = d3.select(svgRef.current)
const margin = { top: 0, right: 0, bottom: 0, left: 0 } const margin = { top: 0, right: 0, bottom: 0, left: 0 }
const width = 336 - margin.left - margin.right const width = 336 - margin.left - margin.right
const height = 128 - margin.top - margin.bottom const height = 140 - margin.top - margin.bottom
const massageData = () => { const massageData = () => {
// if we're viewing transactions for the past day, then we group by hour. If not, we group by day // if we're viewing transactions for the past day, then we group by hour. If not, we group by day
@ -148,7 +148,7 @@ const RefLineChart = ({
const y = d3 const y = d3
.scaleLinear() .scaleLinear()
// 30 is a margin so that the labels and the percentage change label can fit and not overlay the line path // 30 is a margin so that the labels and the percentage change label can fit and not overlay the line path
.range([height, 30]) .range([height, 40])
.domain([0, yDomain[1]]) .domain([0, yDomain[1]])
const x = d3 const x = d3
.scaleTime() .scaleTime()

View file

@ -1,197 +1,357 @@
import BigNumber from 'bignumber.js'
import * as d3 from 'd3' import * as d3 from 'd3'
import { add } from 'date-fns/fp' import { getTimezoneOffset } from 'date-fns-tz'
import React, { useEffect, useRef, useCallback } from 'react' import { add, format, startOfWeek, startOfYear } from 'date-fns/fp'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { backgroundColor, java, neon } from 'src/styling/variables' import {
import { formatDate, toUtc } from 'src/utils/timezones' java,
neon,
subheaderDarkColor,
offColor,
fontSecondary,
backgroundColor
} from 'src/styling/variables'
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
const RefScatterplot = ({ data: realData, timeFrame, timezone }) => { const Graph = ({ data, timeFrame, timezone }) => {
const svgRef = useRef() const ref = useRef(null)
const drawGraph = useCallback(() => {
const svg = d3.select(svgRef.current) const GRAPH_HEIGHT = 250
const margin = { top: 25, right: 0, bottom: 25, left: 15 } const GRAPH_WIDTH = 555
const width = 555 - margin.left - margin.right const GRAPH_MARGIN = useMemo(
const height = 150 - margin.top - margin.bottom () => ({
// finds maximum value for the Y axis. Minimum value is 100. If value is multiple of 1000, add 100 top: 20,
// (this is because the Y axis looks best with multiples of 100) right: 0.5,
const findMaxY = () => { bottom: 27,
if (realData.length === 0) return 100 left: 43.5
const maxvalueTx = }),
100 * Math.ceil(d3.max(realData, t => parseFloat(t.fiat)) / 100) []
const maxY = Math.max(100, maxvalueTx) )
if (maxY % 1000 === 0) return maxY + 100
return maxY const offset = getTimezoneOffset(timezone)
const NOW = Date.now() + offset
const periodDomains = {
Day: [NOW - DAY, NOW],
Week: [NOW - WEEK, NOW],
Month: [NOW - MONTH, NOW]
} }
const timeFormat = v => { const dataPoints = useMemo(
switch (timeFrame) { () => ({
case 'Week': Day: {
return d3.timeFormat('%a %d')(v) freq: 24,
case 'Month': step: 60 * 60 * 1000,
return d3.timeFormat('%b %d')(v) tick: d3.utcHour.every(4),
default: labelFormat: '%H:%M'
return formatDate(v, timezone, 'HH:mm') },
} 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(2),
labelFormat: '%d'
} }
}),
[]
)
const filterDay = useMemo(
x => (timeFrame === 'day' ? x.getUTCHours() === 0 : x.getUTCDate() === 1),
[timeFrame]
)
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())))
)
// changes values of arguments in some d3 function calls to make the graph labels look good according to the selected time frame
const findXAxisSettings = () => {
switch (timeFrame) {
case 'Week':
return { return {
nice: 7, previous:
ticks: 7, currentDateMonth !== previousDateMonth
subtractDays: 7, ? months[previousDateMonth]
timeRange: [50, 500] : `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
} current:
case 'Month': currentDateMonth !== previousDateMonth
return { ? months[currentDateMonth]
nice: 6, : `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
ticks: 6,
subtractDays: 30,
timeRange: [50, 500]
}
default:
return {
nice: null,
ticks: 4,
subtractDays: 1,
timeRange: [50, 500]
}
} }
}, [])
const buildTicks = useCallback(
domain => {
const points = []
const roundDate = d => {
const step = dataPoints[timeFrame].step
return new Date(Math.ceil(d.valueOf() / step) * step)
} }
// sets width of the graph for (let i = 0; i <= dataPoints[timeFrame].freq; i++) {
svg.attr('width', width) const stepDate = new Date(NOW - i * dataPoints[timeFrame].step)
if (roundDate(stepDate) > domain[1]) continue
if (stepDate < domain[0]) continue
points.push(roundDate(stepDate))
}
// background color for the graph return points
svg },
.append('rect') [NOW, dataPoints, timeFrame]
.attr('x', 0) )
.attr('y', 0)
.attr('width', width)
.attr('height', height + margin.top)
.attr('fill', backgroundColor)
// declare g variable where more svg components will be attached const x = d3
const g = svg .scaleUtc()
.append('g') .domain(periodDomains[timeFrame])
.attr('transform', `translate(${margin.left},${margin.top})`) .range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
// y axis range: round up to 100 highest data value, if rounds up to 1000, add 100.
// this keeps the vertical axis nice looking
const maxY = findMaxY()
const xAxisSettings = findXAxisSettings()
// y and x scales
const y = d3 const y = d3
.scaleLinear() .scaleLinear()
.range([height, 0])
.domain([0, maxY])
.nice(3)
const x = d3
.scaleTime()
.domain([ .domain([
add({ days: -xAxisSettings.subtractDays }, new Date()).valueOf(), 0,
new Date().valueOf() (d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.05
]) ])
.range(xAxisSettings.timeRange) .nice()
.nice(xAxisSettings.nice) .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
const timeValue = s => { const buildBackground = useCallback(
const date = toUtc(s) g => {
return x(date.valueOf()) g.append('rect')
} .attr('x', 0)
.attr('y', GRAPH_MARGIN.top)
// horizontal gridlines .attr('width', GRAPH_WIDTH)
const makeYGridlines = () => { .attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.top - GRAPH_MARGIN.bottom)
return d3.axisLeft(y).ticks(4) .attr('fill', backgroundColor)
} },
g.append('g') [GRAPH_MARGIN]
.style('color', '#eef1ff')
.call(
makeYGridlines()
.tickSize(-width)
.tickFormat('')
) )
const buildXAxis = useCallback(
g =>
g
.attr(
'transform',
`translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
)
.call(
d3
.axisBottom(x)
.ticks(dataPoints[timeFrame].tick)
.tickFormat(d => {
return d3.timeFormat(dataPoints[timeFrame].labelFormat)(
d.getTime() + d.getTimezoneOffset() * MINUTE
)
})
)
.call(g => g.select('.domain').remove()),
[GRAPH_MARGIN, dataPoints, timeFrame, x]
)
const buildYAxis = useCallback(
g =>
g
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
.call(d3.axisLeft(y).ticks(5))
.call(g => g.select('.domain').remove()) .call(g => g.select('.domain').remove())
/* X AXIS */
// this one is for the labels at the bottom
g.append('g')
.attr('transform', 'translate(0,' + height + ')')
.style('font-size', '13px')
.style('color', '#5f668a')
.style('font-family', 'MuseoSans')
.style('margin-top', '11px')
.call(
d3
.axisBottom(x)
.ticks(xAxisSettings.ticks)
.tickSize(0)
.tickFormat(timeFormat)
)
.selectAll('text') .selectAll('text')
.attr('dy', '1.5em') .attr('dy', '-0.25rem'),
// this is for the x axis line. It is the same color as the horizontal grid lines [GRAPH_MARGIN, y]
g.append('g')
.attr('transform', 'translate(0,' + height + ')')
.style('color', '#eef1ff')
.call(
d3
.axisBottom(x)
.ticks(6)
.tickSize(0)
.tickFormat('')
) )
.selectAll('text')
.attr('dy', '1.5em')
// Y axis const buildGrid = useCallback(
g.append('g') g => {
.style('font-size', '13px') g.attr('stroke', subheaderDarkColor)
.style('color', '#5f668a') .attr('fill', subheaderDarkColor)
.style('font-family', 'MuseoSans') // Vertical lines
.style('margin-top', '11px') .call(g =>
.call( 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)
.attr('stroke-width', 1)
)
// Horizontal lines
.call(g =>
g
.append('g')
.selectAll('line')
.data(
d3 d3
.axisLeft(y) .axisLeft(y)
.ticks(4) .scale()
.tickSize(0) .ticks(5)
) )
.call(g => g.select('.domain').remove()) .join('line')
.selectAll('text') .attr('y1', d => 0.5 + y(d))
.attr('dy', '-0.40em') .attr('y2', d => 0.5 + y(d))
.attr('dx', '3em') .attr('x1', GRAPH_MARGIN.left)
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
// Append dots )
const dots = svg // Thick vertical lines
.call(g =>
g
.append('g') .append('g')
.attr('transform', `translate(${margin.left},${margin.top})`) .selectAll('line')
.data(buildTicks(x.domain()).filter(filterDay))
.join('line')
.attr('class', 'dateSeparator')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', GRAPH_MARGIN.top - 10)
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
.attr('stroke-width', 2)
.join('text')
)
// Left side breakpoint label
.call(g => {
const separator = d3
?.select('.dateSeparator')
?.node()
?.getBBox()
dots if (!separator) return
.selectAll('circle')
.data(realData) const breakpoint = buildTicks(x.domain()).filter(filterDay)
.enter()
.append('circle') const labels = getPastAndCurrentDayLabels(breakpoint)
.attr('cx', d => timeValue(d.created))
.attr('cy', d => y(d.fiat)) return g
.attr('r', 4) .append('text')
.style('fill', d => (d.txClass === 'cashIn' ? java : neon)) .attr('x', separator.x - 7)
}, [realData, timeFrame, timezone]) .attr('y', separator.y)
.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(filterDay)
const labels = getPastAndCurrentDayLabels(breakpoint)
return g
.append('text')
.attr('x', separator.x + 7)
.attr('y', separator.y)
.attr('text-anchor', 'start')
.attr('dy', '.25em')
.text(labels.current)
})
},
[GRAPH_MARGIN, buildTicks, getPastAndCurrentDayLabels, x, y, filterDay]
)
const formatTicksText = useCallback(
() =>
d3
.selectAll('.tick text')
.style('stroke', offColor)
.style('fill', offColor)
.style('stroke-width', 0)
.style('font-family', fontSecondary),
[]
)
const formatText = useCallback(
() =>
d3
.selectAll('text')
.style('stroke', offColor)
.style('fill', offColor)
.style('stroke-width', 0)
.style('font-family', fontSecondary),
[]
)
const formatTicks = useCallback(() => {
d3.selectAll('.tick line')
.style('stroke', 'transparent')
.style('fill', 'transparent')
}, [])
const drawData = useCallback(
g => {
g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => {
const created = new Date(d.created)
return x(created.setTime(created.getTime() + offset))
})
.attr('cy', d => y(new BigNumber(d.fiat).toNumber()))
.attr('fill', d => (d.txClass === 'cashIn' ? java : neon))
.attr('r', 3.5)
},
[data, offset, x, y]
)
const drawChart = useCallback(() => {
const svg = d3
.select(ref.current)
.attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
svg.append('g').call(buildBackground)
svg.append('g').call(buildGrid)
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)
svg.append('g').call(drawData)
return svg.node()
}, [
buildBackground,
buildGrid,
buildXAxis,
buildYAxis,
drawData,
formatText,
formatTicks,
formatTicksText
])
useEffect(() => { useEffect(() => {
// first we clear old chart DOM elements on component update d3.select(ref.current)
d3.select(svgRef.current)
.selectAll('*') .selectAll('*')
.remove() .remove()
drawGraph() drawChart()
}, [drawGraph]) }, [drawChart])
return ( return <svg ref={ref} />
<>
<svg ref={svgRef} />
</>
)
} }
export default RefScatterplot
export default Graph

View file

@ -9,11 +9,13 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { EmptyTable } from 'src/components/table' import { EmptyTable } from 'src/components/table'
import { Label1, Label2 } from 'src/components/typography/index' import { Label1, Label2, P } from 'src/components/typography/index'
import { ReactComponent as PercentDownIcon } from 'src/styling/icons/dashboard/down.svg' import { ReactComponent as PercentDownIcon } from 'src/styling/icons/dashboard/down.svg'
import { ReactComponent as PercentNeutralIcon } from 'src/styling/icons/dashboard/equal.svg' import { ReactComponent as PercentNeutralIcon } from 'src/styling/icons/dashboard/equal.svg'
import { ReactComponent as PercentUpIcon } from 'src/styling/icons/dashboard/up.svg' import { ReactComponent as PercentUpIcon } from 'src/styling/icons/dashboard/up.svg'
import { java, neon } from 'src/styling/variables'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import { timezones } from 'src/utils/timezone-list'
import { toTimezone } from 'src/utils/timezones' import { toTimezone } from 'src/utils/timezones'
import PercentageChart from './Graphs/PercentageChart' import PercentageChart from './Graphs/PercentageChart'
@ -199,9 +201,30 @@ const SystemPerformance = () => {
</Grid> </Grid>
{/* todo new customers */} {/* todo new customers */}
</Grid> </Grid>
<Grid container className={classes.gridContainer}> <Grid container className={classes.txGraphContainer}>
<Grid item xs={12}> <Grid item xs={12}>
<Label2>Transactions</Label2> <div className={classes.graphHeader}>
<Label2 noMargin>Transactions</Label2>
<div className={classes.labelWrapper}>
<P noMargin>
{timezones[timezone].short ?? timezones[timezone].long}{' '}
timezone
</P>
<span className={classes.verticalLine} />
<div>
<svg width={8} height={8}>
<rect width={8} height={8} rx={4} fill={java} />
</svg>
<Label1 noMargin>In</Label1>
</div>
<div>
<svg width={8} height={8}>
<rect width={8} height={8} rx={4} fill={neon} />
</svg>
<Label1 noMargin>Out</Label1>
</div>
</div>
</div>
<Scatterplot <Scatterplot
timeFrame={selectedRange} timeFrame={selectedRange}
data={transactionsToShow} data={transactionsToShow}
@ -209,9 +232,9 @@ const SystemPerformance = () => {
/> />
</Grid> </Grid>
</Grid> </Grid>
<Grid container className={classes.gridContainer}> <Grid container className={classes.commissionGraphContainer}>
<Grid item xs={8}> <Grid item xs={8}>
<Label2 className={classes.labelMargin}> <Label2 noMargin className={classes.commissionProfitTitle}>
Profit from commissions Profit from commissions
</Label2> </Label2>
<div className={classes.profitContainer}> <div className={classes.profitContainer}>
@ -233,23 +256,22 @@ const SystemPerformance = () => {
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
<Grid container> <Grid container className={classes.graphHeader}>
<Grid item> <Label2 noMargin>Direction</Label2>
<Label2 className={classes.labelMargin}>Direction</Label2> <div className={classes.labelWrapper}>
</Grid> <div>
<Grid <svg width={8} height={8}>
item <rect width={8} height={8} rx={2} fill={java} />
className={classnames( </svg>
classes.directionLabelContainer, <Label1 noMargin>In</Label1>
classes.dirLabContMargin </div>
)}> <div>
<div className={classes.outSquare} /> <svg width={8} height={8}>
<Label1 className={classes.directionLabel}>Out</Label1> <rect width={8} height={8} rx={2} fill={neon} />
</Grid> </svg>
<Grid item className={classes.directionLabelContainer}> <Label1 noMargin>Out</Label1>
<div className={classes.inSquare} /> </div>
<Label1 className={classes.directionLabel}>In</Label1> </div>
</Grid>
</Grid> </Grid>
<Grid item xs> <Grid item xs>
<PercentageChart <PercentageChart

View file

@ -1,5 +1,6 @@
import { import {
offColor, offColor,
offDarkColor,
spacer, spacer,
primaryColor, primaryColor,
fontSize3, fontSize3,
@ -7,8 +8,6 @@ import {
fontColor, fontColor,
spring4, spring4,
tomato, tomato,
java,
neon,
comet comet
} from 'src/styling/variables' } from 'src/styling/variables'
@ -67,12 +66,6 @@ const styles = {
navContainer: { navContainer: {
display: 'flex' display: 'flex'
}, },
profitLabel: {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 700,
color: fontColor
},
percentUp: { percentUp: {
fontSize: fontSize3, fontSize: fontSize3,
fontFamily: fontSecondary, fontFamily: fontSecondary,
@ -96,34 +89,14 @@ const styles = {
profitContainer: { profitContainer: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
margin: '0 26px -30px 16px', margin: '23px 26px -30px 16px',
position: 'relative' position: 'relative'
}, },
gridContainer: { profitLabel: {
marginTop: 30, fontSize: fontSize3,
height: 225 fontFamily: fontSecondary,
}, fontWeight: 700,
inSquare: { color: fontColor
width: 8,
height: 8,
borderRadius: 2,
marginTop: 18,
marginRight: 4,
backgroundColor: java
},
outSquare: {
width: 8,
height: 8,
borderRadius: 2,
marginTop: 18,
marginRight: 4,
backgroundColor: neon
},
directionLabelContainer: {
display: 'flex'
},
dirLabContMargin: {
marginRight: 20
}, },
directionIcon: { directionIcon: {
width: 16, width: 16,
@ -131,12 +104,50 @@ const styles = {
marginBottom: -2, marginBottom: -2,
marginRight: 4 marginRight: 4
}, },
labelMargin: {
marginBottom: 20,
marginRight: 32
},
emptyTransactions: { emptyTransactions: {
paddingTop: 40 paddingTop: 40
},
commissionProfitTitle: {
marginBottom: 16
},
graphHeader: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16
},
labelWrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'& > div': {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginLeft: 15,
'&:first-child': {
marginLeft: 0
},
'& > p': {
marginLeft: 8
}
}
},
txGraphContainer: {
height: 300,
marginTop: 30
},
commissionsGraphContainer: {
height: 250,
marginTop: 30
},
verticalLine: {
height: 15,
width: 1,
backgroundColor: offDarkColor,
marginLeft: 31,
marginRight: 16
} }
} }

View file

@ -212,7 +212,9 @@ const Funding = () => {
<div className={classes.addressWrapper}> <div className={classes.addressWrapper}>
<div className={classes.mono}> <div className={classes.mono}>
<strong> <strong>
<CopyToClipboard buttonClassname={classes.copyToClipboard}> <CopyToClipboard
buttonClassname={classes.copyToClipboard}
key={selected.cryptoCode}>
{formatAddress( {formatAddress(
selected.cryptoCode, selected.cryptoCode,
selected.fundingAddress selected.fundingAddress

View file

@ -2,7 +2,7 @@ import * as R from 'ramda'
import * as Yup from 'yup' import * as Yup from 'yup'
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js' import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
import timezoneList from 'src/utils/timezone-list' import { labels as timezoneList } from 'src/utils/timezone-list'
const getFields = (getData, names, onChange, auxElements = []) => { const getFields = (getData, names, onChange, auxElements = []) => {
return R.filter( return R.filter(

View file

@ -8,7 +8,6 @@ import { DeleteDialog } from 'src/components/DeleteDialog'
import { Link, Button, IconButton } from 'src/components/buttons' import { Link, Button, IconButton } from 'src/components/buttons'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { Label3, TL1 } from 'src/components/typography' import { Label3, TL1 } from 'src/components/typography'
import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg' import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
@ -49,7 +48,6 @@ const GET_CUSTOMERS = gql`
id id
phone phone
idCardData idCardData
phone
} }
} }
` `
@ -64,7 +62,9 @@ const IndividualDiscounts = () => {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const toggleModal = () => setShowModal(!showModal) const toggleModal = () => setShowModal(!showModal)
const { data: discountResponse, loading } = useQuery(GET_INDIVIDUAL_DISCOUNTS) const { data: discountResponse, loading: discountLoading } = useQuery(
GET_INDIVIDUAL_DISCOUNTS
)
const { data: customerData, loading: customerLoading } = useQuery( const { data: customerData, loading: customerLoading } = useQuery(
GET_CUSTOMERS GET_CUSTOMERS
) )
@ -102,12 +102,6 @@ const IndividualDiscounts = () => {
<div className={classes.identification}> <div className={classes.identification}>
<PhoneIdIcon /> <PhoneIdIcon />
<span>{customer.phone}</span> <span>{customer.phone}</span>
{customer?.idCardData?.documentNumber && (
<>
<CardIdIcon />
<span>{customer?.idCardData?.documentNumber}</span>
</>
)}
</div> </div>
) )
} }
@ -160,11 +154,12 @@ const IndividualDiscounts = () => {
} }
] ]
const isLoading = loading || customerLoading const loading = discountLoading || customerLoading
return ( return (
<> <>
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && ( {!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
<>
<Box <Box
marginBottom={4} marginBottom={4}
marginTop={-7} marginTop={-7}
@ -175,9 +170,6 @@ const IndividualDiscounts = () => {
Add new code Add new code
</Link> </Link>
</Box> </Box>
)}
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
<>
<DataTable <DataTable
elements={elements} elements={elements}
data={R.path(['individualDiscounts'])(discountResponse)} data={R.path(['individualDiscounts'])(discountResponse)}
@ -196,7 +188,7 @@ const IndividualDiscounts = () => {
/> />
</> </>
)} )}
{!isLoading && R.isEmpty(discountResponse.individualDiscounts) && ( {!loading && R.isEmpty(discountResponse.individualDiscounts) && (
<Box display="flex" alignItems="left" flexDirection="column"> <Box display="flex" alignItems="left" flexDirection="column">
<Label3> <Label3>
It seems there are no active individual customer discounts on your It seems there are no active individual customer discounts on your

View file

@ -81,16 +81,21 @@ const Logs = () => {
const deviceId = selected?.deviceId const deviceId = selected?.deviceId
const { data: machineResponse } = useQuery(GET_MACHINES) const { data: machineResponse, loading: machinesLoading } = useQuery(
GET_MACHINES
)
const { data: configResponse } = useQuery(GET_DATA) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const { data: logsResponse, loading } = useQuery(GET_MACHINE_LOGS, { const { data: logsResponse, loading: logsLoading } = useQuery(
GET_MACHINE_LOGS,
{
variables: { deviceId, limit: NUM_LOG_RESULTS }, variables: { deviceId, limit: NUM_LOG_RESULTS },
skip: !selected, skip: !selected,
onCompleted: () => setSaveMessage('') onCompleted: () => setSaveMessage('')
}) }
)
if (machineResponse?.machines?.length && !selected) { if (machineResponse?.machines?.length && !selected) {
setSelected(machineResponse?.machines[0]) setSelected(machineResponse?.machines[0])
@ -100,6 +105,8 @@ const Logs = () => {
return R.path(['deviceId'])(selected) === it.deviceId return R.path(['deviceId'])(selected) === it.deviceId
} }
const loading = machinesLoading || configLoading || logsLoading
return ( return (
<> <>
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>

View file

@ -26,7 +26,7 @@ const widthsByNumberOfCassettes = {
const ValidationSchema = Yup.object().shape({ const ValidationSchema = Yup.object().shape({
name: Yup.string().required('Required'), name: Yup.string().required('Required'),
cashbox: Yup.number() cashbox: Yup.number()
.label('Cashbox') .label('Cash box')
.required() .required()
.integer() .integer()
.min(0) .min(0)
@ -82,7 +82,7 @@ const SET_CASSETTE_BILLS = gql`
} }
` `
const CashCassettes = ({ machine, config, refetchData }) => { const CashCassettes = ({ machine, config, refetchData, bills }) => {
const classes = useStyles() const classes = useStyles()
const [wizard, setWizard] = useState(false) const [wizard, setWizard] = useState(false)
@ -101,11 +101,15 @@ const CashCassettes = ({ machine, config, refetchData }) => {
const elements = [ const elements = [
{ {
name: 'cashbox', name: 'cashbox',
header: 'Cashbox', header: 'Cash box',
width: widthsByNumberOfCassettes[numberOfCassettes].cashbox, width: widthsByNumberOfCassettes[numberOfCassettes].cashbox,
stripe: false, stripe: false,
view: value => ( view: value => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} /> <CashIn
currency={{ code: fiatCurrency }}
notes={value}
total={R.sum(R.map(it => it.fiat)(bills))}
/>
), ),
input: NumberInput, input: NumberInput,
inputProps: { inputProps: {
@ -119,7 +123,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
it => { it => {
elements.push({ elements.push({
name: `cassette${it}`, name: `cassette${it}`,
header: `Cash-out ${it}`, header: `Cash cassette ${it}`,
width: widthsByNumberOfCassettes[numberOfCassettes].cassette, width: widthsByNumberOfCassettes[numberOfCassettes].cassette,
stripe: true, stripe: true,
doubleHeader: 'Cash-out', doubleHeader: 'Cash-out',

View file

@ -82,7 +82,7 @@ const Transactions = ({ id }) => {
const { data: configData, loading: configLoading } = useQuery(GET_DATA) const { data: configData, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configData) const timezone = R.path(['config', 'locale_timezone'], configData)
const loading = txLoading && configLoading const loading = txLoading || configLoading
if (!loading && txResponse) { if (!loading && txResponse) {
txResponse.transactions = txResponse.transactions.splice(0, 5) txResponse.transactions = txResponse.transactions.splice(0, 5)

View file

@ -93,13 +93,14 @@ const MachineRoute = () => {
) )
} }
const Machines = ({ data, refetch, reload, bills }) => { const Machines = ({ data, refetch, reload }) => {
const classes = useStyles() const classes = useStyles()
const timezone = R.path(['config', 'locale_timezone'], data) ?? {} const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
const machine = R.path(['machine'])(data) ?? {} const machine = R.path(['machine'])(data) ?? {}
const config = R.path(['config'])(data) ?? {} const config = R.path(['config'])(data) ?? {}
const bills = R.path(['bills'])(data) ?? []
const machineName = R.path(['name'])(machine) ?? null const machineName = R.path(['name'])(machine) ?? null
const machineID = R.path(['deviceId'])(machine) ?? null const machineID = R.path(['deviceId'])(machine) ?? null

View file

@ -31,7 +31,7 @@ const useStyles = makeStyles(styles)
const ValidationSchema = Yup.object().shape({ const ValidationSchema = Yup.object().shape({
name: Yup.string().required(), name: Yup.string().required(),
cashbox: Yup.number() cashbox: Yup.number()
.label('Cashbox') .label('Cash box')
.required() .required()
.integer() .integer()
.min(0) .min(0)
@ -63,7 +63,7 @@ const ValidationSchema = Yup.object().shape({
}) })
const GET_MACHINES_AND_CONFIG = gql` const GET_MACHINES_AND_CONFIG = gql`
query getData { query getData($billFilters: JSONObject) {
machines { machines {
name name
id: deviceId id: deviceId
@ -75,24 +75,21 @@ const GET_MACHINES_AND_CONFIG = gql`
numberOfCassettes numberOfCassettes
} }
config config
bills(filters: $billFilters) {
id
fiat
created
deviceId
}
} }
` `
const SAVE_CONFIG = gql` const SAVE_CONFIG = gql`
mutation Save($config: JSONObject) { mutation Save($config: JSONObject) {
saveConfig(config: $config) saveConfig(config: $config)
} }
` `
/*
// for cash in total calculation
bills {
fiat
deviceId
created
cashbox
}
*/
const SET_CASSETTE_BILLS = gql` const SET_CASSETTE_BILLS = gql`
mutation MachineAction( mutation MachineAction(
$deviceId: ID! $deviceId: ID!
@ -128,7 +125,13 @@ const CashCassettes = () => {
const [editingSchema, setEditingSchema] = useState(null) const [editingSchema, setEditingSchema] = useState(null)
const [selectedRadio, setSelectedRadio] = useState(null) const [selectedRadio, setSelectedRadio] = useState(null)
const { data } = useQuery(GET_MACHINES_AND_CONFIG) const { data, loading: dataLoading } = useQuery(GET_MACHINES_AND_CONFIG, {
variables: {
billFilters: {
batch: 'none'
}
}
})
const [wizard, setWizard] = useState(false) const [wizard, setWizard] = useState(false)
const [machineId, setMachineId] = useState('') const [machineId, setMachineId] = useState('')
@ -204,10 +207,14 @@ const CashCassettes = () => {
}, },
{ {
name: 'cashbox', name: 'cashbox',
header: 'Cash-in', header: 'Cash box',
width: maxNumberOfCassettes > 2 ? 140 : 280, width: maxNumberOfCassettes > 2 ? 140 : 280,
view: value => ( view: (value, { id }) => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} /> <CashIn
currency={{ code: fiatCurrency }}
notes={value}
total={R.sum(R.map(it => it.fiat, bills[id] ?? []))}
/>
), ),
input: NumberInput, input: NumberInput,
inputProps: { inputProps: {
@ -222,7 +229,7 @@ const CashCassettes = () => {
elements.push({ elements.push({
name: `cassette${it}`, name: `cassette${it}`,
header: `Cassette ${it}`, header: `Cassette ${it}`,
width: (maxNumberOfCassettes > 2 ? 700 : 560) / maxNumberOfCassettes, width: (maxNumberOfCassettes > 2 ? 560 : 650) / maxNumberOfCassettes,
stripe: true, stripe: true,
doubleHeader: 'Cash-out', doubleHeader: 'Cash-out',
view: (value, { id }) => ( view: (value, { id }) => (
@ -268,11 +275,12 @@ const CashCassettes = () => {
}) })
return ( return (
!dataLoading && (
<> <>
<TitleSection <TitleSection
title="Cash Cassettes" title="Cash Boxes & Cassettes"
button={{ button={{
text: 'Cashbox history', text: 'Cash box history',
icon: HistoryIcon, icon: HistoryIcon,
inverseIcon: ReverseHistoryIcon, inverseIcon: ReverseHistoryIcon,
toggle: setShowHistory toggle: setShowHistory
@ -281,11 +289,11 @@ const CashCassettes = () => {
className={classes.tableWidth}> className={classes.tableWidth}>
{!showHistory && ( {!showHistory && (
<Box alignItems="center" justifyContent="flex-end"> <Box alignItems="center" justifyContent="flex-end">
<Label1 className={classes.cashboxReset}>Cashbox reset</Label1> <Label1 className={classes.cashboxReset}>Cash box resets</Label1>
<Box <Box
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="flex-end" justifyContent="end"
mr="-4px"> mr="-4px">
{cashboxReset && ( {cashboxReset && (
<P className={classes.selection}> <P className={classes.selection}>
@ -327,12 +335,12 @@ const CashCassettes = () => {
currencyCode={fiatCurrency} currencyCode={fiatCurrency}
machines={machines} machines={machines}
config={config} config={config}
bills={bills} bills={R.path(['bills'])(data)}
deviceIds={deviceIds} deviceIds={deviceIds}
/> />
{wizard && ( {wizard && (
<Wizard <Wizard
machine={R.find(R.propEq('id', machineId))(machines)} machine={R.find(R.propEq('id', machineId), machines)}
cashoutSettings={getCashoutSettings(machineId)} cashoutSettings={getCashoutSettings(machineId)}
onClose={() => { onClose={() => {
setWizard(false) setWizard(false)
@ -344,13 +352,13 @@ const CashCassettes = () => {
)} )}
{editingSchema && ( {editingSchema && (
<Modal <Modal
title={'Cashbox reset'} title={'Cash box resets'}
width={478} width={478}
handleClose={() => setEditingSchema(null)} handleClose={() => setEditingSchema(null)}
open={true}> open={true}>
<P className={classes.descriptions}> <P className={classes.descriptions}>
Specify if you want your cash-in counts to be reset automatically or We can automatically assume you emptied a bill validator's cash
manually. box when the machine detects that it has been removed.
</P> </P>
<RadioGroup <RadioGroup
name="set-automatic-reset" name="set-automatic-reset"
@ -360,8 +368,9 @@ const CashCassettes = () => {
className={classes.radioButtons} className={classes.radioButtons}
/> />
<P className={classes.descriptions}> <P className={classes.descriptions}>
Choose this option if you want your cash-in cashbox count to be Assume the cash box is emptied whenever it's removed, creating a
reset automatically when it is physically removed from the machine. new batch on the history screen and setting its current balance to
zero.
</P> </P>
<RadioGroup <RadioGroup
name="set-manual-reset" name="set-manual-reset"
@ -371,9 +380,9 @@ const CashCassettes = () => {
className={classes.radioButtons} className={classes.radioButtons}
/> />
<P className={classes.descriptions}> <P className={classes.descriptions}>
Choose this option if you want to edit your cash-in counts manually Cash boxes won't be assumed emptied when removed, nor their counts
on Lamassu Admin, after you physically remove the bills from the modified. Instead, to update the count and create a new batch,
cashbox. you'll click the 'Edit' button on this panel.
</P> </P>
<DialogActions className={classes.actions}> <DialogActions className={classes.actions}>
<Button onClick={() => saveCashboxOption(selectedRadio)}> <Button onClick={() => saveCashboxOption(selectedRadio)}>
@ -384,6 +393,7 @@ const CashCassettes = () => {
)} )}
</> </>
) )
)
} }
export default CashCassettes export default CashCassettes

View file

@ -1,20 +1,17 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
// import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import { Info1, Info2, Info3 } from 'src/components/typography/index' import { Info1, Info2, Info3 } from 'src/components/typography/index'
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import { numberToFiatAmount } from 'src/utils/number.js'
import styles from './CashCassettesFooter.styles.js' import styles from './CashCassettesFooter.styles.js'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
/* const sortDate = function(a, b) {
return new Date(b.created).getTime() - new Date(a.created).getTime()
} */
const CashCassettesFooter = ({ const CashCassettesFooter = ({
machines, machines,
config, config,
@ -43,44 +40,34 @@ const CashCassettesFooter = ({
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines)) const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
/* const totalInCashBox = R.sum( const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
R.flatten(
R.map(id => {
const sliceIdx = R.path([id, 0, 'cashbox'])(bills) ?? 0
return R.map(
R.prop('fiat'),
R.slice(0, sliceIdx, R.sort(sortDate, bills[id] ?? []))
)
}, deviceIds)
)
) */
// const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0) const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
return ( return (
<div className={classes.footerContainer}> <div className={classes.footerContainer}>
<div className={classes.footerContent}> <div className={classes.footerContent}>
<Info3 className={classes.footerLabel}>Cash value in System</Info3> <Info3 className={classes.footerLabel}>Cash value in System</Info3>
{/* <div className={classes.flex}> <div className={classes.flex}>
<TxInIcon className={classes.icon} /> <TxInIcon className={classes.icon} />
<Info2 className={classes.iconLabel}>Cash-in:</Info2> <Info2 className={classes.iconLabel}>Cash-in:</Info2>
<Info1 className={classes.valueDisplay}> <Info1 className={classes.valueDisplay}>
{totalInCashBox} {currencyCode} {numberToFiatAmount(totalInCashBox)} {currencyCode}
</Info1> </Info1>
</div> */} </div>
<div className={classes.flex}> <div className={classes.flex}>
<TxOutIcon className={classes.icon} /> <TxOutIcon className={classes.icon} />
<Info2 className={classes.iconLabel}>Cash-out:</Info2> <Info2 className={classes.iconLabel}>Cash-out:</Info2>
<Info1 className={classes.valueDisplay}> <Info1 className={classes.valueDisplay}>
{totalInCassettes} {currencyCode} {numberToFiatAmount(totalInCassettes)} {currencyCode}
</Info1> </Info1>
</div> </div>
{/* <div className={classes.flex}> <div className={classes.flex}>
<Info2 className={classes.iconLabel}>Total:</Info2> <Info2 className={classes.iconLabel}>Total:</Info2>
<Info1 className={classes.valueDisplay}> <Info1 className={classes.valueDisplay}>
{total} {currencyCode} {numberToFiatAmount(total)} {currencyCode}
</Info1> </Info1>
</div> */} </div>
</div> </div>
</div> </div>
) )

View file

@ -24,9 +24,7 @@ export default {
boxShadow: [[0, -1, 10, 0, 'rgba(50, 50, 50, 0.1)']] boxShadow: [[0, -1, 10, 0, 'rgba(50, 50, 50, 0.1)']]
}, },
flex: { flex: {
display: 'flex', display: 'flex'
// temp marginLeft until cashIn square is enabled
marginLeft: -640
}, },
icon: { icon: {
alignSelf: 'center', alignSelf: 'center',

View file

@ -1,15 +1,16 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState } from 'react' import React from 'react'
import * as Yup from 'yup' // import * as Yup from 'yup'
import { Link, IconButton } from 'src/components/buttons' // import { Link, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs' // import { TextInput } from 'src/components/inputs'
import { NumberInput } from 'src/components/inputs/formik' import { NumberInput } from 'src/components/inputs/formik'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' // import { ReactComponent as EditIconDisabled } from 'src/styling/icons/action/edit/disabled.svg'
// import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { formatDate } from 'src/utils/timezones' import { formatDate } from 'src/utils/timezones'
@ -33,13 +34,13 @@ const GET_BATCHES = gql`
} }
` `
const EDIT_BATCH = gql` /* const EDIT_BATCH = gql`
mutation editBatch($id: ID, $performedBy: String) { mutation editBatch($id: ID, $performedBy: String) {
editBatch(id: $id, performedBy: $performedBy) { editBatch(id: $id, performedBy: $performedBy) {
id id
} }
} }
` ` */
const GET_DATA = gql` const GET_DATA = gql`
query getData { query getData {
@ -63,27 +64,29 @@ const styles = {
} }
} }
const schema = Yup.object().shape({ /* const schema = Yup.object().shape({
performedBy: Yup.string().nullable() performedBy: Yup.string().nullable()
}) }) */
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const CashboxHistory = ({ machines, currency }) => { const CashboxHistory = ({ machines, currency }) => {
const classes = useStyles() const classes = useStyles()
const [error, setError] = useState(false)
const [fields, setFields] = useState([]) /* const [error, setError] = useState(false)
const [field, setField] = useState(null)
const [editing, setEditing] = useState(false) */
const { data: batchesData, loading: batchesLoading } = useQuery(GET_BATCHES) const { data: batchesData, loading: batchesLoading } = useQuery(GET_BATCHES)
const [editBatch] = useMutation(EDIT_BATCH, { /* const [editBatch] = useMutation(EDIT_BATCH, {
refetchQueries: () => ['cashboxBatches'] refetchQueries: () => ['cashboxBatches']
}) }) */
const { data: configData, loading: configLoading } = useQuery(GET_DATA) const { data: configData, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configData) const timezone = R.path(['config', 'locale_timezone'], configData)
const loading = batchesLoading && configLoading const loading = batchesLoading || configLoading
const batches = R.path(['cashboxBatches'])(batchesData) const batches = R.path(['cashboxBatches'])(batchesData)
@ -91,33 +94,36 @@ const CashboxHistory = ({ machines, currency }) => {
(ret, i) => (ret, i) =>
R.pipe( R.pipe(
R.assoc( R.assoc(
`cash-out-${i}-refill`, `cash-cassette-${i}-refill`,
<> <>
<TxOutIcon /> <TxOutIcon />
<span className={classes.operationType}>Cash-out {i} refill</span> <span className={classes.operationType}>
Cash cassette {i} refill
</span>
</> </>
), ),
R.assoc( R.assoc(
`cash-out-${i}-empty`, `cash-cassette-${i}-empty`,
<> <>
<TxOutIcon /> <TxOutIcon />
<span className={classes.operationType}>Cash-out {i} emptied</span> <span className={classes.operationType}>
Cash cassette {i} emptied
</span>
</> </>
) )
)(ret), )(ret),
{ {
'cash-in-empty': ( 'cash-box-empty': (
<> <>
<TxInIcon /> <TxInIcon />
<span className={classes.operationType}>Cash-in emptied</span> <span className={classes.operationType}>Cash box emptied</span>
</> </>
) )
}, },
R.range(1, 5) R.range(1, 5)
) )
const save = row => { /* const save = row => {
const field = R.find(f => f.id === row.id, fields)
const performedBy = field.performedBy === '' ? null : field.performedBy const performedBy = field.performedBy === '' ? null : field.performedBy
schema schema
@ -129,14 +135,15 @@ const CashboxHistory = ({ machines, currency }) => {
}) })
}) })
.catch(setError(true)) .catch(setError(true))
return close(row.id) return close()
} }
const close = id => { const close = () => {
setFields(R.filter(f => f.id !== id, fields)) setEditing(false)
setField(null)
} }
const notEditing = id => !R.any(R.propEq('id', id), fields) const notEditing = id => field?.id !== id */
const elements = [ const elements = [
{ {
@ -174,7 +181,7 @@ const CashboxHistory = ({ machines, currency }) => {
{ {
name: 'total', name: 'total',
header: 'Total', header: 'Total',
width: 100, width: 180,
textAlign: 'right', textAlign: 'right',
view: it => ( view: it => (
<span> <span>
@ -195,8 +202,8 @@ const CashboxHistory = ({ machines, currency }) => {
width: 125, width: 125,
textAlign: 'right', textAlign: 'right',
view: it => formatDate(it.created, timezone, 'HH:mm') view: it => formatDate(it.created, timezone, 'HH:mm')
}, }
{ /* {
name: 'performedBy', name: 'performedBy',
header: 'Performed by', header: 'Performed by',
width: 180, width: 180,
@ -206,21 +213,10 @@ const CashboxHistory = ({ machines, currency }) => {
return R.isNil(it.performedBy) ? 'Unknown entity' : it.performedBy return R.isNil(it.performedBy) ? 'Unknown entity' : it.performedBy
return ( return (
<TextInput <TextInput
onChange={e => onChange={e => setField({ ...field, performedBy: e.target.value })}
setFields(
R.map(
f =>
f.id === it.id ? { ...f, performedBy: e.target.value } : f,
fields
)
)
}
error={error} error={error}
width={190 * 0.85} width={190 * 0.85}
value={R.prop( value={field?.performedBy}
'performedBy',
R.find(f => f.id === it.id, fields)
)}
/> />
) )
} }
@ -228,19 +224,18 @@ const CashboxHistory = ({ machines, currency }) => {
{ {
name: '', name: '',
header: 'Edit', header: 'Edit',
width: 150, width: 80,
textAlign: 'right', textAlign: 'right',
view: it => { view: it => {
if (notEditing(it.id)) if (notEditing(it.id))
return ( return (
<IconButton <IconButton
disabled={editing}
onClick={() => { onClick={() => {
setFields([ setField({ id: it.id, performedBy: it.performedBy })
...fields, setEditing(true)
{ id: it.id, performedBy: it.performedBy }
])
}}> }}>
<EditIcon /> {editing ? <EditIconDisabled /> : <EditIcon />}
</IconButton> </IconButton>
) )
return ( return (
@ -248,26 +243,23 @@ const CashboxHistory = ({ machines, currency }) => {
<Link type="submit" color="primary" onClick={() => save(it)}> <Link type="submit" color="primary" onClick={() => save(it)}>
Save Save
</Link> </Link>
<Link color="secondary" onClick={() => close(it.id)}> <Link color="secondary" onClick={close}>
Cancel Cancel
</Link> </Link>
</div> </div>
) )
} }
} } */
] ]
return ( return (
<>
{!loading && (
<DataTable <DataTable
loading={loading}
name="cashboxHistory" name="cashboxHistory"
elements={elements} elements={elements}
data={batches} data={batches}
emptyText="No cashbox batches so far" emptyText="No cashbox batches so far"
/> />
)}
</>
) )
} }

View file

@ -54,7 +54,11 @@ const MachineStatus = () => {
const history = useHistory() const history = useHistory()
const { state } = useLocation() const { state } = useLocation()
const addedMachineId = state?.id const addedMachineId = state?.id
const { data: machinesResponse, refetch, loading } = useQuery(GET_MACHINES) const {
data: machinesResponse,
refetch,
loading: machinesLoading
} = useQuery(GET_MACHINES)
const { data: configResponse, configLoading } = useQuery(GET_DATA) const { data: configResponse, configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
@ -114,6 +118,8 @@ const MachineStatus = () => {
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} /> <MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
) )
const loading = machinesLoading || configLoading
return ( return (
<> <>
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>
@ -132,7 +138,7 @@ const MachineStatus = () => {
</div> </div>
</div> </div>
<DataTable <DataTable
loading={loading && configLoading} loading={loading}
elements={elements} elements={elements}
data={machines} data={machines}
Details={InnerMachineDetailsRow} Details={InnerMachineDetailsRow}

View file

@ -47,7 +47,6 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
const onContinue = it => { const onContinue = it => {
const newConfig = R.merge(config, it) const newConfig = R.merge(config, it)
if (isLastStep) { if (isLastStep) {
const wasCashboxEmptied = [ const wasCashboxEmptied = [
config?.wasCashboxEmptied, config?.wasCashboxEmptied,

View file

@ -67,7 +67,7 @@ const WizardSplash = ({ name, onContinue }) => {
<div className={classes.warningInfo}> <div className={classes.warningInfo}>
<WarningIcon className={classes.warningIcon} /> <WarningIcon className={classes.warningIcon} />
<P noMargin className={classes.warningText}> <P noMargin className={classes.warningText}>
For cash-out cassettes, please make sure you've removed the remaining For cash cassettes, please make sure you've removed the remaining
bills before adding the new ones. bills before adding the new ones.
</P> </P>
</div> </div>

View file

@ -22,6 +22,7 @@ import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-c
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg' import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { comet, errorColor } from 'src/styling/variables' import { comet, errorColor } from 'src/styling/variables'
import { numberToFiatAmount } from 'src/utils/number'
const styles = { const styles = {
content: { content: {
@ -115,7 +116,8 @@ const WizardStep = ({
lastStep, lastStep,
steps, steps,
fiatCurrency, fiatCurrency,
onContinue onContinue,
initialValues
}) => { }) => {
const classes = useStyles() const classes = useStyles()
@ -168,7 +170,7 @@ const WizardStep = ({
classes.verticalAlign, classes.verticalAlign,
classes.fullWidth classes.fullWidth
)}> )}>
<H4 noMargin>Did you empty the cash-in box?</H4> <H4 noMargin>Did you empty the cash box?</H4>
<Field <Field
component={RadioGroup} component={RadioGroup}
name="wasCashboxEmptied" name="wasCashboxEmptied"
@ -188,8 +190,8 @@ const WizardStep = ({
<P>Since previous update</P> <P>Since previous update</P>
<Tooltip width={215}> <Tooltip width={215}>
<P> <P>
Number of bills inside the cashbox, since the last Number of bills inside the cash box, since the last
cashbox changes. cash box changes.
</P> </P>
</Tooltip> </Tooltip>
</div> </div>
@ -219,12 +221,7 @@ const WizardStep = ({
validateOnBlur={false} validateOnBlur={false}
validateOnChange={false} validateOnChange={false}
onSubmit={onContinue} onSubmit={onContinue}
initialValues={{ initialValues={initialValues}
cassette1: '',
cassette2: '',
cassette3: '',
cassette4: ''
}}
enableReinitialize enableReinitialize
validationSchema={steps[step - 1].schema}> validationSchema={steps[step - 1].schema}>
{({ values, errors }) => ( {({ values, errors }) => (
@ -255,7 +252,7 @@ const WizardStep = ({
<H4 <H4
className={classes.cassetteFormTitleContent} className={classes.cassetteFormTitleContent}
noMargin> noMargin>
Cash-out {step - 1} (dispenser) Cash cassette {step - 1} (dispenser)
</H4> </H4>
</div> </div>
<Cashbox <Cashbox
@ -283,7 +280,8 @@ const WizardStep = ({
</P> </P>
</div> </div>
<P noMargin className={classes.fiatTotal}> <P noMargin className={classes.fiatTotal}>
= {cassetteTotal(values)} {fiatCurrency} = {numberToFiatAmount(cassetteTotal(values))}{' '}
{fiatCurrency}
</P> </P>
</div> </div>
</div> </div>

View file

@ -131,10 +131,10 @@ const FiatBalanceOverrides = ({ section }) => {
it => { it => {
elements.push({ elements.push({
name: `fillingPercentageCassette${it}`, name: `fillingPercentageCassette${it}`,
display: `Cash-out ${it}`, display: `Cash cassette ${it}`,
width: 155, width: 155,
textAlign: 'right', textAlign: 'right',
doubleHeader: 'Cash-out (Cassette Empty)', doubleHeader: 'Cash Cassette Empty',
bold: true, bold: true,
input: NumberInput, input: NumberInput,
suffix: '%', suffix: '%',

View file

@ -98,13 +98,13 @@ const Logs = () => {
const [saveMessage, setSaveMessage] = useState(null) const [saveMessage, setSaveMessage] = useState(null)
const [logLevel, setLogLevel] = useState(SHOW_ALL) const [logLevel, setLogLevel] = useState(SHOW_ALL)
const { data, loading } = useQuery(GET_SERVER_DATA, { const { data, loading: dataLoading } = useQuery(GET_SERVER_DATA, {
onCompleted: () => setSaveMessage(''), onCompleted: () => setSaveMessage(''),
variables: { variables: {
limit: NUM_LOG_RESULTS limit: NUM_LOG_RESULTS
} }
}) })
const { data: configResponse, configLoading } = useQuery(GET_DATA) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const defaultLogLevels = [ const defaultLogLevels = [
@ -132,6 +132,8 @@ const Logs = () => {
setLogLevel(logLevel) setLogLevel(logLevel)
} }
const loading = dataLoading || configLoading
return ( return (
<> <>
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>
@ -206,8 +208,8 @@ const Logs = () => {
))} ))}
</TableBody> </TableBody>
</Table> </Table>
{loading && configLoading && <H4>{'Loading...'}</H4>} {loading && <H4>{'Loading...'}</H4>}
{!loading && !configLoading && !data?.serverLogs?.length && ( {!loading && !data?.serverLogs?.length && (
<H4>{'No activity so far'}</H4> <H4>{'No activity so far'}</H4>
)} )}
</div> </div>

View file

@ -27,7 +27,7 @@ export default {
settings: { settings: {
enabled: true, enabled: true,
disabledMessage: 'RBF verification not available', disabledMessage: 'RBF verification not available',
label: 'Enable RBF verification', label: 'Lower the confidence of RBF transactions',
requirement: 'bitcoind' requirement: 'bitcoind'
}, },
face: true face: true

View file

@ -48,7 +48,7 @@ const SessionManagement = () => {
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
const timezone = R.path(['config', 'locale_timezone'], configResponse) const timezone = R.path(['config', 'locale_timezone'], configResponse)
const loading = sessionsLoading && configLoading const loading = sessionsLoading || configLoading
const elements = [ const elements = [
{ {
@ -61,7 +61,7 @@ const SessionManagement = () => {
{ {
header: 'Last known use', header: 'Last known use',
width: 305, width: 305,
textAlign: 'center', textAlign: 'left',
size: 'sm', size: 'sm',
view: s => { view: s => {
if (R.isNil(s.sess.ua)) return 'No Record' if (R.isNil(s.sess.ua)) return 'No Record'
@ -72,7 +72,7 @@ const SessionManagement = () => {
{ {
header: 'Last known location', header: 'Last known location',
width: 250, width: 250,
textAlign: 'center', textAlign: 'left',
size: 'sm', size: 'sm',
view: s => { view: s => {
return isLocalhost(s.sess.ipAddress) ? 'This device' : s.sess.ipAddress return isLocalhost(s.sess.ipAddress) ? 'This device' : s.sess.ipAddress
@ -107,16 +107,15 @@ const SessionManagement = () => {
] ]
return ( return (
!loading && (
<> <>
<TitleSection title="Session Management" /> <TitleSection title="Session Management" />
<DataTable <DataTable
loading={loading}
elements={elements} elements={elements}
data={R.path(['sessions'])(tknResponse)} data={R.path(['sessions'])(tknResponse)}
/> />
</> </>
) )
)
} }
export default SessionManagement export default SessionManagement

View file

@ -15,6 +15,8 @@ import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg' import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
import { ReactComponent as CustomerLinkWhiteIcon } from 'src/styling/icons/month arrows/right_white.svg'
import { errorColor } from 'src/styling/variables'
import { formatDate } from 'src/utils/timezones' import { formatDate } from 'src/utils/timezones'
import DetailsRow from './DetailsCard' import DetailsRow from './DetailsCard'
@ -124,13 +126,13 @@ const Transactions = () => {
const history = useHistory() const history = useHistory()
const [filters, setFilters] = useState([]) const [filters, setFilters] = useState([])
const { data: filtersResponse, loading: loadingFilters } = useQuery( const { data: filtersResponse, loading: filtersLoading } = useQuery(
GET_TRANSACTION_FILTERS GET_TRANSACTION_FILTERS
) )
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS }) const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
const { const {
data: txData, data: txData,
loading: loadingTransactions, loading: transactionsLoading,
refetch, refetch,
startPolling, startPolling,
stopPolling stopPolling
@ -185,7 +187,11 @@ const Transactions = () => {
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div> <div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
{!it.isAnonymous && ( {!it.isAnonymous && (
<div onClick={() => redirect(it.customerId)}> <div onClick={() => redirect(it.customerId)}>
{it.hasError ? (
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
) : (
<CustomerLinkIcon className={classes.customerLinkIcon} /> <CustomerLinkIcon className={classes.customerLinkIcon} />
)}
</div> </div>
)} )}
</div> </div>
@ -294,6 +300,14 @@ const Transactions = () => {
const filterOptions = R.path(['transactionFilters'])(filtersResponse) const filterOptions = R.path(['transactionFilters'])(filtersResponse)
const loading = transactionsLoading || filtersLoading || configLoading
const errorLabel = (
<svg width={12} height={12}>
<rect width={12} height={12} rx={3} fill={errorColor} />
</svg>
)
return ( return (
<> <>
<div className={classes.titleWrapper}> <div className={classes.titleWrapper}>
@ -301,7 +315,7 @@ const Transactions = () => {
<Title>Transactions</Title> <Title>Transactions</Title>
<div className={classes.buttonsWrapper}> <div className={classes.buttonsWrapper}>
<SearchBox <SearchBox
loading={loadingFilters} loading={filtersLoading}
filters={filters} filters={filters}
options={filterOptions} options={filterOptions}
inputPlaceholder={'Search Transactions'} inputPlaceholder={'Search Transactions'}
@ -331,6 +345,10 @@ const Transactions = () => {
<TxOutIcon /> <TxOutIcon />
<span>Cash-out</span> <span>Cash-out</span>
</div> </div>
<div>
{errorLabel}
<span>Transaction error</span>
</div>
</div> </div>
</div> </div>
{filters.length > 0 && ( {filters.length > 0 && (
@ -342,7 +360,7 @@ const Transactions = () => {
/> />
)} )}
<DataTable <DataTable
loading={loadingTransactions && configLoading} loading={loading}
emptyText="No transactions so far" emptyText="No transactions so far"
elements={elements} elements={elements}
data={txList} data={txList}

View file

@ -80,8 +80,11 @@ const mainStyles = {
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center'
}, },
'& > div': {
marginLeft: 24
},
'& > div:first-child': { '& > div:first-child': {
marginRight: 24 marginLeft: 0
}, },
'& span': { '& span': {
extend: label1, extend: label1,

View file

@ -183,10 +183,10 @@ const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => {
return ( return (
<> <>
<H5 className={classes.infoTitle}>Trigger overview so far</H5> <H5 className={classes.infoTitle}>Trigger overview so far</H5>
<Info3 noMargin className={classes.infoText}> <Info3 noMargin>
{oldText} {oldText}
{step !== 1 && ', '} {step !== 1 && ', '}
{newText} <span className={classes.infoCurrentText}>{newText}</span>
{!isLastStep && '...'} {!isLastStep && '...'}
</Info3> </Info3>
</> </>

View file

@ -61,7 +61,7 @@ const getDefaultSettings = () => {
return [ return [
{ {
name: 'expirationTime', name: 'expirationTime',
header: 'Expiration Time', header: 'Expiration time',
width: 196, width: 196,
size: 'sm', size: 'sm',
editable: false editable: false
@ -101,7 +101,7 @@ const getOverrides = () => {
}, },
{ {
name: 'expirationTime', name: 'expirationTime',
header: 'Expiration Time', header: 'Expiration time',
width: 196, width: 196,
size: 'sm', size: 'sm',
editable: false editable: false

View file

@ -6,10 +6,16 @@ import * as R from 'ramda'
import React, { useReducer, useState, useContext } from 'react' import React, { useReducer, useState, useContext } from 'react'
import AppContext from 'src/AppContext' import AppContext from 'src/AppContext'
import { Link } from 'src/components/buttons' import { ActionButton, Link } from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as WhiteKeyIcon } from 'src/styling/icons/button/key/white.svg'
import { ReactComponent as KeyIcon } from 'src/styling/icons/button/key/zodiac.svg'
import { ReactComponent as WhiteLockIcon } from 'src/styling/icons/button/lock/white.svg'
import { ReactComponent as LockIcon } from 'src/styling/icons/button/lock/zodiac.svg'
import { ReactComponent as WhiteUserRoleIcon } from 'src/styling/icons/button/user-role/white.svg'
import { ReactComponent as UserRoleIcon } from 'src/styling/icons/button/user-role/zodiac.svg'
import styles from './UserManagement.styles' import styles from './UserManagement.styles'
import ChangeRoleModal from './modals/ChangeRoleModal' import ChangeRoleModal from './modals/ChangeRoleModal'
@ -153,35 +159,37 @@ const Users = () => {
size: 'sm', size: 'sm',
view: u => { view: u => {
return ( return (
<> <div className={classes.actionButtonWrapper}>
<Chip <ActionButton
size="small" Icon={KeyIcon}
label="Reset password" InverseIcon={WhiteKeyIcon}
className={classes.actionChip} color="primary"
onClick={() => { onClick={() => {
setUserInfo(u) setUserInfo(u)
dispatch({ dispatch({
type: 'open', type: 'open',
payload: 'showResetPasswordModal' payload: 'showResetPasswordModal'
}) })
}} }}>
/> Reset password
<Chip </ActionButton>
size="small" <ActionButton
label="Reset 2FA" Icon={LockIcon}
className={classes.actionChip} InverseIcon={WhiteLockIcon}
color="primary"
onClick={() => { onClick={() => {
setUserInfo(u) setUserInfo(u)
dispatch({ dispatch({
type: 'open', type: 'open',
payload: 'showReset2FAModal' payload: 'showReset2FAModal'
}) })
}} }}>
/> Reset 2FA
<Chip </ActionButton>
size="small" <ActionButton
label="Add FIDO" Icon={UserRoleIcon}
className={classes.actionChip} InverseIcon={WhiteUserRoleIcon}
color="primary"
onClick={() => { onClick={() => {
setUserInfo(u) setUserInfo(u)
generateAttestationOptions({ generateAttestationOptions({
@ -189,9 +197,10 @@ const Users = () => {
userID: u.id userID: u.id
} }
}) })
}} }}>
/> Add FIDO
</> </ActionButton>
</div>
) )
} }
}, },

View file

@ -52,10 +52,6 @@ const styles = {
fontFamily: fontPrimary, fontFamily: fontPrimary,
marginLeft: 10 marginLeft: 10
}, },
actionChip: {
backgroundColor: subheaderColor,
marginRight: 15
},
info: { info: {
fontFamily: fontSecondary, fontFamily: fontSecondary,
textAlign: 'justify' textAlign: 'justify'
@ -118,6 +114,10 @@ const styles = {
}, },
roleSwitch: { roleSwitch: {
marginLeft: 15 marginLeft: 15
},
actionButtonWrapper: {
display: 'flex',
gap: 12
} }
} }

View file

@ -1,5 +1,6 @@
import { useLazyQuery } from '@apollo/react-hooks' import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import { Form, Formik } from 'formik'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import React, { useState } from 'react' import React, { useState } from 'react'
@ -48,6 +49,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
return null return null
} }
const handleSubmit = () => {
if (twoFACode.length !== 6) {
setInvalidCode(true)
return
}
confirm2FA({ variables: { code: twoFACode } })
}
return ( return (
showModal && ( showModal && (
<Modal <Modal
@ -61,6 +70,9 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
To make changes on this user, please confirm this action by entering To make changes on this user, please confirm this action by entering
your two-factor authentication code below. your two-factor authentication code below.
</P> </P>
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
<Formik onSubmit={() => {}} initialValues={{}}>
<Form>
<CodeInput <CodeInput
name="2fa" name="2fa"
value={twoFACode} value={twoFACode}
@ -70,19 +82,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
containerStyle={classes.codeContainer} containerStyle={classes.codeContainer}
shouldAutoFocus shouldAutoFocus
/> />
<button onClick={handleSubmit} className={classes.enterButton} />
</Form>
</Formik>
{getErrorMsg() && ( {getErrorMsg() && (
<P className={classes.errorMessage}>{getErrorMsg()}</P> <P className={classes.errorMessage}>{getErrorMsg()}</P>
)} )}
<div className={classes.footer}> <div className={classes.footer}>
<Button <Button className={classes.submit} onClick={handleSubmit}>
className={classes.submit}
onClick={() => {
if (twoFACode.length !== 6) {
setInvalidCode(true)
return
}
confirm2FA({ variables: { code: twoFACode } })
}}>
Confirm Confirm
</Button> </Button>
</div> </div>

View file

@ -38,7 +38,8 @@ const SAVE_ACCOUNTS = gql`
} }
` `
const isConfigurable = it => !R.isNil(it) && !R.contains(it)(['mock-exchange']) const isConfigurable = it =>
!R.isNil(it) && !R.contains(it)(['mock-exchange', 'no-exchange'])
const ChooseExchange = ({ data: currentData, addData }) => { const ChooseExchange = ({ data: currentData, addData }) => {
const classes = useStyles() const classes = useStyles()

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/key/white</title>
<g id="icon/button/key/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(0.500000, 0.500000)" stroke="#FFFFFF">
<circle id="Oval" cx="2.75" cy="8.25" r="2.75"></circle>
<line x1="5.04166667" y1="5.95833333" x2="11" y2="0" id="Path-13" stroke-linecap="round" stroke-linejoin="round"></line>
<line x1="8.25" y1="3.66666667" x2="10.5416667" y2="1.375" id="Path-13-Copy" stroke-width="2" stroke-linejoin="round"></line>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/key/zodiac</title>
<g id="icon/button/key/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(0.500000, 0.500000)" stroke="#1B2559">
<circle id="Oval" cx="2.75" cy="8.25" r="2.75"></circle>
<line x1="5.04166667" y1="5.95833333" x2="11" y2="0" id="Path-13" stroke-linecap="round" stroke-linejoin="round"></line>
<line x1="8.25" y1="3.66666667" x2="10.5416667" y2="1.375" id="Path-13-Copy" stroke-width="2" stroke-linejoin="round"></line>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 775 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/lock/white</title>
<g id="icon/button/lock/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Lock-Icon-White" transform="translate(0.500000, 0.500000)">
<path d="M7.98058644,2.48058644 C7.98058644,1.11059638 6.86999006,0 5.5,0 C4.13000994,0 3.01941356,1.11059638 3.01941356,2.48058644 C3.01941356,3.39391315 3.01941356,4.09482878 3.01941356,4.58333333 L7.98058644,4.58333333 C7.98058644,4.09482878 7.98058644,3.39391315 7.98058644,2.48058644 Z" id="Lock" stroke="#FFFFFF" stroke-linejoin="round"></path>
<rect id="Body" stroke="#FFFFFF" stroke-linejoin="round" x="0" y="4.58333333" width="11" height="6.41666667"></rect>
<circle id="Key-Hole" fill="#FFFFFF" cx="5.5" cy="7.33333333" r="1"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1,010 B

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/lock/zodiac</title>
<g id="icon/button/lock/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Lock-Icon-Zodiac" transform="translate(0.500000, 0.500000)">
<path d="M7.98058644,2.48058644 C7.98058644,1.11059638 6.86999006,0 5.5,0 C4.13000994,0 3.01941356,1.11059638 3.01941356,2.48058644 C3.01941356,3.39391315 3.01941356,4.09482878 3.01941356,4.58333333 L7.98058644,4.58333333 C7.98058644,4.09482878 7.98058644,3.39391315 7.98058644,2.48058644 Z" id="Lock" stroke="#1B2559" stroke-linejoin="round"></path>
<rect id="Body" stroke="#1B2559" stroke-linejoin="round" x="0" y="4.58333333" width="11" height="6.41666667"></rect>
<circle id="Key-Hole" fill="#1B2559" cx="5.5" cy="7.33333333" r="1"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1,013 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/user-role/white</title>
<g id="icon/button/user-role/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
<g id="User-Role-Icon-White" transform="translate(2.500000, 0.500000)" stroke="#FFFFFF">
<path d="M5.50008791,6.84274776 L5.5,11 L3.66666667,9.35927189 L1.83333333,11 L1.83223109,6.84216075 C2.37179795,7.15453375 2.99835187,7.33333333 3.66666667,7.33333333 C4.33456272,7.33333333 4.96075021,7.15475774 5.50008791,6.84274776 Z" id="Bottom"></path>
<circle id="Top" cx="3.66666667" cy="3.66666667" r="3.66666667"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 840 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/button/user-role/zodiac</title>
<g id="icon/button/user-role/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
<g id="User-Role-Icon-Zodiac" transform="translate(2.500000, 0.500000)" stroke="#1B2559">
<path d="M5.50008791,6.84274776 L5.5,11 L3.66666667,9.35927189 L1.83333333,11 L1.83223109,6.84216075 C2.37179795,7.15453375 2.99835187,7.33333333 3.66666667,7.33333333 C4.33456272,7.33333333 4.96075021,7.15475774 5.50008791,6.84274776 Z" id="Bottom"></path>
<circle id="Top" cx="3.66666667" cy="3.66666667" r="3.66666667"></circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 843 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/customer-nav/photos/comet</title>
<g id="icon/customer-nav/photos/comet" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#5F668A" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#5F668A" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#5F668A" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#5F668A" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>icon/customer-nav/photos/white</title>
<g id="icon/customer-nav/photos/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="Rectangle" stroke="#FFFFFF" stroke-width="2" x="1" y="1" width="18" height="18" rx="1"></rect>
<circle id="Oval" stroke="#FFFFFF" stroke-width="2" cx="15" cy="5" r="1"></circle>
<polyline id="Path" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round" points="1 19 7 13 13 19"></polyline>
<path d="M13.3333333,14 L18,19 L13.3333333,19 L11,16.5 L13.3333333,14 Z" id="Combined-Shape" stroke="#FFFFFF" stroke-width="2" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 850 B

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="path-1-right" cx="10" cy="10" r="10"></circle>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="pop-up/action/download-logs/date-range-copy-2" transform="translate(-232.000000, -187.000000)">
<g id="icon/sf-contain-b-copy-4" transform="translate(242.000000, 197.000000) scale(-1, 1) rotate(-270.000000) translate(-242.000000, -197.000000) translate(232.000000, 187.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1-right"></use>
</mask>
<use id="Mask" fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1-right"></use>
<g id="icon/sf-small/wizzard" mask="url(#mask-2)" stroke-linecap="round" stroke-linejoin="round">
<g transform="translate(6.666667, 6.000000)" id="Group">
<g>
<polyline id="Path-3" stroke="#1B2559" stroke-width="2" points="0 4.83333333 3.33333333 8.16666667 6.66666667 4.83333333"></polyline>
<line x1="3.33333333" y1="0.25" x2="3.33333333" y2="6.5" id="Path-4" stroke="#1B2559" stroke-width="2"></line>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -7,4 +7,7 @@ const transformNumber = value => (isValidNumber(value) ? value : null)
const defaultToZero = value => const defaultToZero = value =>
isValidNumber(parseInt(value)) ? parseInt(value) : 0 isValidNumber(parseInt(value)) ? parseInt(value) : 0
export { defaultToZero, transformNumber } const numberToFiatAmount = value =>
value.toLocaleString('en-US', { maximumFractionDigits: 2 })
export { defaultToZero, transformNumber, numberToFiatAmount }

View file

@ -3,85 +3,109 @@ import { getTimezoneOffset } from 'date-fns-tz'
import * as R from 'ramda' import * as R from 'ramda'
const timezones = { const timezones = {
'Pacific/Midway': 'Midway Island, Samoa', 'Pacific/Midway': { short: 'SST', long: 'Midway Island, Samoa' },
'Pacific/Honolulu': 'Hawaii', 'Pacific/Honolulu': { short: 'HAST', long: 'Hawaii' },
'America/Juneau': 'Alaska', 'America/Juneau': { short: 'AKST', long: 'Alaska' },
'America/Boise': 'Mountain Time', 'America/Boise': { short: 'MST', long: 'Mountain Time' },
'America/Dawson': 'Dawson, Yukon', 'America/Dawson': { short: 'MST', long: 'Dawson, Yukon' },
'America/Chihuahua': 'Chihuahua, La Paz, Mazatlan', 'America/Chihuahua': { short: null, long: 'Chihuahua, La Paz, Mazatlan' },
'America/Phoenix': 'Arizona', 'America/Phoenix': { short: 'MST', long: 'Arizona' },
'America/Chicago': 'Central Time', 'America/Chicago': { short: 'CST', long: 'Central Time' },
'America/Regina': 'Saskatchewan', 'America/Regina': { short: 'CST', long: 'Saskatchewan' },
'America/Mexico_City': 'Guadalajara, Mexico City, Monterrey', 'America/Mexico_City': {
'America/Belize': 'Central America', short: 'CST',
'America/Detroit': 'Eastern Time', long: 'Guadalajara, Mexico City, Monterrey'
'America/Bogota': 'Bogota, Lima, Quito', },
'America/Caracas': 'Caracas, La Paz', 'America/Belize': { short: 'CST', long: 'Central America' },
'America/Santiago': 'Santiago', 'America/Detroit': { short: 'EST', long: 'Eastern Time' },
'America/St_Johns': 'Newfoundland and Labrador', 'America/Bogota': { short: 'COT', long: 'Bogota, Lima, Quito' },
'America/Sao_Paulo': 'Brasilia', 'America/Caracas': { short: 'VET', long: 'Caracas, La Paz' },
'America/Tijuana': 'Tijuana', 'America/Santiago': { short: 'CLST', long: 'Santiago' },
'America/Montevideo': 'Montevideo', 'America/St_Johns': { short: 'HNTN', long: 'Newfoundland and Labrador' },
'America/Argentina/Buenos_Aires': 'Buenos Aires, Georgetown', 'America/Sao_Paulo': { short: 'BRT', long: 'Brasilia' },
'America/Godthab': 'Greenland', 'America/Tijuana': { short: 'PST', long: 'Tijuana' },
'America/Los_Angeles': 'Pacific Time', 'America/Montevideo': { short: 'UYT', long: 'Montevideo' },
'Atlantic/Azores': 'Azores', 'America/Argentina/Buenos_Aires': {
'Atlantic/Cape_Verde': 'Cape Verde Islands', short: null,
GMT: 'UTC', long: 'Buenos Aires, Georgetown'
'Europe/London': 'Edinburgh, London', },
'Europe/Dublin': 'Dublin', 'America/Godthab': { short: null, long: 'Greenland' },
'Europe/Lisbon': 'Lisbon', 'America/Los_Angeles': { short: 'PST', long: 'Pacific Time' },
'Africa/Casablanca': 'Casablanca, Monrovia', 'Atlantic/Azores': { short: 'AZOT', long: 'Azores' },
'Atlantic/Canary': 'Canary Islands', 'Atlantic/Cape_Verde': { short: 'CVT', long: 'Cape Verde Islands' },
'Europe/Belgrade': 'Belgrade, Bratislava, Budapest, Ljubljana, Prague', GMT: { short: 'GMT', long: 'UTC' },
'Europe/Sarajevo': 'Sarajevo, Skopje, Warsaw, Zagreb', 'Europe/London': { short: 'GMT', long: 'Edinburgh, London' },
'Europe/Brussels': 'Brussels, Copenhagen, Madrid, Paris', 'Europe/Dublin': { short: 'GMT', long: 'Dublin' },
'Europe/Amsterdam': 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna', 'Europe/Lisbon': { short: 'WET', long: 'Lisbon' },
'Africa/Algiers': 'West Central Africa', 'Africa/Casablanca': { short: 'WET', long: 'Casablanca, Monrovia' },
'Europe/Bucharest': 'Bucharest', 'Atlantic/Canary': { short: 'WET', long: 'Canary Islands' },
'Africa/Cairo': 'Cairo', 'Europe/Belgrade': {
'Europe/Helsinki': 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius', short: 'CET',
'Europe/Athens': 'Athens, Istanbul, Minsk', long: 'Belgrade, Bratislava, Budapest, Ljubljana, Prague'
'Asia/Jerusalem': 'Jerusalem', },
'Africa/Harare': 'Harare, Pretoria', 'Europe/Sarajevo': { short: 'CET', long: 'Sarajevo, Skopje, Warsaw, Zagreb' },
'Europe/Moscow': 'Moscow, St. Petersburg, Volgograd', 'Europe/Brussels': {
'Asia/Kuwait': 'Kuwait, Riyadh', short: 'CET',
'Africa/Nairobi': 'Nairobi', long: 'Brussels, Copenhagen, Madrid, Paris'
'Asia/Baghdad': 'Baghdad', },
'Asia/Tehran': 'Tehran', 'Europe/Amsterdam': {
'Asia/Dubai': 'Abu Dhabi, Muscat', short: 'CET',
'Asia/Baku': 'Baku, Tbilisi, Yerevan', long: 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna'
'Asia/Kabul': 'Kabul', },
'Asia/Yekaterinburg': 'Ekaterinburg', 'Africa/Algiers': { short: 'CET', long: 'West Central Africa' },
'Asia/Karachi': 'Islamabad, Karachi, Tashkent', 'Europe/Bucharest': { short: 'EET', long: 'Bucharest' },
'Asia/Kolkata': 'Chennai, Kolkata, Mumbai, New Delhi', 'Africa/Cairo': { short: 'EET', long: 'Cairo' },
'Asia/Kathmandu': 'Kathmandu', 'Europe/Helsinki': {
'Asia/Dhaka': 'Astana, Dhaka', short: 'EET',
'Asia/Colombo': 'Sri Jayawardenepura', long: 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius'
'Asia/Almaty': 'Almaty, Novosibirsk', },
'Asia/Rangoon': 'Yangon Rangoon', 'Europe/Athens': { short: 'EET', long: 'Athens, Istanbul, Minsk' },
'Asia/Bangkok': 'Bangkok, Hanoi, Jakarta', 'Asia/Jerusalem': { short: 'IST', long: 'Jerusalem' },
'Asia/Krasnoyarsk': 'Krasnoyarsk', 'Africa/Harare': { short: 'CAT', long: 'Harare, Pretoria' },
'Asia/Shanghai': 'Beijing, Chongqing, Hong Kong SAR, Urumqi', 'Europe/Moscow': { short: 'MSK', long: 'Moscow, St. Petersburg, Volgograd' },
'Asia/Kuala_Lumpur': 'Kuala Lumpur, Singapore', 'Asia/Kuwait': { short: 'AST', long: 'Kuwait, Riyadh' },
'Asia/Taipei': 'Taipei', 'Africa/Nairobi': { short: 'EAT', long: 'Nairobi' },
'Australia/Perth': 'Perth', 'Asia/Baghdad': { short: 'AST', long: 'Baghdad' },
'Asia/Irkutsk': 'Irkutsk, Ulaanbaatar', 'Asia/Tehran': { short: 'IRST', long: 'Tehran' },
'Asia/Seoul': 'Seoul', 'Asia/Dubai': { short: 'GST', long: 'Abu Dhabi, Muscat' },
'Asia/Tokyo': 'Osaka, Sapporo, Tokyo', 'Asia/Baku': { short: 'AZT', long: 'Baku, Tbilisi, Yerevan' },
'Asia/Yakutsk': 'Yakutsk', 'Asia/Kabul': { short: 'AFT', long: 'Kabul' },
'Australia/Darwin': 'Darwin', 'Asia/Yekaterinburg': { short: 'YEKT', long: 'Ekaterinburg' },
'Australia/Adelaide': 'Adelaide', 'Asia/Karachi': { short: 'PKT', long: 'Islamabad, Karachi, Tashkent' },
'Australia/Sydney': 'Canberra, Melbourne, Sydney', 'Asia/Kolkata': { short: 'IST', long: 'Chennai, Kolkata, Mumbai, New Delhi' },
'Australia/Brisbane': 'Brisbane', 'Asia/Kathmandu': { short: null, long: 'Kathmandu' },
'Australia/Hobart': 'Hobart', 'Asia/Dhaka': { short: 'BST', long: 'Astana, Dhaka' },
'Asia/Vladivostok': 'Vladivostok', 'Asia/Colombo': { short: 'IST', long: 'Sri Jayawardenepura' },
'Pacific/Guam': 'Guam, Port Moresby', 'Asia/Almaty': { short: 'ALMT', long: 'Almaty, Novosibirsk' },
'Asia/Magadan': 'Magadan, Solomon Islands, New Caledonia', 'Asia/Rangoon': { short: null, long: 'Yangon Rangoon' },
'Asia/Kamchatka': 'Kamchatka, Marshall Islands', 'Asia/Bangkok': { short: 'ICT', long: 'Bangkok, Hanoi, Jakarta' },
'Pacific/Fiji': 'Fiji Islands', 'Asia/Krasnoyarsk': { short: 'KRAT', long: 'Krasnoyarsk' },
'Pacific/Auckland': 'Auckland, Wellington', 'Asia/Shanghai': {
'Pacific/Tongatapu': "Nuku'alofa" short: 'CST',
long: 'Beijing, Chongqing, Hong Kong SAR, Urumqi'
},
'Asia/Kuala_Lumpur': { short: 'MYT', long: 'Kuala Lumpur, Singapore' },
'Asia/Taipei': { short: 'CST', long: 'Taipei' },
'Australia/Perth': { short: 'AWST', long: 'Perth' },
'Asia/Irkutsk': { short: 'IRKT', long: 'Irkutsk, Ulaanbaatar' },
'Asia/Seoul': { short: 'KST', long: 'Seoul' },
'Asia/Tokyo': { short: 'JST', long: 'Osaka, Sapporo, Tokyo' },
'Asia/Yakutsk': { short: 'YAKT', long: 'Yakutsk' },
'Australia/Darwin': { short: 'ACST', long: 'Darwin' },
'Australia/Adelaide': { short: 'ACDT', long: 'Adelaide' },
'Australia/Sydney': { short: 'AEDT', long: 'Canberra, Melbourne, Sydney' },
'Australia/Brisbane': { short: 'AEST', long: 'Brisbane' },
'Australia/Hobart': { short: 'AEDT', long: 'Hobart' },
'Asia/Vladivostok': { short: 'VLAT', long: 'Vladivostok' },
'Pacific/Guam': { short: 'ChST', long: 'Guam, Port Moresby' },
'Asia/Magadan': {
short: 'MAGT',
long: 'Magadan, Solomon Islands, New Caledonia'
},
'Asia/Kamchatka': { short: 'PETT', long: 'Kamchatka, Marshall Islands' },
'Pacific/Fiji': { short: 'FJT', long: 'Fiji Islands' },
'Pacific/Auckland': { short: 'NZDT', long: 'Auckland, Wellington' },
'Pacific/Tongatapu': { short: null, long: "Nuku'alofa" }
} }
const buildTzLabels = timezoneList => { const buildTzLabels = timezoneList => {
@ -106,7 +130,7 @@ const buildTzLabels = timezoneList => {
const prefix = `(GMT${isNegative ? `-` : `+`}${hours}:${minutes})` const prefix = `(GMT${isNegative ? `-` : `+`}${hours}:${minutes})`
acc.push({ acc.push({
label: `${prefix} - ${value[1]}`, label: `${prefix} - ${value[1].long}`,
code: value[0] code: value[0]
}) })
@ -117,4 +141,6 @@ const buildTzLabels = timezoneList => {
) )
} }
export default buildTzLabels(timezones) const labels = buildTzLabels(timezones)
export { labels, timezones }

Some files were not shown because too many files have changed in this diff Show more