Merge branch 'dev' into fix/machine_upairing
|
|
@ -45,6 +45,7 @@ keypool=10000
|
|||
prune=4000
|
||||
daemon=0
|
||||
addresstype=p2sh-segwit
|
||||
changetype=bech32
|
||||
walletrbf=1
|
||||
bind=0.0.0.0:8332
|
||||
rpcport=8333`
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ const BINARIES = {
|
|||
dir: 'bitcoin-22.0/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.12-6c4dc6c3.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.12-6c4dc6c3'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.13-7a0c19f8.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.13-7a0c19f8'
|
||||
},
|
||||
ZEC: {
|
||||
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'
|
||||
},
|
||||
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',
|
||||
dir: 'bitcoin-cash-node-23.1.0/bin',
|
||||
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-24.0.0/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
},
|
||||
XMR: {
|
||||
|
|
|
|||
|
|
@ -44,5 +44,6 @@ connections=40
|
|||
keypool=10000
|
||||
prune=4000
|
||||
daemon=0
|
||||
addresstype=p2sh-segwit`
|
||||
addresstype=p2sh-segwit
|
||||
changetype=bech32`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
|||
const uuid = require('uuid')
|
||||
|
||||
function createCashboxBatch (deviceId, cashboxCount) {
|
||||
if (_.isEqual(0, cashboxCount)) throw new Error('Cashbox is empty. Cashbox 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 *`
|
||||
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-box-empty') RETURNING *`
|
||||
const sql2 = `
|
||||
UPDATE bills SET cashbox_batch_id=$1
|
||||
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))
|
||||
if (!isValidContext && !isCassetteAmountWithinRange)
|
||||
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 => {
|
||||
const deviceId = machineContext.deviceId
|
||||
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
|
||||
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
|
||||
cash_in_txs.device_id = $2 AND
|
||||
|
|
@ -68,4 +68,10 @@ function getBillsByBatchId (id) {
|
|||
return db.any(sql, [id])
|
||||
}
|
||||
|
||||
module.exports = { createCashboxBatch, updateMachineWithBatch, getBatches, getBillsByBatchId, editBatchById }
|
||||
module.exports = {
|
||||
createCashboxBatch,
|
||||
updateMachineWithBatch,
|
||||
getBatches,
|
||||
getBillsByBatchId,
|
||||
editBatchById
|
||||
}
|
||||
|
|
|
|||
|
|
@ -683,18 +683,18 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
*/
|
||||
function getCustomerById (id) {
|
||||
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,
|
||||
phone, sms_override, 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,
|
||||
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_at, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||
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,
|
||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, custom_fields, notes
|
||||
FROM (
|
||||
SELECT c.id, c.authorized_override,
|
||||
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
|
||||
c.suspended_until > now() AS is_suspended,
|
||||
c.front_camera_path, c.front_camera_override,
|
||||
c.phone, c.sms_override, c.id_card_data, 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.front_camera_path, c.front_camera_override, c.front_camera_at,
|
||||
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_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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ function addName (pings, events, config) {
|
|||
const statuses = [
|
||||
getStatus(
|
||||
_.first(pings[machine.deviceId]),
|
||||
_.first(checkStuckScreen(events, machine.name))
|
||||
_.first(checkStuckScreen(events, machine))
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const bills = require('../../services/bills')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
bills: () => bills.getBills()
|
||||
bills: (...[, { filters }]) => bills.getBills(filters)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ const { gql } = require('apollo-server-express')
|
|||
|
||||
const typeDef = gql`
|
||||
type Bill {
|
||||
id: ID
|
||||
fiat: Int
|
||||
deviceId: ID
|
||||
created: Date
|
||||
cashbox: Int
|
||||
cashboxBatchId: ID
|
||||
}
|
||||
|
||||
type Query {
|
||||
bills: [Bill] @auth
|
||||
bills(filters: JSONObject): [Bill] @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const db = require('../../db')
|
||||
|
||||
// Get all bills with device id
|
||||
const getBills = () => {
|
||||
return Promise.reject(new Error('This functionality hasn\'t been implemented yet'))
|
||||
/* return db.any(`
|
||||
SELECT d.device_id, b.fiat, b.created, d.cashbox
|
||||
FROM cash_in_txs
|
||||
INNER JOIN bills AS b ON b.cash_in_txs_id = cash_in_txs.id
|
||||
INNER JOIN devices as d ON d.device_id = cash_in_txs.device_id
|
||||
ORDER BY device_id, created DESC`
|
||||
)
|
||||
.then(res => {
|
||||
return res.map(item => ({
|
||||
fiat: item.fiat,
|
||||
deviceId: item.device_id,
|
||||
cashbox: item.cashbox,
|
||||
created: item.created
|
||||
}))
|
||||
}) */
|
||||
const getBills = filters => {
|
||||
const deviceStatement = !_.isNil(filters.deviceId) ? `WHERE device_id = ${pgp.as.text(filters.deviceId)}` : ``
|
||||
const batchStatement = filter => {
|
||||
switch (filter) {
|
||||
case 'none':
|
||||
return `WHERE b.cashbox_batch_id IS NULL`
|
||||
case 'any':
|
||||
return `WHERE b.cashbox_batch_id IS NOT NULL`
|
||||
default:
|
||||
return _.isNil(filter) ? `` : `WHERE b.cashbox_batch_id = ${pgp.as.text(filter)}`
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
SELECT id, device_id FROM cash_in_txs ${deviceStatement}
|
||||
) 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 = {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ function batch (
|
|||
((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
|
||||
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 ${
|
||||
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
|
||||
AND actions.action = 'provisionAddress'
|
||||
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 ${
|
||||
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 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)
|
||||
return getCommissionFee(it).plus(BN(it.cashInFee))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,12 +85,8 @@ function buildAlerts (pings, balances, events, devices) {
|
|||
alerts.general = _.filter(r => !r.deviceId, balances)
|
||||
_.forEach(device => {
|
||||
const deviceId = device.deviceId
|
||||
const deviceName = device.name
|
||||
const deviceEvents = events.filter(function (eventRow) {
|
||||
return eventRow.device_id === deviceId
|
||||
})
|
||||
const ping = pings[deviceId] || []
|
||||
const stuckScreen = checkStuckScreen(deviceEvents, deviceName)
|
||||
const stuckScreen = checkStuckScreen(events, device)
|
||||
|
||||
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
|
||||
['deviceId', deviceId],
|
||||
|
|
@ -98,7 +94,7 @@ function buildAlerts (pings, balances, events, devices) {
|
|||
), alerts.devices)
|
||||
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
||||
|
||||
alerts.deviceNames[deviceId] = deviceName
|
||||
alerts.deviceNames[deviceId] = device.name
|
||||
}, devices)
|
||||
|
||||
return alerts
|
||||
|
|
@ -110,12 +106,13 @@ function checkPings (devices) {
|
|||
return _.zipObject(deviceIds)(pings)
|
||||
}
|
||||
|
||||
function checkStuckScreen (deviceEvents, machineName) {
|
||||
const sortedEvents = _.sortBy(
|
||||
utils.getDeviceTime,
|
||||
_.map(utils.parseEventNote, deviceEvents)
|
||||
)
|
||||
const lastEvent = _.last(sortedEvents)
|
||||
function checkStuckScreen (deviceEvents, machine) {
|
||||
const lastEvent = _.pipe(
|
||||
_.filter(e => e.device_id === machine.deviceId),
|
||||
_.sortBy(utils.getDeviceTime),
|
||||
_.map(utils.parseEventNote),
|
||||
_.last
|
||||
)(deviceEvents)
|
||||
|
||||
if (!lastEvent) return []
|
||||
|
||||
|
|
@ -125,6 +122,7 @@ function checkStuckScreen (deviceEvents, machineName) {
|
|||
if (isIdle) return []
|
||||
|
||||
const age = Math.floor(lastEvent.age)
|
||||
const machineName = machine.name
|
||||
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
|
||||
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -18,27 +18,10 @@ module.exports.up = function (next) {
|
|||
}
|
||||
|
||||
loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
|
||||
.then(async settings => {
|
||||
if (_.isEmpty(settings.config)) {
|
||||
return {
|
||||
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))
|
||||
})
|
||||
.then(settings => _.isEmpty(settings.config)
|
||||
? next()
|
||||
: migrateConfig(settings)
|
||||
)
|
||||
.catch(err => {
|
||||
if (err.message === 'lamassu-server is not configured') {
|
||||
return next()
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ var db = require('./db')
|
|||
exports.up = function (next) {
|
||||
var sqls = [
|
||||
`CREATE TYPE cashbox_batch_type AS ENUM(
|
||||
'cash-in-empty',
|
||||
'cash-out-1-refill',
|
||||
'cash-out-1-empty',
|
||||
'cash-out-2-refill',
|
||||
'cash-out-2-empty',
|
||||
'cash-out-3-refill',
|
||||
'cash-out-3-empty',
|
||||
'cash-out-4-refill',
|
||||
'cash-out-4-empty'
|
||||
'cash-box-empty',
|
||||
'cash-cassette-1-refill',
|
||||
'cash-cassette-1-empty',
|
||||
'cash-cassette-2-refill',
|
||||
'cash-cassette-2-empty',
|
||||
'cash-cassette-3-refill',
|
||||
'cash-cassette-3-empty',
|
||||
'cash-cassette-4-refill',
|
||||
'cash-cassette-4-empty'
|
||||
)`,
|
||||
`ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`,
|
||||
`ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`,
|
||||
|
|
|
|||
22
new-lamassu-admin/package-lock.json
generated
|
|
@ -6889,11 +6889,14 @@
|
|||
}
|
||||
},
|
||||
"apollo-upload-client": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-16.0.0.tgz",
|
||||
"integrity": "sha512-aLhYucyA0T8aBEQ5g+p13qnR9RUyL8xqb8FSZ7e/Kw2KUOsotLUlFluLobqaE7JSUFwc6sKfXIcwB7y4yEjbZg==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz",
|
||||
"integrity": "sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==",
|
||||
"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": {
|
||||
|
|
@ -12617,9 +12620,9 @@
|
|||
}
|
||||
},
|
||||
"extract-files": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz",
|
||||
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ=="
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-8.1.0.tgz",
|
||||
"integrity": "sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ=="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
|
|
@ -27096,6 +27099,11 @@
|
|||
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-error": "^1.1.13",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"apollo-upload-client": "^16.0.0",
|
||||
"apollo-upload-client": "^13.0.0",
|
||||
"axios": "0.21.1",
|
||||
"base-64": "^1.0.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
"react-use": "15.3.2",
|
||||
"react-virtualized": "^9.21.2",
|
||||
"sanctuary": "^2.0.1",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^7.0.2",
|
||||
"yup": "0.32.9"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -153,7 +153,6 @@ const NotificationCenter = ({
|
|||
{!loading && buildNotifications()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.background} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,16 @@ import {
|
|||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
background: {
|
||||
position: 'absolute',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: -1,
|
||||
container: {
|
||||
'@media only screen and (max-width: 1920px)': {
|
||||
width: '30vw'
|
||||
},
|
||||
width: '40vw',
|
||||
height: '110vh',
|
||||
right: 0,
|
||||
backgroundColor: white,
|
||||
boxShadow: '0 0 14px 0 rgba(0, 0, 0, 0.24)'
|
||||
},
|
||||
container: {
|
||||
left: -200,
|
||||
top: -42,
|
||||
backgroundColor: white,
|
||||
height: '110vh'
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
|
|
@ -39,7 +33,7 @@ const styles = {
|
|||
},
|
||||
notificationIcon: ({ buttonCoords, xOffset }) => ({
|
||||
position: 'absolute',
|
||||
top: buttonCoords ? buttonCoords.y - 1 : 0,
|
||||
top: buttonCoords ? buttonCoords.y : 0,
|
||||
left: buttonCoords ? buttonCoords.x - xOffset : 0,
|
||||
cursor: 'pointer',
|
||||
background: 'transparent',
|
||||
|
|
@ -54,21 +48,33 @@ const styles = {
|
|||
backgroundColor: zircon
|
||||
},
|
||||
notificationsList: {
|
||||
width: 440,
|
||||
height: '90vh',
|
||||
maxHeight: '100vh',
|
||||
marginTop: spacer * 3,
|
||||
marginLeft: 0,
|
||||
marginRight: -50,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
backgroundColor: white,
|
||||
zIndex: 10
|
||||
},
|
||||
notificationRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
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: {
|
||||
backgroundColor: spring3
|
||||
|
|
@ -79,6 +85,9 @@ const styles = {
|
|||
marginLeft: spacer * 3
|
||||
}
|
||||
},
|
||||
readIconWrapper: {
|
||||
flexGrow: 1
|
||||
},
|
||||
unreadIcon: {
|
||||
marginLeft: spacer,
|
||||
marginTop: 5,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import prettyMs from 'pretty-ms'
|
||||
|
|
@ -8,7 +7,6 @@ import React from 'react'
|
|||
import { Label1, Label2, TL2 } from 'src/components/typography'
|
||||
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 StripesSvg } from 'src/styling/icons/stripes.svg'
|
||||
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
|
||||
|
||||
import styles from './NotificationCenter.styles'
|
||||
|
|
@ -54,36 +52,26 @@ const NotificationRow = ({
|
|||
[classes.unreadIcon]: !read
|
||||
}
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
<div
|
||||
className={classnames(
|
||||
classes.notificationRow,
|
||||
!read && valid ? classes.unread : ''
|
||||
)}>
|
||||
<Grid item xs={2} className={classes.notificationRowIcon}>
|
||||
{icon}
|
||||
</Grid>
|
||||
<Grid item container xs={7} direction="row">
|
||||
<Grid item xs={12}>
|
||||
<div className={classes.notificationRowIcon}>{icon}</div>
|
||||
<div className={classes.notificationContent}>
|
||||
<Label2 className={classes.notificationTitle}>
|
||||
{notificationTitle}
|
||||
</Label2>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TL2 className={classes.notificationBody}>{message}</TL2>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Label1 className={classes.notificationSubtitle}>{age}</Label1>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={3} style={{ zIndex: 1 }}>
|
||||
</div>
|
||||
<div className={classes.readIconWrapper}>
|
||||
<div
|
||||
onClick={() => toggleClear(id)}
|
||||
className={classnames(iconClass)}
|
||||
/>
|
||||
</Grid>
|
||||
{!valid && <StripesSvg className={classes.stripes} />}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ const Tr = ({
|
|||
onClick,
|
||||
error,
|
||||
errorMessage,
|
||||
shouldShowError,
|
||||
children,
|
||||
className,
|
||||
size,
|
||||
|
|
@ -99,7 +100,9 @@ const Tr = ({
|
|||
<Card className={classnames(classNames, className)} onClick={onClick}>
|
||||
<CardContent classes={cardClasses}>
|
||||
<div className={classes.mainContent}>{children}</div>
|
||||
{error && <div className={classes.errorContent}>{errorMessage}</div>}
|
||||
{error && shouldShowError && (
|
||||
<div className={classes.errorContent}>{errorMessage}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ const styles = {
|
|||
borderRadius: '4px'
|
||||
},
|
||||
focus: {
|
||||
color: primaryColor,
|
||||
border: '2px solid',
|
||||
borderColor: primaryColor,
|
||||
borderRadius: '4px'
|
||||
borderRadius: '4px',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
borderColor: errorColor
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React from 'react'
|
|||
|
||||
import Chip from 'src/components/Chip'
|
||||
import { Info2, Label1, Label2 } from 'src/components/typography'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
|
||||
import { cashboxStyles, gridStyles } from './Cashbox.styles'
|
||||
|
||||
|
|
@ -64,11 +65,9 @@ const CashIn = ({ currency, notes, total }) => {
|
|||
<Info2 className={classes.noMarginText}>{notes} notes</Info2>
|
||||
</div>
|
||||
<div className={classes.innerRow}>
|
||||
{/* Feature on hold until this can be calculated
|
||||
<Label1 className={classes.noMarginText}>
|
||||
{total} {currency.code}
|
||||
</Label1>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,7 +111,7 @@ const CashOut = ({
|
|||
</div>
|
||||
<div className={classes.innerRow}>
|
||||
<Label1 className={classes.noMarginText}>
|
||||
{notes * denomination} {currency.code}
|
||||
{numberToFiatAmount(notes * denomination)} {currency.code}
|
||||
</Label1>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ const Header = memo(({ tree, user }) => {
|
|||
|
||||
const handleClick = event => {
|
||||
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)
|
||||
document.querySelector('#root').classList.add('root-notifcenter-open')
|
||||
|
|
@ -132,7 +132,7 @@ const Header = memo(({ tree, user }) => {
|
|||
return (
|
||||
<NavLink
|
||||
key={idx}
|
||||
to={it.route || it.children[0].route}
|
||||
to={!R.isNil(it.children) ? it.children[0].route : it.route}
|
||||
isActive={match => {
|
||||
if (!match) return false
|
||||
setActive(it)
|
||||
|
|
@ -173,10 +173,16 @@ const Header = memo(({ tree, user }) => {
|
|||
anchorEl={anchorEl}
|
||||
className={classes.popper}
|
||||
disablePortal={false}
|
||||
placement="bottom-end"
|
||||
modifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: '100vw'
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
boundariesElement: 'viewport',
|
||||
padding: 0
|
||||
}
|
||||
}}>
|
||||
<NotificationCenter
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ const styles = {
|
|||
hasUnread: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
left: 182,
|
||||
left: 186,
|
||||
width: '9px',
|
||||
height: '9px',
|
||||
backgroundColor: secondaryColor,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const Row = ({
|
||||
id,
|
||||
index,
|
||||
elements,
|
||||
data,
|
||||
width,
|
||||
|
|
@ -48,9 +49,11 @@ const Row = ({
|
|||
[classes.row]: true,
|
||||
[classes.expanded]: expanded
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.rowWrapper}>
|
||||
<div className={classnames({ [classes.before]: expanded && id !== 0 })}>
|
||||
<div
|
||||
className={classnames({ [classes.before]: expanded && index !== 0 })}>
|
||||
<Tr
|
||||
size={size}
|
||||
className={classnames(trClasses)}
|
||||
|
|
@ -58,8 +61,9 @@ const Row = ({
|
|||
expandable && expandRow(id, data)
|
||||
onClick && onClick(data)
|
||||
}}
|
||||
error={data.error}
|
||||
errorMessage={data.errorMessage}>
|
||||
error={data.error || data.hasError}
|
||||
shouldShowError={false}
|
||||
errorMessage={data.errorMessage || data.hasError}>
|
||||
{elements.map(({ view = it => it?.toString(), ...props }, idx) => (
|
||||
<Td key={idx} {...props}>
|
||||
{view(data)}
|
||||
|
|
@ -142,6 +146,7 @@ const DataTable = ({
|
|||
width={width}
|
||||
size={rowSize}
|
||||
id={data[index].id ? data[index].id : index}
|
||||
index={index}
|
||||
expWidth={expWidth}
|
||||
elements={elements}
|
||||
data={data[index]}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export default {
|
|||
confirmationCode: {
|
||||
extend: base,
|
||||
fontSize: codeInputFontSize,
|
||||
fontFamily: fontPrimary,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 900
|
||||
},
|
||||
inline: {
|
||||
|
|
|
|||
|
|
@ -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 LitecoinLogo } from 'src/styling/logos/icon-litecoin-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'
|
||||
|
||||
|
|
@ -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 Assets = ({ balance, wallets, currency }) => {
|
||||
|
|
@ -69,7 +67,7 @@ const Assets = ({ balance, wallets, currency }) => {
|
|||
<P className={classes.fieldHeader}>Available balance</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(balance)}
|
||||
{numberToFiatAmount(balance)}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -81,7 +79,7 @@ const Assets = ({ balance, wallets, currency }) => {
|
|||
<P className={classes.fieldHeader}>Total balance in wallets</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(walletFiatSum())}
|
||||
{numberToFiatAmount(walletFiatSum())}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -93,7 +91,7 @@ const Assets = ({ balance, wallets, currency }) => {
|
|||
<P className={classes.fieldHeader}>Total assets</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(balance)}
|
||||
{numberToFiatAmount(balance)}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -144,17 +142,11 @@ const WalletInfoChip = ({ wallet, currency }) => {
|
|||
<div className={classes.walletValueWrapper}>
|
||||
<Label2 className={classes.fieldHeader}>{wallet.name} value</Label2>
|
||||
<Label2 className={classes.walletValue}>
|
||||
{wallet.amount.toFixed(1).toLocaleString('en-US', {
|
||||
maximumFractionDigits: 2
|
||||
})}{' '}
|
||||
{wallet.cryptoCode}
|
||||
{numberToFiatAmount(wallet.amount.toFixed(1))} {wallet.cryptoCode}
|
||||
</Label2>
|
||||
<Label2 className={classes.fieldHeader}>Hedged value</Label2>
|
||||
<Label2 className={classes.walletValue}>
|
||||
{wallet.fiatValue.toLocaleString('en-US', {
|
||||
maximumFractionDigits: 2
|
||||
})}{' '}
|
||||
{currency}
|
||||
{numberToFiatAmount(wallet.fiatValue)} {currency}
|
||||
</Label2>
|
||||
</div>
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import { Tooltip } from 'src/components/Tooltip'
|
|||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { H4, Info2, P } from 'src/components/typography'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
import styles from './Accounting.styles'
|
||||
|
||||
const formatCurrency = amount =>
|
||||
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_OPERATOR_BY_USERNAME = gql`
|
||||
|
|
@ -64,7 +62,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
|
|||
<P className={classes.fieldHeader}>Pazuz fiat balance</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(balance)}
|
||||
{numberToFiatAmount(balance)}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -76,7 +74,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
|
|||
<P className={classes.fieldHeader}>Hedging reserve</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(hedgingReserve)}
|
||||
{numberToFiatAmount(hedgingReserve)}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -88,7 +86,7 @@ const Assets = ({ balance, hedgingReserve, currency }) => {
|
|||
<P className={classes.fieldHeader}>Available balance</P>
|
||||
<div className={classes.totalAssetWrapper}>
|
||||
<Info2 noMargin className={classes.fieldValue}>
|
||||
{formatCurrency(balance - hedgingReserve)}
|
||||
{numberToFiatAmount(balance - hedgingReserve)}
|
||||
</Info2>
|
||||
<Info2 noMargin className={classes.fieldCurrency}>
|
||||
{R.toUpper(currency)}
|
||||
|
|
@ -114,7 +112,7 @@ const Accounting = () => {
|
|||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
const loading = operatorLoading && configLoading
|
||||
const loading = operatorLoading || configLoading
|
||||
|
||||
const operatorData = R.path(['operatorByUsername'], opData)
|
||||
|
||||
|
|
@ -143,7 +141,7 @@ const Accounting = () => {
|
|||
size: 'sm',
|
||||
textAlign: 'right',
|
||||
view: it =>
|
||||
`${formatCurrency(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}`
|
||||
`${numberToFiatAmount(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}`
|
||||
},
|
||||
{
|
||||
header: 'Balance after operation',
|
||||
|
|
@ -151,7 +149,9 @@ const Accounting = () => {
|
|||
size: 'sm',
|
||||
textAlign: 'right',
|
||||
view: it =>
|
||||
`${formatCurrency(it.fiatBalanceAfter)} ${R.toUpper(it.fiatCurrency)}`
|
||||
`${numberToFiatAmount(it.fiatBalanceAfter)} ${R.toUpper(
|
||||
it.fiatCurrency
|
||||
)}`
|
||||
},
|
||||
{
|
||||
header: 'Date',
|
||||
|
|
@ -170,19 +170,16 @@ const Accounting = () => {
|
|||
]
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<TitleSection title="Accounting" />
|
||||
<Assets
|
||||
balance={
|
||||
operatorData.fiatBalances[operatorData.preferredFiatCurrency]
|
||||
}
|
||||
balance={operatorData.fiatBalances[operatorData.preferredFiatCurrency]}
|
||||
hedgingReserve={operatorData.hedgingReserve ?? 0}
|
||||
currency={operatorData.preferredFiatCurrency}
|
||||
/>
|
||||
<H4 className={classes.tableTitle}>Fiat balance history</H4>
|
||||
<DataTable
|
||||
loading={false}
|
||||
loading={loading}
|
||||
emptyText="No transactions so far"
|
||||
elements={elements}
|
||||
data={operatorData.fundings ?? []}
|
||||
|
|
@ -190,7 +187,6 @@ const Accounting = () => {
|
|||
/>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Accounting
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { Button } from 'src/components/buttons'
|
|||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
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 CompleteStageIconSpring } from 'src/styling/icons/stage/spring/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
|
||||
</Info2>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
<div>
|
||||
<QRCode size={240} fgColor={primaryColor} value={qrCode} />
|
||||
<div className={classes.qrCodeImageWrapper}>
|
||||
<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 className={classes.qrTextWrapper}>
|
||||
<div className={classes.qrTextInfoWrapper}>
|
||||
|
|
|
|||
|
|
@ -126,6 +126,23 @@ const styles = {
|
|||
},
|
||||
errorMessage: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 UpIcon } from 'src/styling/icons/dashboard/up.svg'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { DAY, WEEK, MONTH } from 'src/utils/time'
|
||||
|
||||
import styles from './Analytics.styles'
|
||||
|
|
@ -97,9 +98,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
|
|||
<div className={classes.overviewEntry}>
|
||||
<P noMargin>{label}</P>
|
||||
<Info2 noMargin className={classes.overviewFieldWrapper}>
|
||||
<span>
|
||||
{value.toLocaleString('en-US', { maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
<span>{numberToFiatAmount(value)}</span>
|
||||
{!!currency && ` ${currency}`}
|
||||
</Info2>
|
||||
<span className={classes.overviewGrowth}>
|
||||
|
|
@ -107,7 +106,7 @@ const OverviewEntry = ({ label, value, oldValue, currency }) => {
|
|||
{R.lt(growthRate, 0) && <DownIcon height={10} />}
|
||||
{R.equals(growthRate, 0) && <EqualIcon height={10} />}
|
||||
<P noMargin className={classnames(growthClasses)}>
|
||||
{growthRate.toLocaleString('en-US', { maximumFractionDigits: 2 })}%
|
||||
{numberToFiatAmount(growthRate)}%
|
||||
</P>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import React, { memo } from 'react'
|
|||
import { Info2, Label3, P } from 'src/components/typography'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { singularOrPlural } from 'src/utils/string'
|
||||
import { formatDate, formatDateNonUtc } from 'src/utils/timezones'
|
||||
|
||||
|
|
@ -13,9 +14,6 @@ import styles from './GraphTooltip.styles'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const formatCurrency = amount =>
|
||||
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
|
||||
const GraphTooltip = ({
|
||||
coords,
|
||||
data,
|
||||
|
|
@ -67,7 +65,7 @@ const GraphTooltip = ({
|
|||
{singularOrPlural(R.length(data), 'transaction', 'transactions')}
|
||||
</P>
|
||||
<P noMargin className={classes.dotOtTransactionVolume}>
|
||||
{formatCurrency(transactions.volume)} {currency} in volume
|
||||
{numberToFiatAmount(transactions.volume)} {currency} in volume
|
||||
</P>
|
||||
<div className={classes.dotOtTransactionClasses}>
|
||||
<Label3 noMargin>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import React, { useContext } from 'react'
|
|||
import AppContext from 'src/AppContext'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { H4, Label2, P, Info2 } from 'src/components/typography'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
|
||||
import styles from './Assets.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const GET_OPERATOR_BY_USERNAME = gql`
|
||||
|
|
@ -105,7 +107,7 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
|
|||
</Cell>
|
||||
<Cell align="right">
|
||||
<P>{`${selectAmountPrefix(asset)}
|
||||
${formatCurrency(Math.abs(asset.amount))} ${
|
||||
${numberToFiatAmount(Math.abs(asset.amount))} ${
|
||||
asset.currency
|
||||
}`}</P>
|
||||
</Cell>
|
||||
|
|
@ -117,7 +119,9 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
|
|||
<Info2>{`Total ${R.toLower(title)}`}</Info2>
|
||||
</Cell>
|
||||
<Cell align="right">
|
||||
<Info2>{`${formatCurrency(totalAmount)} ${currency}`}</Info2>
|
||||
<Info2>{`${numberToFiatAmount(
|
||||
totalAmount
|
||||
)} ${currency}`}</Info2>
|
||||
</Cell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
|
|
@ -128,9 +132,6 @@ const AssetsAmountTable = ({ title, data = [], numToRender }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const formatCurrency = amount =>
|
||||
amount.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
|
||||
const Assets = () => {
|
||||
const classes = useStyles()
|
||||
const { userData } = useContext(AppContext)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import base64 from 'base-64'
|
||||
import { Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
|
@ -120,6 +121,9 @@ const Input2FAState = ({ state, dispatch }) => {
|
|||
<TL1 className={classes.info}>
|
||||
Enter your two-factor authentication code
|
||||
</TL1>
|
||||
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||
<Form>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={state.twoFAField}
|
||||
|
|
@ -128,6 +132,9 @@ const Input2FAState = ({ state, dispatch }) => {
|
|||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||
</Form>
|
||||
</Formik>
|
||||
<div className={classes.twofaFooter}>
|
||||
{errorMessage && <P className={classes.errorMessage}>{errorMessage}</P>}
|
||||
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import { Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useReducer, useState } from 'react'
|
||||
|
|
@ -101,6 +102,20 @@ const Reset2FA = () => {
|
|||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
reset2FA({
|
||||
variables: {
|
||||
token: token,
|
||||
userID: state.userID,
|
||||
code: twoFAConfirmation
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
|
|
@ -152,6 +167,9 @@ const Reset2FA = () => {
|
|||
</ActionButton>
|
||||
</div>
|
||||
<div className={classes.confirm2FAInput}>
|
||||
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||
<Form>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFAConfirmation}
|
||||
|
|
@ -160,25 +178,19 @@ const Reset2FA = () => {
|
|||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className={classes.enterButton}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
reset2FA({
|
||||
variables: {
|
||||
token: token,
|
||||
userID: state.userID,
|
||||
code: twoFAConfirmation
|
||||
}
|
||||
})
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQuery, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import base64 from 'base-64'
|
||||
import { Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useContext, useState } from 'react'
|
||||
|
|
@ -125,6 +126,14 @@ const Setup2FAState = ({ state, dispatch }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
setup2FA(mutationOptions)
|
||||
}
|
||||
|
||||
return (
|
||||
secret &&
|
||||
otpauth && (
|
||||
|
|
@ -159,6 +168,9 @@ const Setup2FAState = ({ state, dispatch }) => {
|
|||
</ActionButton>
|
||||
</div>
|
||||
<div className={classes.confirm2FAInput}>
|
||||
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||
<Form>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFAConfirmation}
|
||||
|
|
@ -167,20 +179,15 @@ const Setup2FAState = ({ state, dispatch }) => {
|
|||
error={invalidToken}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (twoFAConfirmation.length !== 6) {
|
||||
setInvalidToken(true)
|
||||
return
|
||||
}
|
||||
setup2FA(mutationOptions)
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
<Button onClick={handleSubmit} buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ const styles = {
|
|||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
},
|
||||
enterButton: {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { spacer, white, errorColor } from 'src/styling/variables'
|
||||
import { spacer, white } from 'src/styling/variables'
|
||||
const styles = {
|
||||
grid: {
|
||||
flex: 1,
|
||||
|
|
@ -32,7 +32,7 @@ const styles = {
|
|||
marginLeft: 8
|
||||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
marginTop: 20
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as R from 'ramda'
|
|||
import React from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
|
|
@ -32,7 +33,10 @@ const BlackListModal = ({
|
|||
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
|
||||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm'
|
||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
||||
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
|
||||
XMR:
|
||||
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -53,9 +57,8 @@ const BlackListModal = ({
|
|||
.trim()
|
||||
.required('An address is required')
|
||||
})}
|
||||
onSubmit={({ address }, { resetForm }) => {
|
||||
onSubmit={({ address }) => {
|
||||
handleAddToBlacklist(address.trim())
|
||||
resetForm()
|
||||
}}>
|
||||
<Form id="address-form">
|
||||
<H3 className={classes.modalTitle}>
|
||||
|
|
@ -63,7 +66,6 @@ const BlackListModal = ({
|
|||
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
||||
: ''}
|
||||
</H3>
|
||||
<span className={classes.error}>{errorMsg}</span>
|
||||
<Field
|
||||
name="address"
|
||||
fullWidth
|
||||
|
|
@ -72,6 +74,9 @@ const BlackListModal = ({
|
|||
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
||||
component={TextInput}
|
||||
/>
|
||||
{!R.isNil(errorMsg) && (
|
||||
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
|
||||
)}
|
||||
</Form>
|
||||
</Formik>
|
||||
<div className={classes.footer}>
|
||||
|
|
|
|||
|
|
@ -162,14 +162,14 @@ const WizardStep = ({
|
|||
|
||||
{lastStep && (
|
||||
<div className={classes.disclaimer}>
|
||||
<Info2 className={classes.title}>Cash-out Bill Count</Info2>
|
||||
<Info2 className={classes.title}>Cash Cassette Bill Count</Info2>
|
||||
<P>
|
||||
<WarningIcon className={classes.disclaimerIcon} />
|
||||
When enabling cash-out, your bill count will be automatically set to
|
||||
zero. Make sure you physically put cash inside the cash cassettes to
|
||||
allow the machine to dispense it to your users. If you already did,
|
||||
make sure you set the correct cash-out bill count for this machine
|
||||
on your Cash Cassettes tab under Maintenance.
|
||||
make sure you set the correct cash cassette bill count for this
|
||||
machine on your Cash Boxes & Cassettes tab under Maintenance.
|
||||
</P>
|
||||
|
||||
<Info2 className={classes.title}>Default Commissions</Info2>
|
||||
|
|
|
|||
81
new-lamassu-admin/src/pages/Customers/CustomerPhotos.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -24,6 +24,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
|
|||
|
||||
import CustomerData from './CustomerData'
|
||||
import CustomerNotes from './CustomerNotes'
|
||||
import CustomerPhotos from './CustomerPhotos'
|
||||
import styles from './CustomerProfile.styles'
|
||||
import {
|
||||
CustomerDetails,
|
||||
|
|
@ -31,7 +32,7 @@ import {
|
|||
CustomerSidebar,
|
||||
Wizard
|
||||
} from './components'
|
||||
import { getFormattedPhone, getName } from './helper'
|
||||
import { getFormattedPhone, getName, formatPhotosData } from './helper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
|
@ -367,12 +368,24 @@ const CustomerProfile = memo(() => {
|
|||
const isCustomerData = clickedItem === 'customerData'
|
||||
const isOverview = clickedItem === 'overview'
|
||||
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 classes = useStyles({ blocked })
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -406,29 +419,26 @@ const CustomerProfile = memo(() => {
|
|||
/>
|
||||
</div>
|
||||
<Label1 className={classes.actionLabel}>Actions</Label1>
|
||||
<div>
|
||||
<div className={classes.actionBar}>
|
||||
<ActionButton
|
||||
className={classes.customerManualDataEntry}
|
||||
className={classes.actionButton}
|
||||
color="primary"
|
||||
Icon={DataIcon}
|
||||
InverseIcon={DataReversedIcon}
|
||||
onClick={() => setWizard(true)}>
|
||||
{`Manual data entry`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
className={classes.customerDiscount}
|
||||
className={classes.actionButton}
|
||||
color="primary"
|
||||
Icon={Discount}
|
||||
InverseIcon={DiscountReversedIcon}
|
||||
onClick={() => {}}>
|
||||
{`Add individual discount`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div>
|
||||
{isSuspended && (
|
||||
<ActionButton
|
||||
className={classes.actionButton}
|
||||
color="primary"
|
||||
Icon={AuthorizeIcon}
|
||||
InverseIcon={AuthorizeReversedIcon}
|
||||
|
|
@ -442,7 +452,7 @@ const CustomerProfile = memo(() => {
|
|||
)}
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.customerBlock}
|
||||
className={classes.actionButton}
|
||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||
InverseIcon={
|
||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||
|
|
@ -458,7 +468,7 @@ const CustomerProfile = memo(() => {
|
|||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.retrieveInformation}
|
||||
className={classes.actionButton}
|
||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||
InverseIcon={
|
||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||
|
|
@ -488,6 +498,7 @@ const CustomerProfile = memo(() => {
|
|||
justifyContent="space-between">
|
||||
<CustomerDetails
|
||||
customer={customerData}
|
||||
photosData={photosData}
|
||||
locale={locale}
|
||||
setShowCompliance={() => setShowCompliance(!showCompliance)}
|
||||
/>
|
||||
|
|
@ -524,6 +535,11 @@ const CustomerProfile = memo(() => {
|
|||
timezone={timezone}></CustomerNotes>
|
||||
</div>
|
||||
)}
|
||||
{isPhotos && (
|
||||
<div>
|
||||
<CustomerPhotos photosData={photosData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
|
|
|
|||
|
|
@ -15,29 +15,16 @@ export default {
|
|||
customerDetails: {
|
||||
marginBottom: 18
|
||||
},
|
||||
customerBlock: props => ({
|
||||
actionButton: {
|
||||
margin: [[0, 0, 4, 0]],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[0, 0, 4, 0]],
|
||||
padding: [[0, props.blocked ? 35 : 48, 0]]
|
||||
}),
|
||||
customerDiscount: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[0, 0, 4, 0]],
|
||||
padding: [[0, 23.5, 0]]
|
||||
justifyContent: 'center'
|
||||
},
|
||||
customerManualDataEntry: {
|
||||
actionBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[8, 0, 4, 0]],
|
||||
padding: [[0, 40.5, 0]]
|
||||
},
|
||||
retrieveInformation: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [[0, 0, 4, 0]],
|
||||
padding: [[0, 32.5, 0]]
|
||||
flexDirection: 'column',
|
||||
width: 219
|
||||
},
|
||||
panels: {
|
||||
display: 'flex'
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import PhotosCard from './PhotosCard'
|
|||
|
||||
const useStyles = makeStyles(mainStyles)
|
||||
|
||||
const CustomerDetails = memo(
|
||||
({ txData, customer, locale, setShowCompliance }) => {
|
||||
const CustomerDetails = memo(({ customer, photosData, locale }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const idNumber = R.path(['idCardData', 'documentNumber'])(customer)
|
||||
|
|
@ -45,27 +44,14 @@ const CustomerDetails = memo(
|
|||
|
||||
return (
|
||||
<Box display="flex">
|
||||
<PhotosCard
|
||||
frontCameraData={R.pick(['frontCameraPath', 'frontCameraAt'])(
|
||||
customer
|
||||
)}
|
||||
txPhotosData={
|
||||
txData &&
|
||||
R.map(R.pick(['id', 'txCustomerPhotoPath', 'txCustomerPhotoAt']))(
|
||||
txData
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PhotosCard photosData={photosData} />
|
||||
<Box display="flex" flexDirection="column">
|
||||
<div className={classes.name}>
|
||||
<IdIcon className={classes.idIcon} />
|
||||
<H2 noMargin>
|
||||
{name.length
|
||||
? name
|
||||
: getFormattedPhone(
|
||||
R.path(['phone'])(customer),
|
||||
locale.country
|
||||
)}
|
||||
: getFormattedPhone(R.path(['phone'])(customer), locale.country)}
|
||||
</H2>
|
||||
</div>
|
||||
<Box display="flex" mt="auto">
|
||||
|
|
@ -93,7 +79,6 @@ const CustomerDetails = memo(
|
|||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export default CustomerDetails
|
||||
|
|
|
|||
|
|
@ -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 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 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'
|
||||
|
||||
|
|
@ -33,6 +35,12 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
|
|||
display: 'Notes',
|
||||
Icon: NoteIcon,
|
||||
InverseIcon: NoteReversedIcon
|
||||
},
|
||||
{
|
||||
code: 'photos',
|
||||
display: 'Photos & files',
|
||||
Icon: Photos,
|
||||
InverseIcon: PhotosReversedIcon
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,58 +4,21 @@ 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 { 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 { URI } from 'src/utils/apollo'
|
||||
|
||||
import CopyToClipboard from '../../Transactions/CopyToClipboard'
|
||||
|
||||
import styles from './PhotosCard.styles'
|
||||
import PhotosCarousel from './PhotosCarousel'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Label = ({ children }) => {
|
||||
const classes = useStyles()
|
||||
return <Label1 className={classes.label}>{children}</Label1>
|
||||
}
|
||||
|
||||
const PhotosCard = memo(({ frontCameraData, txPhotosData }) => {
|
||||
const PhotosCard = memo(({ photosData }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { zircon, backgroundColor, offColor } from 'src/styling/variables'
|
||||
|
||||
const { p } = typographyStyles
|
||||
import { zircon, backgroundColor } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
photo: {
|
||||
|
|
@ -41,43 +38,5 @@ export default {
|
|||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,12 +70,7 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
|
|||
|
||||
const tableElements = [
|
||||
{
|
||||
header: 'Machine',
|
||||
width: 160,
|
||||
view: R.path(['machineName'])
|
||||
},
|
||||
{
|
||||
width: 125,
|
||||
width: 40,
|
||||
view: it => (
|
||||
<>
|
||||
{it.txClass === 'cashOut' ? (
|
||||
|
|
@ -86,6 +81,11 @@ const TransactionsList = ({ customer, data, loading, locale }) => {
|
|||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Machine',
|
||||
width: 160,
|
||||
view: R.path(['machineName'])
|
||||
},
|
||||
{
|
||||
header: 'Transaction ID',
|
||||
width: 145,
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import CustomerSidebar from './CustomerSidebar'
|
|||
import EditableCard from './EditableCard'
|
||||
import Field from './Field'
|
||||
import IdDataCard from './IdDataCard'
|
||||
import PhotosCarousel from './PhotosCarousel'
|
||||
import TransactionsList from './TransactionsList'
|
||||
import Upload from './Upload'
|
||||
|
||||
export {
|
||||
PhotosCarousel,
|
||||
CustomerDetails,
|
||||
IdDataCard,
|
||||
TransactionsList,
|
||||
|
|
|
|||
|
|
@ -209,10 +209,41 @@ const 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 {
|
||||
getAuthorizedStatus,
|
||||
getFormattedPhone,
|
||||
getName,
|
||||
entryType,
|
||||
customElements
|
||||
customElements,
|
||||
formatPhotosData
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { useHistory } from 'react-router-dom'
|
|||
|
||||
import { P } from 'src/components/typography/index'
|
||||
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 AlertLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
|
||||
|
||||
import styles from './Alerts.styles'
|
||||
|
|
@ -49,7 +49,7 @@ const AlertsTable = ({ numToRender, alerts, machines }) => {
|
|||
<Wrench style={{ height: 23, width: 23, marginRight: 8 }} />
|
||||
)}
|
||||
<P className={classes.listItemText}>{alertMessage(alert)}</P>
|
||||
<LinkIcon
|
||||
<AlertLinkIcon
|
||||
className={classes.linkIcon}
|
||||
onClick={() => history.push(links[alert.type] || '/dashboard')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
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 { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.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 Footer from './Footer'
|
||||
|
|
@ -46,20 +46,20 @@ const Dashboard = () => {
|
|||
<>
|
||||
<TitleSection title="Dashboard">
|
||||
<div className={classes.headerLabels}>
|
||||
<>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.headerLabelContainer,
|
||||
classes.headerLabelContainerMargin
|
||||
)}>
|
||||
<TxOutIcon />
|
||||
<span className={classes.headerLabelSpan}>Cash-out</span>
|
||||
</div>
|
||||
<div className={classes.headerLabelContainer}>
|
||||
<div>
|
||||
<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>
|
||||
</TitleSection>
|
||||
<div className={classes.root}>
|
||||
|
|
|
|||
|
|
@ -12,18 +12,26 @@ const { label1 } = typographyStyles
|
|||
const styles = {
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
headerLabelContainerMargin: {
|
||||
marginRight: 24
|
||||
},
|
||||
headerLabelContainer: {
|
||||
flexDirection: 'row',
|
||||
'& > div:first-child': {
|
||||
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,
|
||||
marginLeft: 6
|
||||
marginLeft: 7
|
||||
}
|
||||
},
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
|
|
@ -27,19 +28,15 @@ const GET_DATA = gql`
|
|||
}
|
||||
}
|
||||
`
|
||||
|
||||
BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
const Footer = () => {
|
||||
const { data } = useQuery(GET_DATA)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [delayedExpand, setDelayedExpand] = useState(null)
|
||||
|
||||
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
||||
const classes = useStyles({
|
||||
bigFooter: R.keys(withCommissions).length > 8,
|
||||
expanded
|
||||
})
|
||||
const classes = useStyles()
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
className={classes.mouseWatcher}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
/>
|
||||
<div className={classes.content}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid container className={classes.footerContainer}>
|
||||
<div className={classes.footer1}>
|
||||
<div className={classes.content1}>
|
||||
<Grid container>
|
||||
<Grid container className={classes.footerContainer1}>
|
||||
{R.keys(withCommissions).map(key => renderFooterItem(key))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
<div className={classes.footer} />
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,52 +17,34 @@ const styles = {
|
|||
txOutMargin: {
|
||||
marginLeft: spacer * 3
|
||||
},
|
||||
footer: ({ expanded, bigFooter }) => ({
|
||||
height:
|
||||
expanded && bigFooter
|
||||
? spacer * 12 * 3 + spacer * 3
|
||||
: expanded
|
||||
? spacer * 12 * 2 + spacer * 2
|
||||
: spacer * 12,
|
||||
tickerLabel: {
|
||||
color: offColor,
|
||||
marginTop: -5
|
||||
},
|
||||
footer1: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
backgroundColor: white,
|
||||
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,
|
||||
position: 'fixed',
|
||||
bottom: -spacer,
|
||||
transform: 'translateY(-100%)'
|
||||
boxShadow: '0px -1px 10px 0px rgba(50, 50, 50, 0.1)',
|
||||
minHeight: spacer * 12,
|
||||
transition: 'min-height 0.5s ease-out',
|
||||
'&:hover': {
|
||||
transition: 'min-height 0.5s ease-in',
|
||||
minHeight: 200
|
||||
}
|
||||
},
|
||||
footerContainer: ({ expanded, bigFooter }) => ({
|
||||
marginLeft: spacer * 5,
|
||||
height: 100,
|
||||
marginTop: expanded && bigFooter ? -300 : expanded ? -200 : -100,
|
||||
overflow: !expanded && 'hidden'
|
||||
}),
|
||||
mouseWatcher: ({ expanded, bigFooter }) => ({
|
||||
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
|
||||
})
|
||||
content1: {
|
||||
width: 1200,
|
||||
maxHeight: 100,
|
||||
backgroundColor: white,
|
||||
zIndex: 2,
|
||||
bottom: -spacer,
|
||||
margin: '0 auto'
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ import { java, neon, white } from 'src/styling/variables'
|
|||
const styles = {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
height: 130,
|
||||
marginTop: -8
|
||||
height: 142
|
||||
},
|
||||
percentageBox: {
|
||||
height: 130,
|
||||
height: 142,
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
|
@ -33,11 +32,11 @@ const styles = {
|
|||
borderRadius: 2
|
||||
},
|
||||
inWidth: {
|
||||
width: value => `${value}%`
|
||||
width: value => `${value}%`,
|
||||
marginRight: value => (value === 100 ? 0 : 4)
|
||||
},
|
||||
outWidth: {
|
||||
width: value => `${100 - value}%`,
|
||||
marginRight: 4
|
||||
width: value => `${100 - value}%`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,14 +58,6 @@ const PercentageChart = ({ cashIn, cashOut }) => {
|
|||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div
|
||||
className={classnames(
|
||||
percentageClasses,
|
||||
classes.outColor,
|
||||
classes.outWidth
|
||||
)}>
|
||||
{buildPercentageView(100 - value, 'cashOut')}
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
percentageClasses,
|
||||
|
|
@ -75,6 +66,14 @@ const PercentageChart = ({ cashIn, cashOut }) => {
|
|||
)}>
|
||||
{buildPercentageView(value, 'cashIn')}
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
percentageClasses,
|
||||
classes.outColor,
|
||||
classes.outWidth
|
||||
)}>
|
||||
{buildPercentageView(100 - value, 'cashOut')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const RefLineChart = ({
|
|||
const svg = d3.select(svgRef.current)
|
||||
const margin = { top: 0, right: 0, bottom: 0, left: 0 }
|
||||
const width = 336 - margin.left - margin.right
|
||||
const height = 128 - margin.top - margin.bottom
|
||||
const height = 140 - margin.top - margin.bottom
|
||||
|
||||
const massageData = () => {
|
||||
// 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
|
||||
.scaleLinear()
|
||||
// 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]])
|
||||
const x = d3
|
||||
.scaleTime()
|
||||
|
|
|
|||
|
|
@ -1,197 +1,357 @@
|
|||
import BigNumber from 'bignumber.js'
|
||||
import * as d3 from 'd3'
|
||||
import { add } from 'date-fns/fp'
|
||||
import React, { useEffect, useRef, useCallback } from 'react'
|
||||
import { getTimezoneOffset } from 'date-fns-tz'
|
||||
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 { formatDate, toUtc } from 'src/utils/timezones'
|
||||
import {
|
||||
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 svgRef = useRef()
|
||||
const drawGraph = useCallback(() => {
|
||||
const svg = d3.select(svgRef.current)
|
||||
const margin = { top: 25, right: 0, bottom: 25, left: 15 }
|
||||
const width = 555 - margin.left - margin.right
|
||||
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
|
||||
// (this is because the Y axis looks best with multiples of 100)
|
||||
const findMaxY = () => {
|
||||
if (realData.length === 0) return 100
|
||||
const maxvalueTx =
|
||||
100 * Math.ceil(d3.max(realData, t => parseFloat(t.fiat)) / 100)
|
||||
const maxY = Math.max(100, maxvalueTx)
|
||||
if (maxY % 1000 === 0) return maxY + 100
|
||||
return maxY
|
||||
const Graph = ({ data, timeFrame, timezone }) => {
|
||||
const ref = useRef(null)
|
||||
|
||||
const GRAPH_HEIGHT = 250
|
||||
const GRAPH_WIDTH = 555
|
||||
const GRAPH_MARGIN = useMemo(
|
||||
() => ({
|
||||
top: 20,
|
||||
right: 0.5,
|
||||
bottom: 27,
|
||||
left: 43.5
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
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 => {
|
||||
switch (timeFrame) {
|
||||
case 'Week':
|
||||
return d3.timeFormat('%a %d')(v)
|
||||
case 'Month':
|
||||
return d3.timeFormat('%b %d')(v)
|
||||
default:
|
||||
return formatDate(v, timezone, 'HH:mm')
|
||||
}
|
||||
const dataPoints = useMemo(
|
||||
() => ({
|
||||
Day: {
|
||||
freq: 24,
|
||||
step: 60 * 60 * 1000,
|
||||
tick: d3.utcHour.every(4),
|
||||
labelFormat: '%H:%M'
|
||||
},
|
||||
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 {
|
||||
nice: 7,
|
||||
ticks: 7,
|
||||
subtractDays: 7,
|
||||
timeRange: [50, 500]
|
||||
}
|
||||
case 'Month':
|
||||
return {
|
||||
nice: 6,
|
||||
ticks: 6,
|
||||
subtractDays: 30,
|
||||
timeRange: [50, 500]
|
||||
}
|
||||
default:
|
||||
return {
|
||||
nice: null,
|
||||
ticks: 4,
|
||||
subtractDays: 1,
|
||||
timeRange: [50, 500]
|
||||
}
|
||||
previous:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[previousDateMonth]
|
||||
: `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
|
||||
current:
|
||||
currentDateMonth !== previousDateMonth
|
||||
? months[currentDateMonth]
|
||||
: `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buildTicks = useCallback(
|
||||
domain => {
|
||||
const points = []
|
||||
|
||||
const roundDate = d => {
|
||||
const step = dataPoints[timeFrame].step
|
||||
return new Date(Math.ceil(d.valueOf() / step) * step)
|
||||
}
|
||||
|
||||
// sets width of the graph
|
||||
svg.attr('width', width)
|
||||
for (let i = 0; i <= dataPoints[timeFrame].freq; i++) {
|
||||
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
|
||||
svg
|
||||
.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', width)
|
||||
.attr('height', height + margin.top)
|
||||
.attr('fill', backgroundColor)
|
||||
return points
|
||||
},
|
||||
[NOW, dataPoints, timeFrame]
|
||||
)
|
||||
|
||||
// declare g variable where more svg components will be attached
|
||||
const g = svg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
const x = d3
|
||||
.scaleUtc()
|
||||
.domain(periodDomains[timeFrame])
|
||||
.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
|
||||
.scaleLinear()
|
||||
.range([height, 0])
|
||||
.domain([0, maxY])
|
||||
.nice(3)
|
||||
const x = d3
|
||||
.scaleTime()
|
||||
.domain([
|
||||
add({ days: -xAxisSettings.subtractDays }, new Date()).valueOf(),
|
||||
new Date().valueOf()
|
||||
0,
|
||||
(d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.05
|
||||
])
|
||||
.range(xAxisSettings.timeRange)
|
||||
.nice(xAxisSettings.nice)
|
||||
.nice()
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const timeValue = s => {
|
||||
const date = toUtc(s)
|
||||
return x(date.valueOf())
|
||||
}
|
||||
|
||||
// horizontal gridlines
|
||||
const makeYGridlines = () => {
|
||||
return d3.axisLeft(y).ticks(4)
|
||||
}
|
||||
g.append('g')
|
||||
.style('color', '#eef1ff')
|
||||
.call(
|
||||
makeYGridlines()
|
||||
.tickSize(-width)
|
||||
.tickFormat('')
|
||||
const buildBackground = useCallback(
|
||||
g => {
|
||||
g.append('rect')
|
||||
.attr('x', 0)
|
||||
.attr('y', GRAPH_MARGIN.top)
|
||||
.attr('width', GRAPH_WIDTH)
|
||||
.attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.top - GRAPH_MARGIN.bottom)
|
||||
.attr('fill', backgroundColor)
|
||||
},
|
||||
[GRAPH_MARGIN]
|
||||
)
|
||||
|
||||
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())
|
||||
|
||||
/* 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')
|
||||
.attr('dy', '1.5em')
|
||||
// this is for the x axis line. It is the same color as the horizontal grid lines
|
||||
g.append('g')
|
||||
.attr('transform', 'translate(0,' + height + ')')
|
||||
.style('color', '#eef1ff')
|
||||
.call(
|
||||
d3
|
||||
.axisBottom(x)
|
||||
.ticks(6)
|
||||
.tickSize(0)
|
||||
.tickFormat('')
|
||||
.attr('dy', '-0.25rem'),
|
||||
[GRAPH_MARGIN, y]
|
||||
)
|
||||
.selectAll('text')
|
||||
.attr('dy', '1.5em')
|
||||
|
||||
// Y axis
|
||||
g.append('g')
|
||||
.style('font-size', '13px')
|
||||
.style('color', '#5f668a')
|
||||
.style('font-family', 'MuseoSans')
|
||||
.style('margin-top', '11px')
|
||||
.call(
|
||||
const buildGrid = useCallback(
|
||||
g => {
|
||||
g.attr('stroke', subheaderDarkColor)
|
||||
.attr('fill', subheaderDarkColor)
|
||||
// Vertical lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(buildTicks(x.domain()))
|
||||
.join('line')
|
||||
.attr('x1', d => 0.5 + x(d))
|
||||
.attr('x2', d => 0.5 + x(d))
|
||||
.attr('y1', GRAPH_MARGIN.top)
|
||||
.attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
|
||||
.attr('stroke-width', 1)
|
||||
)
|
||||
// Horizontal lines
|
||||
.call(g =>
|
||||
g
|
||||
.append('g')
|
||||
.selectAll('line')
|
||||
.data(
|
||||
d3
|
||||
.axisLeft(y)
|
||||
.ticks(4)
|
||||
.tickSize(0)
|
||||
.scale()
|
||||
.ticks(5)
|
||||
)
|
||||
.call(g => g.select('.domain').remove())
|
||||
.selectAll('text')
|
||||
.attr('dy', '-0.40em')
|
||||
.attr('dx', '3em')
|
||||
|
||||
// Append dots
|
||||
const dots = svg
|
||||
.join('line')
|
||||
.attr('y1', d => 0.5 + y(d))
|
||||
.attr('y2', d => 0.5 + y(d))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
||||
)
|
||||
// Thick vertical lines
|
||||
.call(g =>
|
||||
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
|
||||
.selectAll('circle')
|
||||
.data(realData)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('cx', d => timeValue(d.created))
|
||||
.attr('cy', d => y(d.fiat))
|
||||
.attr('r', 4)
|
||||
.style('fill', d => (d.txClass === 'cashIn' ? java : neon))
|
||||
}, [realData, timeFrame, timezone])
|
||||
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', '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(() => {
|
||||
// first we clear old chart DOM elements on component update
|
||||
d3.select(svgRef.current)
|
||||
d3.select(ref.current)
|
||||
.selectAll('*')
|
||||
.remove()
|
||||
drawGraph()
|
||||
}, [drawGraph])
|
||||
drawChart()
|
||||
}, [drawChart])
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg ref={svgRef} />
|
||||
</>
|
||||
)
|
||||
return <svg ref={ref} />
|
||||
}
|
||||
export default RefScatterplot
|
||||
|
||||
export default Graph
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import * as R from 'ramda'
|
|||
import React, { useState } from 'react'
|
||||
|
||||
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 PercentNeutralIcon } from 'src/styling/icons/dashboard/equal.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 { timezones } from 'src/utils/timezone-list'
|
||||
import { toTimezone } from 'src/utils/timezones'
|
||||
|
||||
import PercentageChart from './Graphs/PercentageChart'
|
||||
|
|
@ -199,9 +201,30 @@ const SystemPerformance = () => {
|
|||
</Grid>
|
||||
{/* todo new customers */}
|
||||
</Grid>
|
||||
<Grid container className={classes.gridContainer}>
|
||||
<Grid container className={classes.txGraphContainer}>
|
||||
<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
|
||||
timeFrame={selectedRange}
|
||||
data={transactionsToShow}
|
||||
|
|
@ -209,9 +232,9 @@ const SystemPerformance = () => {
|
|||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container className={classes.gridContainer}>
|
||||
<Grid container className={classes.commissionGraphContainer}>
|
||||
<Grid item xs={8}>
|
||||
<Label2 className={classes.labelMargin}>
|
||||
<Label2 noMargin className={classes.commissionProfitTitle}>
|
||||
Profit from commissions
|
||||
</Label2>
|
||||
<div className={classes.profitContainer}>
|
||||
|
|
@ -233,23 +256,22 @@ const SystemPerformance = () => {
|
|||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<Label2 className={classes.labelMargin}>Direction</Label2>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
className={classnames(
|
||||
classes.directionLabelContainer,
|
||||
classes.dirLabContMargin
|
||||
)}>
|
||||
<div className={classes.outSquare} />
|
||||
<Label1 className={classes.directionLabel}>Out</Label1>
|
||||
</Grid>
|
||||
<Grid item className={classes.directionLabelContainer}>
|
||||
<div className={classes.inSquare} />
|
||||
<Label1 className={classes.directionLabel}>In</Label1>
|
||||
</Grid>
|
||||
<Grid container className={classes.graphHeader}>
|
||||
<Label2 noMargin>Direction</Label2>
|
||||
<div className={classes.labelWrapper}>
|
||||
<div>
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={java} />
|
||||
</svg>
|
||||
<Label1 noMargin>In</Label1>
|
||||
</div>
|
||||
<div>
|
||||
<svg width={8} height={8}>
|
||||
<rect width={8} height={8} rx={2} fill={neon} />
|
||||
</svg>
|
||||
<Label1 noMargin>Out</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<PercentageChart
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
offColor,
|
||||
offDarkColor,
|
||||
spacer,
|
||||
primaryColor,
|
||||
fontSize3,
|
||||
|
|
@ -7,8 +8,6 @@ import {
|
|||
fontColor,
|
||||
spring4,
|
||||
tomato,
|
||||
java,
|
||||
neon,
|
||||
comet
|
||||
} from 'src/styling/variables'
|
||||
|
||||
|
|
@ -67,12 +66,6 @@ const styles = {
|
|||
navContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
profitLabel: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: fontColor
|
||||
},
|
||||
percentUp: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
|
|
@ -96,34 +89,14 @@ const styles = {
|
|||
profitContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: '0 26px -30px 16px',
|
||||
margin: '23px 26px -30px 16px',
|
||||
position: 'relative'
|
||||
},
|
||||
gridContainer: {
|
||||
marginTop: 30,
|
||||
height: 225
|
||||
},
|
||||
inSquare: {
|
||||
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
|
||||
profitLabel: {
|
||||
fontSize: fontSize3,
|
||||
fontFamily: fontSecondary,
|
||||
fontWeight: 700,
|
||||
color: fontColor
|
||||
},
|
||||
directionIcon: {
|
||||
width: 16,
|
||||
|
|
@ -131,12 +104,50 @@ const styles = {
|
|||
marginBottom: -2,
|
||||
marginRight: 4
|
||||
},
|
||||
labelMargin: {
|
||||
marginBottom: 20,
|
||||
marginRight: 32
|
||||
},
|
||||
emptyTransactions: {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -212,7 +212,9 @@ const Funding = () => {
|
|||
<div className={classes.addressWrapper}>
|
||||
<div className={classes.mono}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
<CopyToClipboard
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
key={selected.cryptoCode}>
|
||||
{formatAddress(
|
||||
selected.cryptoCode,
|
||||
selected.fundingAddress
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as R from 'ramda'
|
|||
import * as Yup from 'yup'
|
||||
|
||||
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 = []) => {
|
||||
return R.filter(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { DeleteDialog } from 'src/components/DeleteDialog'
|
|||
import { Link, Button, IconButton } from 'src/components/buttons'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
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 DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
|
|
@ -49,7 +48,6 @@ const GET_CUSTOMERS = gql`
|
|||
id
|
||||
phone
|
||||
idCardData
|
||||
phone
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -64,7 +62,9 @@ const IndividualDiscounts = () => {
|
|||
const [showModal, setShowModal] = useState(false)
|
||||
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(
|
||||
GET_CUSTOMERS
|
||||
)
|
||||
|
|
@ -102,12 +102,6 @@ const IndividualDiscounts = () => {
|
|||
<div className={classes.identification}>
|
||||
<PhoneIdIcon />
|
||||
<span>{customer.phone}</span>
|
||||
{customer?.idCardData?.documentNumber && (
|
||||
<>
|
||||
<CardIdIcon />
|
||||
<span>{customer?.idCardData?.documentNumber}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -160,11 +154,12 @@ const IndividualDiscounts = () => {
|
|||
}
|
||||
]
|
||||
|
||||
const isLoading = loading || customerLoading
|
||||
const loading = discountLoading || customerLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||
{!loading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||
<>
|
||||
<Box
|
||||
marginBottom={4}
|
||||
marginTop={-7}
|
||||
|
|
@ -175,9 +170,6 @@ const IndividualDiscounts = () => {
|
|||
Add new code
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
{!isLoading && !R.isEmpty(discountResponse.individualDiscounts) && (
|
||||
<>
|
||||
<DataTable
|
||||
elements={elements}
|
||||
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">
|
||||
<Label3>
|
||||
It seems there are no active individual customer discounts on your
|
||||
|
|
|
|||
|
|
@ -81,16 +81,21 @@ const Logs = () => {
|
|||
|
||||
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 { data: logsResponse, loading } = useQuery(GET_MACHINE_LOGS, {
|
||||
const { data: logsResponse, loading: logsLoading } = useQuery(
|
||||
GET_MACHINE_LOGS,
|
||||
{
|
||||
variables: { deviceId, limit: NUM_LOG_RESULTS },
|
||||
skip: !selected,
|
||||
onCompleted: () => setSaveMessage('')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (machineResponse?.machines?.length && !selected) {
|
||||
setSelected(machineResponse?.machines[0])
|
||||
|
|
@ -100,6 +105,8 @@ const Logs = () => {
|
|||
return R.path(['deviceId'])(selected) === it.deviceId
|
||||
}
|
||||
|
||||
const loading = machinesLoading || configLoading || logsLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const widthsByNumberOfCassettes = {
|
|||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required('Required'),
|
||||
cashbox: Yup.number()
|
||||
.label('Cashbox')
|
||||
.label('Cash box')
|
||||
.required()
|
||||
.integer()
|
||||
.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 [wizard, setWizard] = useState(false)
|
||||
|
|
@ -101,11 +101,15 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
const elements = [
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cashbox',
|
||||
header: 'Cash box',
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].cashbox,
|
||||
stripe: false,
|
||||
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,
|
||||
inputProps: {
|
||||
|
|
@ -119,7 +123,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
it => {
|
||||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cash-out ${it}`,
|
||||
header: `Cash cassette ${it}`,
|
||||
width: widthsByNumberOfCassettes[numberOfCassettes].cassette,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const Transactions = ({ id }) => {
|
|||
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configData)
|
||||
|
||||
const loading = txLoading && configLoading
|
||||
const loading = txLoading || configLoading
|
||||
|
||||
if (!loading && txResponse) {
|
||||
txResponse.transactions = txResponse.transactions.splice(0, 5)
|
||||
|
|
|
|||
|
|
@ -93,13 +93,14 @@ const MachineRoute = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const Machines = ({ data, refetch, reload, bills }) => {
|
||||
const Machines = ({ data, refetch, reload }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
|
||||
|
||||
const machine = R.path(['machine'])(data) ?? {}
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
const bills = R.path(['bills'])(data) ?? []
|
||||
|
||||
const machineName = R.path(['name'])(machine) ?? null
|
||||
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const useStyles = makeStyles(styles)
|
|||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
cashbox: Yup.number()
|
||||
.label('Cashbox')
|
||||
.label('Cash box')
|
||||
.required()
|
||||
.integer()
|
||||
.min(0)
|
||||
|
|
@ -63,7 +63,7 @@ const ValidationSchema = Yup.object().shape({
|
|||
})
|
||||
|
||||
const GET_MACHINES_AND_CONFIG = gql`
|
||||
query getData {
|
||||
query getData($billFilters: JSONObject) {
|
||||
machines {
|
||||
name
|
||||
id: deviceId
|
||||
|
|
@ -75,24 +75,21 @@ const GET_MACHINES_AND_CONFIG = gql`
|
|||
numberOfCassettes
|
||||
}
|
||||
config
|
||||
bills(filters: $billFilters) {
|
||||
id
|
||||
fiat
|
||||
created
|
||||
deviceId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
/*
|
||||
// for cash in total calculation
|
||||
bills {
|
||||
fiat
|
||||
deviceId
|
||||
created
|
||||
cashbox
|
||||
}
|
||||
*/
|
||||
|
||||
const SET_CASSETTE_BILLS = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
|
|
@ -128,7 +125,13 @@ const CashCassettes = () => {
|
|||
const [editingSchema, setEditingSchema] = 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 [machineId, setMachineId] = useState('')
|
||||
|
||||
|
|
@ -204,10 +207,14 @@ const CashCassettes = () => {
|
|||
},
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cash-in',
|
||||
header: 'Cash box',
|
||||
width: maxNumberOfCassettes > 2 ? 140 : 280,
|
||||
view: value => (
|
||||
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
|
||||
view: (value, { id }) => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
total={R.sum(R.map(it => it.fiat, bills[id] ?? []))}
|
||||
/>
|
||||
),
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
|
|
@ -222,7 +229,7 @@ const CashCassettes = () => {
|
|||
elements.push({
|
||||
name: `cassette${it}`,
|
||||
header: `Cassette ${it}`,
|
||||
width: (maxNumberOfCassettes > 2 ? 700 : 560) / maxNumberOfCassettes,
|
||||
width: (maxNumberOfCassettes > 2 ? 560 : 650) / maxNumberOfCassettes,
|
||||
stripe: true,
|
||||
doubleHeader: 'Cash-out',
|
||||
view: (value, { id }) => (
|
||||
|
|
@ -268,11 +275,12 @@ const CashCassettes = () => {
|
|||
})
|
||||
|
||||
return (
|
||||
!dataLoading && (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Cash Cassettes"
|
||||
title="Cash Boxes & Cassettes"
|
||||
button={{
|
||||
text: 'Cashbox history',
|
||||
text: 'Cash box history',
|
||||
icon: HistoryIcon,
|
||||
inverseIcon: ReverseHistoryIcon,
|
||||
toggle: setShowHistory
|
||||
|
|
@ -281,11 +289,11 @@ const CashCassettes = () => {
|
|||
className={classes.tableWidth}>
|
||||
{!showHistory && (
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Label1 className={classes.cashboxReset}>Cashbox reset</Label1>
|
||||
<Label1 className={classes.cashboxReset}>Cash box resets</Label1>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
justifyContent="end"
|
||||
mr="-4px">
|
||||
{cashboxReset && (
|
||||
<P className={classes.selection}>
|
||||
|
|
@ -327,12 +335,12 @@ const CashCassettes = () => {
|
|||
currencyCode={fiatCurrency}
|
||||
machines={machines}
|
||||
config={config}
|
||||
bills={bills}
|
||||
bills={R.path(['bills'])(data)}
|
||||
deviceIds={deviceIds}
|
||||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
machine={R.find(R.propEq('id', machineId))(machines)}
|
||||
machine={R.find(R.propEq('id', machineId), machines)}
|
||||
cashoutSettings={getCashoutSettings(machineId)}
|
||||
onClose={() => {
|
||||
setWizard(false)
|
||||
|
|
@ -344,13 +352,13 @@ const CashCassettes = () => {
|
|||
)}
|
||||
{editingSchema && (
|
||||
<Modal
|
||||
title={'Cashbox reset'}
|
||||
title={'Cash box resets'}
|
||||
width={478}
|
||||
handleClose={() => setEditingSchema(null)}
|
||||
open={true}>
|
||||
<P className={classes.descriptions}>
|
||||
Specify if you want your cash-in counts to be reset automatically or
|
||||
manually.
|
||||
We can automatically assume you emptied a bill validator's cash
|
||||
box when the machine detects that it has been removed.
|
||||
</P>
|
||||
<RadioGroup
|
||||
name="set-automatic-reset"
|
||||
|
|
@ -360,8 +368,9 @@ const CashCassettes = () => {
|
|||
className={classes.radioButtons}
|
||||
/>
|
||||
<P className={classes.descriptions}>
|
||||
Choose this option if you want your cash-in cashbox count to be
|
||||
reset automatically when it is physically removed from the machine.
|
||||
Assume the cash box is emptied whenever it's removed, creating a
|
||||
new batch on the history screen and setting its current balance to
|
||||
zero.
|
||||
</P>
|
||||
<RadioGroup
|
||||
name="set-manual-reset"
|
||||
|
|
@ -371,9 +380,9 @@ const CashCassettes = () => {
|
|||
className={classes.radioButtons}
|
||||
/>
|
||||
<P className={classes.descriptions}>
|
||||
Choose this option if you want to edit your cash-in counts manually
|
||||
on Lamassu Admin, after you physically remove the bills from the
|
||||
cashbox.
|
||||
Cash boxes won't be assumed emptied when removed, nor their counts
|
||||
modified. Instead, to update the count and create a new batch,
|
||||
you'll click the 'Edit' button on this panel.
|
||||
</P>
|
||||
<DialogActions className={classes.actions}>
|
||||
<Button onClick={() => saveCashboxOption(selectedRadio)}>
|
||||
|
|
@ -384,6 +393,7 @@ const CashCassettes = () => {
|
|||
)}
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default CashCassettes
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
// import BigNumber from 'bignumber.js'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
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 { fromNamespace } from 'src/utils/config'
|
||||
import { numberToFiatAmount } from 'src/utils/number.js'
|
||||
|
||||
import styles from './CashCassettesFooter.styles.js'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
/* const sortDate = function(a, b) {
|
||||
return new Date(b.created).getTime() - new Date(a.created).getTime()
|
||||
} */
|
||||
|
||||
const CashCassettesFooter = ({
|
||||
machines,
|
||||
config,
|
||||
|
|
@ -43,44 +40,34 @@ const CashCassettesFooter = ({
|
|||
|
||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
|
||||
|
||||
/* const totalInCashBox = R.sum(
|
||||
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 totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
|
||||
|
||||
// const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
|
||||
const total = new BigNumber(totalInCassettes + totalInCashBox).toFormat(0)
|
||||
|
||||
return (
|
||||
<div className={classes.footerContainer}>
|
||||
<div className={classes.footerContent}>
|
||||
<Info3 className={classes.footerLabel}>Cash value in System</Info3>
|
||||
{/* <div className={classes.flex}>
|
||||
<div className={classes.flex}>
|
||||
<TxInIcon className={classes.icon} />
|
||||
<Info2 className={classes.iconLabel}>Cash-in:</Info2>
|
||||
<Info1 className={classes.valueDisplay}>
|
||||
{totalInCashBox} {currencyCode}
|
||||
{numberToFiatAmount(totalInCashBox)} {currencyCode}
|
||||
</Info1>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className={classes.flex}>
|
||||
<TxOutIcon className={classes.icon} />
|
||||
<Info2 className={classes.iconLabel}>Cash-out:</Info2>
|
||||
<Info1 className={classes.valueDisplay}>
|
||||
{totalInCassettes} {currencyCode}
|
||||
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
{/* <div className={classes.flex}>
|
||||
<div className={classes.flex}>
|
||||
<Info2 className={classes.iconLabel}>Total:</Info2>
|
||||
<Info1 className={classes.valueDisplay}>
|
||||
{total} {currencyCode}
|
||||
{numberToFiatAmount(total)} {currencyCode}
|
||||
</Info1>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ export default {
|
|||
boxShadow: [[0, -1, 10, 0, 'rgba(50, 50, 50, 0.1)']]
|
||||
},
|
||||
flex: {
|
||||
display: 'flex',
|
||||
// temp marginLeft until cashIn square is enabled
|
||||
marginLeft: -640
|
||||
display: 'flex'
|
||||
},
|
||||
icon: {
|
||||
alignSelf: 'center',
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
import React from 'react'
|
||||
// import * as Yup from 'yup'
|
||||
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs'
|
||||
// import { Link, IconButton } from 'src/components/buttons'
|
||||
// import { TextInput } from 'src/components/inputs'
|
||||
import { NumberInput } from 'src/components/inputs/formik'
|
||||
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 TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
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) {
|
||||
editBatch(id: $id, performedBy: $performedBy) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
` */
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
|
|
@ -63,27 +64,29 @@ const styles = {
|
|||
}
|
||||
}
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
/* const schema = Yup.object().shape({
|
||||
performedBy: Yup.string().nullable()
|
||||
})
|
||||
}) */
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CashboxHistory = ({ machines, currency }) => {
|
||||
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 [editBatch] = useMutation(EDIT_BATCH, {
|
||||
/* const [editBatch] = useMutation(EDIT_BATCH, {
|
||||
refetchQueries: () => ['cashboxBatches']
|
||||
})
|
||||
}) */
|
||||
|
||||
const { data: configData, loading: configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configData)
|
||||
|
||||
const loading = batchesLoading && configLoading
|
||||
const loading = batchesLoading || configLoading
|
||||
|
||||
const batches = R.path(['cashboxBatches'])(batchesData)
|
||||
|
||||
|
|
@ -91,33 +94,36 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
(ret, i) =>
|
||||
R.pipe(
|
||||
R.assoc(
|
||||
`cash-out-${i}-refill`,
|
||||
`cash-cassette-${i}-refill`,
|
||||
<>
|
||||
<TxOutIcon />
|
||||
<span className={classes.operationType}>Cash-out {i} refill</span>
|
||||
<span className={classes.operationType}>
|
||||
Cash cassette {i} refill
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
R.assoc(
|
||||
`cash-out-${i}-empty`,
|
||||
`cash-cassette-${i}-empty`,
|
||||
<>
|
||||
<TxOutIcon />
|
||||
<span className={classes.operationType}>Cash-out {i} emptied</span>
|
||||
<span className={classes.operationType}>
|
||||
Cash cassette {i} emptied
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
)(ret),
|
||||
{
|
||||
'cash-in-empty': (
|
||||
'cash-box-empty': (
|
||||
<>
|
||||
<TxInIcon />
|
||||
<span className={classes.operationType}>Cash-in emptied</span>
|
||||
<span className={classes.operationType}>Cash box emptied</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
R.range(1, 5)
|
||||
)
|
||||
|
||||
const save = row => {
|
||||
const field = R.find(f => f.id === row.id, fields)
|
||||
/* const save = row => {
|
||||
const performedBy = field.performedBy === '' ? null : field.performedBy
|
||||
|
||||
schema
|
||||
|
|
@ -129,14 +135,15 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
})
|
||||
})
|
||||
.catch(setError(true))
|
||||
return close(row.id)
|
||||
return close()
|
||||
}
|
||||
|
||||
const close = id => {
|
||||
setFields(R.filter(f => f.id !== id, fields))
|
||||
const close = () => {
|
||||
setEditing(false)
|
||||
setField(null)
|
||||
}
|
||||
|
||||
const notEditing = id => !R.any(R.propEq('id', id), fields)
|
||||
const notEditing = id => field?.id !== id */
|
||||
|
||||
const elements = [
|
||||
{
|
||||
|
|
@ -174,7 +181,7 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
{
|
||||
name: 'total',
|
||||
header: 'Total',
|
||||
width: 100,
|
||||
width: 180,
|
||||
textAlign: 'right',
|
||||
view: it => (
|
||||
<span>
|
||||
|
|
@ -195,8 +202,8 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
width: 125,
|
||||
textAlign: 'right',
|
||||
view: it => formatDate(it.created, timezone, 'HH:mm')
|
||||
},
|
||||
{
|
||||
}
|
||||
/* {
|
||||
name: 'performedBy',
|
||||
header: 'Performed by',
|
||||
width: 180,
|
||||
|
|
@ -206,21 +213,10 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
return R.isNil(it.performedBy) ? 'Unknown entity' : it.performedBy
|
||||
return (
|
||||
<TextInput
|
||||
onChange={e =>
|
||||
setFields(
|
||||
R.map(
|
||||
f =>
|
||||
f.id === it.id ? { ...f, performedBy: e.target.value } : f,
|
||||
fields
|
||||
)
|
||||
)
|
||||
}
|
||||
onChange={e => setField({ ...field, performedBy: e.target.value })}
|
||||
error={error}
|
||||
width={190 * 0.85}
|
||||
value={R.prop(
|
||||
'performedBy',
|
||||
R.find(f => f.id === it.id, fields)
|
||||
)}
|
||||
value={field?.performedBy}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -228,19 +224,18 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
{
|
||||
name: '',
|
||||
header: 'Edit',
|
||||
width: 150,
|
||||
width: 80,
|
||||
textAlign: 'right',
|
||||
view: it => {
|
||||
if (notEditing(it.id))
|
||||
return (
|
||||
<IconButton
|
||||
disabled={editing}
|
||||
onClick={() => {
|
||||
setFields([
|
||||
...fields,
|
||||
{ id: it.id, performedBy: it.performedBy }
|
||||
])
|
||||
setField({ id: it.id, performedBy: it.performedBy })
|
||||
setEditing(true)
|
||||
}}>
|
||||
<EditIcon />
|
||||
{editing ? <EditIconDisabled /> : <EditIcon />}
|
||||
</IconButton>
|
||||
)
|
||||
return (
|
||||
|
|
@ -248,26 +243,23 @@ const CashboxHistory = ({ machines, currency }) => {
|
|||
<Link type="submit" color="primary" onClick={() => save(it)}>
|
||||
Save
|
||||
</Link>
|
||||
<Link color="secondary" onClick={() => close(it.id)}>
|
||||
<Link color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
} */
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loading && (
|
||||
<DataTable
|
||||
loading={loading}
|
||||
name="cashboxHistory"
|
||||
elements={elements}
|
||||
data={batches}
|
||||
emptyText="No cashbox batches so far"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,11 @@ const MachineStatus = () => {
|
|||
const history = useHistory()
|
||||
const { state } = useLocation()
|
||||
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 timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
|
|
@ -114,6 +118,8 @@ const MachineStatus = () => {
|
|||
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
|
||||
)
|
||||
|
||||
const loading = machinesLoading || configLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
|
|
@ -132,7 +138,7 @@ const MachineStatus = () => {
|
|||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
loading={loading && configLoading}
|
||||
loading={loading}
|
||||
elements={elements}
|
||||
data={machines}
|
||||
Details={InnerMachineDetailsRow}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
|
||||
const onContinue = it => {
|
||||
const newConfig = R.merge(config, it)
|
||||
|
||||
if (isLastStep) {
|
||||
const wasCashboxEmptied = [
|
||||
config?.wasCashboxEmptied,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const WizardSplash = ({ name, onContinue }) => {
|
|||
<div className={classes.warningInfo}>
|
||||
<WarningIcon className={classes.warningIcon} />
|
||||
<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.
|
||||
</P>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { comet, errorColor } from 'src/styling/variables'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
|
|
@ -115,7 +116,8 @@ const WizardStep = ({
|
|||
lastStep,
|
||||
steps,
|
||||
fiatCurrency,
|
||||
onContinue
|
||||
onContinue,
|
||||
initialValues
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -168,7 +170,7 @@ const WizardStep = ({
|
|||
classes.verticalAlign,
|
||||
classes.fullWidth
|
||||
)}>
|
||||
<H4 noMargin>Did you empty the cash-in box?</H4>
|
||||
<H4 noMargin>Did you empty the cash box?</H4>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="wasCashboxEmptied"
|
||||
|
|
@ -188,8 +190,8 @@ const WizardStep = ({
|
|||
<P>Since previous update</P>
|
||||
<Tooltip width={215}>
|
||||
<P>
|
||||
Number of bills inside the cashbox, since the last
|
||||
cashbox changes.
|
||||
Number of bills inside the cash box, since the last
|
||||
cash box changes.
|
||||
</P>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -219,12 +221,7 @@ const WizardStep = ({
|
|||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
onSubmit={onContinue}
|
||||
initialValues={{
|
||||
cassette1: '',
|
||||
cassette2: '',
|
||||
cassette3: '',
|
||||
cassette4: ''
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
validationSchema={steps[step - 1].schema}>
|
||||
{({ values, errors }) => (
|
||||
|
|
@ -255,7 +252,7 @@ const WizardStep = ({
|
|||
<H4
|
||||
className={classes.cassetteFormTitleContent}
|
||||
noMargin>
|
||||
Cash-out {step - 1} (dispenser)
|
||||
Cash cassette {step - 1} (dispenser)
|
||||
</H4>
|
||||
</div>
|
||||
<Cashbox
|
||||
|
|
@ -283,7 +280,8 @@ const WizardStep = ({
|
|||
</P>
|
||||
</div>
|
||||
<P noMargin className={classes.fiatTotal}>
|
||||
= {cassetteTotal(values)} {fiatCurrency}
|
||||
= {numberToFiatAmount(cassetteTotal(values))}{' '}
|
||||
{fiatCurrency}
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,10 +131,10 @@ const FiatBalanceOverrides = ({ section }) => {
|
|||
it => {
|
||||
elements.push({
|
||||
name: `fillingPercentageCassette${it}`,
|
||||
display: `Cash-out ${it}`,
|
||||
display: `Cash cassette ${it}`,
|
||||
width: 155,
|
||||
textAlign: 'right',
|
||||
doubleHeader: 'Cash-out (Cassette Empty)',
|
||||
doubleHeader: 'Cash Cassette Empty',
|
||||
bold: true,
|
||||
input: NumberInput,
|
||||
suffix: '%',
|
||||
|
|
|
|||
|
|
@ -98,13 +98,13 @@ const Logs = () => {
|
|||
const [saveMessage, setSaveMessage] = useState(null)
|
||||
const [logLevel, setLogLevel] = useState(SHOW_ALL)
|
||||
|
||||
const { data, loading } = useQuery(GET_SERVER_DATA, {
|
||||
const { data, loading: dataLoading } = useQuery(GET_SERVER_DATA, {
|
||||
onCompleted: () => setSaveMessage(''),
|
||||
variables: {
|
||||
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 defaultLogLevels = [
|
||||
|
|
@ -132,6 +132,8 @@ const Logs = () => {
|
|||
setLogLevel(logLevel)
|
||||
}
|
||||
|
||||
const loading = dataLoading || configLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
|
|
@ -206,8 +208,8 @@ const Logs = () => {
|
|||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{loading && configLoading && <H4>{'Loading...'}</H4>}
|
||||
{!loading && !configLoading && !data?.serverLogs?.length && (
|
||||
{loading && <H4>{'Loading...'}</H4>}
|
||||
{!loading && !data?.serverLogs?.length && (
|
||||
<H4>{'No activity so far'}</H4>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default {
|
|||
settings: {
|
||||
enabled: true,
|
||||
disabledMessage: 'RBF verification not available',
|
||||
label: 'Enable RBF verification',
|
||||
label: 'Lower the confidence of RBF transactions',
|
||||
requirement: 'bitcoind'
|
||||
},
|
||||
face: true
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const SessionManagement = () => {
|
|||
const { data: configResponse, loading: configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
const loading = sessionsLoading && configLoading
|
||||
const loading = sessionsLoading || configLoading
|
||||
|
||||
const elements = [
|
||||
{
|
||||
|
|
@ -61,7 +61,7 @@ const SessionManagement = () => {
|
|||
{
|
||||
header: 'Last known use',
|
||||
width: 305,
|
||||
textAlign: 'center',
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: s => {
|
||||
if (R.isNil(s.sess.ua)) return 'No Record'
|
||||
|
|
@ -72,7 +72,7 @@ const SessionManagement = () => {
|
|||
{
|
||||
header: 'Last known location',
|
||||
width: 250,
|
||||
textAlign: 'center',
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: s => {
|
||||
return isLocalhost(s.sess.ipAddress) ? 'This device' : s.sess.ipAddress
|
||||
|
|
@ -107,16 +107,15 @@ const SessionManagement = () => {
|
|||
]
|
||||
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<TitleSection title="Session Management" />
|
||||
<DataTable
|
||||
loading={loading}
|
||||
elements={elements}
|
||||
data={R.path(['sessions'])(tknResponse)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionManagement
|
||||
|
|
|
|||
|
|
@ -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 TxOutIcon } from 'src/styling/icons/direction/cash-out.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 DetailsRow from './DetailsCard'
|
||||
|
|
@ -124,13 +126,13 @@ const Transactions = () => {
|
|||
const history = useHistory()
|
||||
|
||||
const [filters, setFilters] = useState([])
|
||||
const { data: filtersResponse, loading: loadingFilters } = useQuery(
|
||||
const { data: filtersResponse, loading: filtersLoading } = useQuery(
|
||||
GET_TRANSACTION_FILTERS
|
||||
)
|
||||
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
|
||||
const {
|
||||
data: txData,
|
||||
loading: loadingTransactions,
|
||||
loading: transactionsLoading,
|
||||
refetch,
|
||||
startPolling,
|
||||
stopPolling
|
||||
|
|
@ -185,7 +187,11 @@ const Transactions = () => {
|
|||
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
|
||||
{!it.isAnonymous && (
|
||||
<div onClick={() => redirect(it.customerId)}>
|
||||
{it.hasError ? (
|
||||
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
|
||||
) : (
|
||||
<CustomerLinkIcon className={classes.customerLinkIcon} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -294,6 +300,14 @@ const Transactions = () => {
|
|||
|
||||
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 (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
|
|
@ -301,7 +315,7 @@ const Transactions = () => {
|
|||
<Title>Transactions</Title>
|
||||
<div className={classes.buttonsWrapper}>
|
||||
<SearchBox
|
||||
loading={loadingFilters}
|
||||
loading={filtersLoading}
|
||||
filters={filters}
|
||||
options={filterOptions}
|
||||
inputPlaceholder={'Search Transactions'}
|
||||
|
|
@ -331,6 +345,10 @@ const Transactions = () => {
|
|||
<TxOutIcon />
|
||||
<span>Cash-out</span>
|
||||
</div>
|
||||
<div>
|
||||
{errorLabel}
|
||||
<span>Transaction error</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{filters.length > 0 && (
|
||||
|
|
@ -342,7 +360,7 @@ const Transactions = () => {
|
|||
/>
|
||||
)}
|
||||
<DataTable
|
||||
loading={loadingTransactions && configLoading}
|
||||
loading={loading}
|
||||
emptyText="No transactions so far"
|
||||
elements={elements}
|
||||
data={txList}
|
||||
|
|
|
|||
|
|
@ -80,8 +80,11 @@ const mainStyles = {
|
|||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& > div': {
|
||||
marginLeft: 24
|
||||
},
|
||||
'& > div:first-child': {
|
||||
marginRight: 24
|
||||
marginLeft: 0
|
||||
},
|
||||
'& span': {
|
||||
extend: label1,
|
||||
|
|
|
|||
|
|
@ -183,10 +183,10 @@ const InfoPanel = ({ step, config = {}, liveValues = {}, currency }) => {
|
|||
return (
|
||||
<>
|
||||
<H5 className={classes.infoTitle}>Trigger overview so far</H5>
|
||||
<Info3 noMargin className={classes.infoText}>
|
||||
<Info3 noMargin>
|
||||
{oldText}
|
||||
{step !== 1 && ', '}
|
||||
{newText}
|
||||
<span className={classes.infoCurrentText}>{newText}</span>
|
||||
{!isLastStep && '...'}
|
||||
</Info3>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const getDefaultSettings = () => {
|
|||
return [
|
||||
{
|
||||
name: 'expirationTime',
|
||||
header: 'Expiration Time',
|
||||
header: 'Expiration time',
|
||||
width: 196,
|
||||
size: 'sm',
|
||||
editable: false
|
||||
|
|
@ -101,7 +101,7 @@ const getOverrides = () => {
|
|||
},
|
||||
{
|
||||
name: 'expirationTime',
|
||||
header: 'Expiration Time',
|
||||
header: 'Expiration time',
|
||||
width: 196,
|
||||
size: 'sm',
|
||||
editable: false
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@ import * as R from 'ramda'
|
|||
import React, { useReducer, useState, useContext } from 'react'
|
||||
|
||||
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 TitleSection from 'src/components/layout/TitleSection'
|
||||
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 ChangeRoleModal from './modals/ChangeRoleModal'
|
||||
|
|
@ -153,35 +159,37 @@ const Users = () => {
|
|||
size: 'sm',
|
||||
view: u => {
|
||||
return (
|
||||
<>
|
||||
<Chip
|
||||
size="small"
|
||||
label="Reset password"
|
||||
className={classes.actionChip}
|
||||
<div className={classes.actionButtonWrapper}>
|
||||
<ActionButton
|
||||
Icon={KeyIcon}
|
||||
InverseIcon={WhiteKeyIcon}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
dispatch({
|
||||
type: 'open',
|
||||
payload: 'showResetPasswordModal'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label="Reset 2FA"
|
||||
className={classes.actionChip}
|
||||
}}>
|
||||
Reset password
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Icon={LockIcon}
|
||||
InverseIcon={WhiteLockIcon}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
dispatch({
|
||||
type: 'open',
|
||||
payload: 'showReset2FAModal'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label="Add FIDO"
|
||||
className={classes.actionChip}
|
||||
}}>
|
||||
Reset 2FA
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Icon={UserRoleIcon}
|
||||
InverseIcon={WhiteUserRoleIcon}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
generateAttestationOptions({
|
||||
|
|
@ -189,9 +197,10 @@ const Users = () => {
|
|||
userID: u.id
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}}>
|
||||
Add FIDO
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -52,10 +52,6 @@ const styles = {
|
|||
fontFamily: fontPrimary,
|
||||
marginLeft: 10
|
||||
},
|
||||
actionChip: {
|
||||
backgroundColor: subheaderColor,
|
||||
marginRight: 15
|
||||
},
|
||||
info: {
|
||||
fontFamily: fontSecondary,
|
||||
textAlign: 'justify'
|
||||
|
|
@ -118,6 +114,10 @@ const styles = {
|
|||
},
|
||||
roleSwitch: {
|
||||
marginLeft: 15
|
||||
},
|
||||
actionButtonWrapper: {
|
||||
display: 'flex',
|
||||
gap: 12
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Form, Formik } from 'formik'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
|
|
@ -48,6 +49,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (twoFACode.length !== 6) {
|
||||
setInvalidCode(true)
|
||||
return
|
||||
}
|
||||
confirm2FA({ variables: { code: twoFACode } })
|
||||
}
|
||||
|
||||
return (
|
||||
showModal && (
|
||||
<Modal
|
||||
|
|
@ -61,6 +70,9 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
|||
To make changes on this user, please confirm this action by entering
|
||||
your two-factor authentication code below.
|
||||
</P>
|
||||
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||
<Form>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFACode}
|
||||
|
|
@ -70,19 +82,14 @@ const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
|||
containerStyle={classes.codeContainer}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
<button onClick={handleSubmit} className={classes.enterButton} />
|
||||
</Form>
|
||||
</Formik>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
className={classes.submit}
|
||||
onClick={() => {
|
||||
if (twoFACode.length !== 6) {
|
||||
setInvalidCode(true)
|
||||
return
|
||||
}
|
||||
confirm2FA({ variables: { code: twoFACode } })
|
||||
}}>
|
||||
<Button className={classes.submit} onClick={handleSubmit}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 classes = useStyles()
|
||||
|
|
|
|||
11
new-lamassu-admin/src/styling/icons/button/key/white.svg
Normal 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 |
11
new-lamassu-admin/src/styling/icons/button/key/zodiac.svg
Normal 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 |
11
new-lamassu-admin/src/styling/icons/button/lock/white.svg
Normal 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 |
11
new-lamassu-admin/src/styling/icons/button/lock/zodiac.svg
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -7,4 +7,7 @@ const transformNumber = value => (isValidNumber(value) ? value : null)
|
|||
const defaultToZero = value =>
|
||||
isValidNumber(parseInt(value)) ? parseInt(value) : 0
|
||||
|
||||
export { defaultToZero, transformNumber }
|
||||
const numberToFiatAmount = value =>
|
||||
value.toLocaleString('en-US', { maximumFractionDigits: 2 })
|
||||
|
||||
export { defaultToZero, transformNumber, numberToFiatAmount }
|
||||
|
|
|
|||
|
|
@ -3,85 +3,109 @@ import { getTimezoneOffset } from 'date-fns-tz'
|
|||
import * as R from 'ramda'
|
||||
|
||||
const timezones = {
|
||||
'Pacific/Midway': 'Midway Island, Samoa',
|
||||
'Pacific/Honolulu': 'Hawaii',
|
||||
'America/Juneau': 'Alaska',
|
||||
'America/Boise': 'Mountain Time',
|
||||
'America/Dawson': 'Dawson, Yukon',
|
||||
'America/Chihuahua': 'Chihuahua, La Paz, Mazatlan',
|
||||
'America/Phoenix': 'Arizona',
|
||||
'America/Chicago': 'Central Time',
|
||||
'America/Regina': 'Saskatchewan',
|
||||
'America/Mexico_City': 'Guadalajara, Mexico City, Monterrey',
|
||||
'America/Belize': 'Central America',
|
||||
'America/Detroit': 'Eastern Time',
|
||||
'America/Bogota': 'Bogota, Lima, Quito',
|
||||
'America/Caracas': 'Caracas, La Paz',
|
||||
'America/Santiago': 'Santiago',
|
||||
'America/St_Johns': 'Newfoundland and Labrador',
|
||||
'America/Sao_Paulo': 'Brasilia',
|
||||
'America/Tijuana': 'Tijuana',
|
||||
'America/Montevideo': 'Montevideo',
|
||||
'America/Argentina/Buenos_Aires': 'Buenos Aires, Georgetown',
|
||||
'America/Godthab': 'Greenland',
|
||||
'America/Los_Angeles': 'Pacific Time',
|
||||
'Atlantic/Azores': 'Azores',
|
||||
'Atlantic/Cape_Verde': 'Cape Verde Islands',
|
||||
GMT: 'UTC',
|
||||
'Europe/London': 'Edinburgh, London',
|
||||
'Europe/Dublin': 'Dublin',
|
||||
'Europe/Lisbon': 'Lisbon',
|
||||
'Africa/Casablanca': 'Casablanca, Monrovia',
|
||||
'Atlantic/Canary': 'Canary Islands',
|
||||
'Europe/Belgrade': 'Belgrade, Bratislava, Budapest, Ljubljana, Prague',
|
||||
'Europe/Sarajevo': 'Sarajevo, Skopje, Warsaw, Zagreb',
|
||||
'Europe/Brussels': 'Brussels, Copenhagen, Madrid, Paris',
|
||||
'Europe/Amsterdam': 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna',
|
||||
'Africa/Algiers': 'West Central Africa',
|
||||
'Europe/Bucharest': 'Bucharest',
|
||||
'Africa/Cairo': 'Cairo',
|
||||
'Europe/Helsinki': 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius',
|
||||
'Europe/Athens': 'Athens, Istanbul, Minsk',
|
||||
'Asia/Jerusalem': 'Jerusalem',
|
||||
'Africa/Harare': 'Harare, Pretoria',
|
||||
'Europe/Moscow': 'Moscow, St. Petersburg, Volgograd',
|
||||
'Asia/Kuwait': 'Kuwait, Riyadh',
|
||||
'Africa/Nairobi': 'Nairobi',
|
||||
'Asia/Baghdad': 'Baghdad',
|
||||
'Asia/Tehran': 'Tehran',
|
||||
'Asia/Dubai': 'Abu Dhabi, Muscat',
|
||||
'Asia/Baku': 'Baku, Tbilisi, Yerevan',
|
||||
'Asia/Kabul': 'Kabul',
|
||||
'Asia/Yekaterinburg': 'Ekaterinburg',
|
||||
'Asia/Karachi': 'Islamabad, Karachi, Tashkent',
|
||||
'Asia/Kolkata': 'Chennai, Kolkata, Mumbai, New Delhi',
|
||||
'Asia/Kathmandu': 'Kathmandu',
|
||||
'Asia/Dhaka': 'Astana, Dhaka',
|
||||
'Asia/Colombo': 'Sri Jayawardenepura',
|
||||
'Asia/Almaty': 'Almaty, Novosibirsk',
|
||||
'Asia/Rangoon': 'Yangon Rangoon',
|
||||
'Asia/Bangkok': 'Bangkok, Hanoi, Jakarta',
|
||||
'Asia/Krasnoyarsk': 'Krasnoyarsk',
|
||||
'Asia/Shanghai': 'Beijing, Chongqing, Hong Kong SAR, Urumqi',
|
||||
'Asia/Kuala_Lumpur': 'Kuala Lumpur, Singapore',
|
||||
'Asia/Taipei': 'Taipei',
|
||||
'Australia/Perth': 'Perth',
|
||||
'Asia/Irkutsk': 'Irkutsk, Ulaanbaatar',
|
||||
'Asia/Seoul': 'Seoul',
|
||||
'Asia/Tokyo': 'Osaka, Sapporo, Tokyo',
|
||||
'Asia/Yakutsk': 'Yakutsk',
|
||||
'Australia/Darwin': 'Darwin',
|
||||
'Australia/Adelaide': 'Adelaide',
|
||||
'Australia/Sydney': 'Canberra, Melbourne, Sydney',
|
||||
'Australia/Brisbane': 'Brisbane',
|
||||
'Australia/Hobart': 'Hobart',
|
||||
'Asia/Vladivostok': 'Vladivostok',
|
||||
'Pacific/Guam': 'Guam, Port Moresby',
|
||||
'Asia/Magadan': 'Magadan, Solomon Islands, New Caledonia',
|
||||
'Asia/Kamchatka': 'Kamchatka, Marshall Islands',
|
||||
'Pacific/Fiji': 'Fiji Islands',
|
||||
'Pacific/Auckland': 'Auckland, Wellington',
|
||||
'Pacific/Tongatapu': "Nuku'alofa"
|
||||
'Pacific/Midway': { short: 'SST', long: 'Midway Island, Samoa' },
|
||||
'Pacific/Honolulu': { short: 'HAST', long: 'Hawaii' },
|
||||
'America/Juneau': { short: 'AKST', long: 'Alaska' },
|
||||
'America/Boise': { short: 'MST', long: 'Mountain Time' },
|
||||
'America/Dawson': { short: 'MST', long: 'Dawson, Yukon' },
|
||||
'America/Chihuahua': { short: null, long: 'Chihuahua, La Paz, Mazatlan' },
|
||||
'America/Phoenix': { short: 'MST', long: 'Arizona' },
|
||||
'America/Chicago': { short: 'CST', long: 'Central Time' },
|
||||
'America/Regina': { short: 'CST', long: 'Saskatchewan' },
|
||||
'America/Mexico_City': {
|
||||
short: 'CST',
|
||||
long: 'Guadalajara, Mexico City, Monterrey'
|
||||
},
|
||||
'America/Belize': { short: 'CST', long: 'Central America' },
|
||||
'America/Detroit': { short: 'EST', long: 'Eastern Time' },
|
||||
'America/Bogota': { short: 'COT', long: 'Bogota, Lima, Quito' },
|
||||
'America/Caracas': { short: 'VET', long: 'Caracas, La Paz' },
|
||||
'America/Santiago': { short: 'CLST', long: 'Santiago' },
|
||||
'America/St_Johns': { short: 'HNTN', long: 'Newfoundland and Labrador' },
|
||||
'America/Sao_Paulo': { short: 'BRT', long: 'Brasilia' },
|
||||
'America/Tijuana': { short: 'PST', long: 'Tijuana' },
|
||||
'America/Montevideo': { short: 'UYT', long: 'Montevideo' },
|
||||
'America/Argentina/Buenos_Aires': {
|
||||
short: null,
|
||||
long: 'Buenos Aires, Georgetown'
|
||||
},
|
||||
'America/Godthab': { short: null, long: 'Greenland' },
|
||||
'America/Los_Angeles': { short: 'PST', long: 'Pacific Time' },
|
||||
'Atlantic/Azores': { short: 'AZOT', long: 'Azores' },
|
||||
'Atlantic/Cape_Verde': { short: 'CVT', long: 'Cape Verde Islands' },
|
||||
GMT: { short: 'GMT', long: 'UTC' },
|
||||
'Europe/London': { short: 'GMT', long: 'Edinburgh, London' },
|
||||
'Europe/Dublin': { short: 'GMT', long: 'Dublin' },
|
||||
'Europe/Lisbon': { short: 'WET', long: 'Lisbon' },
|
||||
'Africa/Casablanca': { short: 'WET', long: 'Casablanca, Monrovia' },
|
||||
'Atlantic/Canary': { short: 'WET', long: 'Canary Islands' },
|
||||
'Europe/Belgrade': {
|
||||
short: 'CET',
|
||||
long: 'Belgrade, Bratislava, Budapest, Ljubljana, Prague'
|
||||
},
|
||||
'Europe/Sarajevo': { short: 'CET', long: 'Sarajevo, Skopje, Warsaw, Zagreb' },
|
||||
'Europe/Brussels': {
|
||||
short: 'CET',
|
||||
long: 'Brussels, Copenhagen, Madrid, Paris'
|
||||
},
|
||||
'Europe/Amsterdam': {
|
||||
short: 'CET',
|
||||
long: 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna'
|
||||
},
|
||||
'Africa/Algiers': { short: 'CET', long: 'West Central Africa' },
|
||||
'Europe/Bucharest': { short: 'EET', long: 'Bucharest' },
|
||||
'Africa/Cairo': { short: 'EET', long: 'Cairo' },
|
||||
'Europe/Helsinki': {
|
||||
short: 'EET',
|
||||
long: 'Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius'
|
||||
},
|
||||
'Europe/Athens': { short: 'EET', long: 'Athens, Istanbul, Minsk' },
|
||||
'Asia/Jerusalem': { short: 'IST', long: 'Jerusalem' },
|
||||
'Africa/Harare': { short: 'CAT', long: 'Harare, Pretoria' },
|
||||
'Europe/Moscow': { short: 'MSK', long: 'Moscow, St. Petersburg, Volgograd' },
|
||||
'Asia/Kuwait': { short: 'AST', long: 'Kuwait, Riyadh' },
|
||||
'Africa/Nairobi': { short: 'EAT', long: 'Nairobi' },
|
||||
'Asia/Baghdad': { short: 'AST', long: 'Baghdad' },
|
||||
'Asia/Tehran': { short: 'IRST', long: 'Tehran' },
|
||||
'Asia/Dubai': { short: 'GST', long: 'Abu Dhabi, Muscat' },
|
||||
'Asia/Baku': { short: 'AZT', long: 'Baku, Tbilisi, Yerevan' },
|
||||
'Asia/Kabul': { short: 'AFT', long: 'Kabul' },
|
||||
'Asia/Yekaterinburg': { short: 'YEKT', long: 'Ekaterinburg' },
|
||||
'Asia/Karachi': { short: 'PKT', long: 'Islamabad, Karachi, Tashkent' },
|
||||
'Asia/Kolkata': { short: 'IST', long: 'Chennai, Kolkata, Mumbai, New Delhi' },
|
||||
'Asia/Kathmandu': { short: null, long: 'Kathmandu' },
|
||||
'Asia/Dhaka': { short: 'BST', long: 'Astana, Dhaka' },
|
||||
'Asia/Colombo': { short: 'IST', long: 'Sri Jayawardenepura' },
|
||||
'Asia/Almaty': { short: 'ALMT', long: 'Almaty, Novosibirsk' },
|
||||
'Asia/Rangoon': { short: null, long: 'Yangon Rangoon' },
|
||||
'Asia/Bangkok': { short: 'ICT', long: 'Bangkok, Hanoi, Jakarta' },
|
||||
'Asia/Krasnoyarsk': { short: 'KRAT', long: 'Krasnoyarsk' },
|
||||
'Asia/Shanghai': {
|
||||
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 => {
|
||||
|
|
@ -106,7 +130,7 @@ const buildTzLabels = timezoneList => {
|
|||
const prefix = `(GMT${isNegative ? `-` : `+`}${hours}:${minutes})`
|
||||
|
||||
acc.push({
|
||||
label: `${prefix} - ${value[1]}`,
|
||||
label: `${prefix} - ${value[1].long}`,
|
||||
code: value[0]
|
||||
})
|
||||
|
||||
|
|
@ -117,4 +141,6 @@ const buildTzLabels = timezoneList => {
|
|||
)
|
||||
}
|
||||
|
||||
export default buildTzLabels(timezones)
|
||||
const labels = buildTzLabels(timezones)
|
||||
|
||||
export { labels, timezones }
|
||||
|
|
|
|||