Merge remote-tracking branch 'upstream/release-7.5.0' into chore/merge-release-into-dev

This commit is contained in:
Taranto 2021-11-24 14:53:50 +00:00
commit 0ad2ee362a
109 changed files with 3283 additions and 697 deletions

1
.env.sample Normal file
View file

@ -0,0 +1 @@
LAMASSU_DB=DEV

1
.gitignore vendored
View file

@ -40,3 +40,4 @@ terraform.*
.terraform .terraform
db.json db.json
.env

View file

@ -0,0 +1,34 @@
#!/usr/bin/env node
const _ = require('lodash')
const db = require('../lib/db')
if (process.argv.length !== 4) {
console.log('Usage: lamassu-update-cassettes <device_id> <number_of_cassettes>')
process.exit(1)
}
if (!_.isFinite(parseInt(process.argv[3]))) {
console.log('Error: <number_of_cassettes> is not a valid number (%s)', err)
process.exit(3)
}
if (parseInt(process.argv[3]) > 4 || parseInt(process.argv[3]) < 2) {
console.log('Error: <number_of_cassettes> is out of range. Should be a number between 2 and 4')
process.exit(3)
}
const deviceId = process.argv[2]
const numberOfCassettes = parseInt(process.argv[3])
const query = `UPDATE devices SET number_of_cassettes = $1 WHERE device_id = $2`
db.none(query, [numberOfCassettes, deviceId])
.then(() => {
console.log('Success! Device %s updated to %s cassettes', deviceId, numberOfCassettes)
process.exit(0)
})
.catch(err => {
console.log('Error: %s', err)
process.exit(3)
})

View file

@ -33,7 +33,7 @@ function batch () {
order by created desc limit $2` order by created desc limit $2`
const cashOutSql = `select 'cashOut' as tx_class, cash_out_txs.*, const cashOutSql = `select 'cashOut' as tx_class, cash_out_txs.*,
(extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) >= $2 as expired (NOT dispense AND extract(epoch from (now() - greatest(created, confirmed_at))) >= $2) as expired
from cash_out_txs from cash_out_txs
order by created desc limit $1` order by created desc limit $1`
@ -51,7 +51,7 @@ function single (txId) {
where id=$2` where id=$2`
const cashOutSql = `select 'cashOut' as tx_class, const cashOutSql = `select 'cashOut' as tx_class,
(extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) >= $2 as expired, (NOT dispense AND extract(epoch from (now() - greatest(created, confirmed_at))) >= $2) as expired,
cash_out_txs.* cash_out_txs.*
from cash_out_txs from cash_out_txs
where id=$1` where id=$1`

View file

@ -1,16 +1,78 @@
const _ = require('lodash/fp')
const uuid = require('uuid') const uuid = require('uuid')
// Custom algorith for two cassettes. For three or more denominations, we'll need const MAX_AMOUNT_OF_SOLUTIONS = 10000
// to rethink this. Greedy algorithm fails to find *any* solution in some cases. const MAX_BRUTEFORCE_ITERATIONS = 10000000
// Dynamic programming may be too inefficient for large amounts.
//
// We can either require canononical denominations for 3+, or try to expand
// this algorithm.
exports.makeChange = function makeChange (cassettes, amount) {
// Note: Everything here is converted to primitive numbers,
// since they're all integers, well within JS number range,
// and this is way more efficient in a tight loop.
function newSolution(cassettes, c0, c1, c2, c3, shouldFlip) {
return [
{
provisioned: shouldFlip ? cassettes[0].count - c0 : c0,
denomination: cassettes[0].denomination
},
{
provisioned: shouldFlip ? cassettes[1].count - c1 : c1,
denomination: cassettes[1].denomination
},
{
provisioned: shouldFlip ? cassettes[2].count - c2 : c2,
denomination: cassettes[2].denomination
},
{
provisioned: shouldFlip ? cassettes[3].count - c3 : c3,
denomination: cassettes[3].denomination
}
]
}
function mergeCassettes(cassettes) {
const map = {}
_.forEach(it => {
if (!map[it.denomination]) {
map[it.denomination] = 0
}
map[it.denomination] += it.count
}, cassettes)
return _.map(it => ({ denomination: it, count: map[it] }), _.keys(map))
}
function unmergeCassettes(cassettes, output) {
const map = {}
_.forEach(it => {
if (!map[it.denomination]) {
map[it.denomination] = 0
}
map[it.denomination] += it.provisioned
}, output)
const response = []
_.forEach(it => {
const value = {
denomination: it.denomination,
id: uuid.v4()
}
const amountNeeded = map[it.denomination]
if (!amountNeeded) {
return response.push({ provisioned: 0, ...value })
}
if (amountNeeded < it.count) {
map[it.denomination] = 0
return response.push({ provisioned: amountNeeded, ...value })
}
map[it.denomination] -= it.count
return response.push({ provisioned: it.count, ...value })
}, cassettes)
return response
}
function makeChangeDuo(cassettes, amount) {
const small = cassettes[0] const small = cassettes[0]
const large = cassettes[1] const large = cassettes[1]
@ -23,15 +85,140 @@ exports.makeChange = function makeChange (cassettes, amount) {
const remainder = amountNum - largeDenom * i const remainder = amountNum - largeDenom * i
if (remainder % smallDenom !== 0) continue if (remainder % smallDenom !== 0) continue
const smallCount = remainder / smallDenom const smallCount = remainder / smallDenom
if (smallCount > small.count) continue if (smallCount > small.count) continue
return [ return [
{provisioned: smallCount, denomination: small.denomination, id: uuid.v4()}, {
{provisioned: i, denomination: largeDenom, id: uuid.v4()} provisioned: smallCount,
denomination: small.denomination,
id: uuid.v4()
},
{ provisioned: i, denomination: largeDenom, id: uuid.v4() }
] ]
} }
return null return []
} }
function makeChange(outCassettes, amount) {
const available = _.reduce(
(res, val) => res + val.count * val.denomination,
0,
outCassettes
)
if (available < amount) {
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(cassettes)}`)
return null
}
const cassettes = mergeCassettes(outCassettes)
const result =
_.size(cassettes) >= 3
? makeChangeDynamic(cassettes, amount, available)
: makeChangeDuo(cassettes, amount)
if (!result.length) return null
return unmergeCassettes(outCassettes, result)
}
function makeChangeDynamicBruteForce(outCassettes, amount, available) {
const solutions = []
let x = 0
const shouldFlip = amount > _.max(_.map(it => it.denomination * it.count, outCassettes))
const amountNum = shouldFlip ? available - amount : amount
const cassettes = shouldFlip ? _.reverse(outCassettes) : outCassettes
const { denomination: denomination0, count: count0 } = cassettes[0]
const { denomination: denomination1, count: count1 } = cassettes[1]
const { denomination: denomination2, count: count2 } = cassettes[2]
const { denomination: denomination3, count: count3 } = cassettes[3]
const startTime = new Date().getTime()
loop1: for (let i = 0; i <= count0; i++) {
const firstSum = i * denomination0
for (let j = 0; j <= count1; j++) {
const secondSum = firstSum + j * denomination1
if (secondSum > amountNum) break
if (secondSum === amountNum) {
solutions.push(newSolution(cassettes, i, j, 0, 0, shouldFlip))
}
for (let k = 0; k <= count2; k++) {
const thirdSum = secondSum + k * denomination2
if (thirdSum > amountNum) break
if (denomination2 === 0) break
if (thirdSum === amountNum) {
solutions.push(newSolution(cassettes, i, j, k, 0, shouldFlip))
}
for (let l = 0; l <= count3; l++) {
if ((x > MAX_AMOUNT_OF_SOLUTIONS && solutions.length >= 1) || x > MAX_BRUTEFORCE_ITERATIONS) break loop1
x++
const fourthSum = thirdSum + l * denomination3
if (fourthSum > amountNum) break
if (denomination3 === 0) break
if (fourthSum === amountNum) {
solutions.push(newSolution(cassettes, i, j, k, l, shouldFlip))
}
}
}
}
}
const endTime = new Date().getTime()
console.log(`Exiting bruteforce after ${x} tries. Took ${endTime - startTime} ms`)
return solutions
}
function makeChangeDynamic(cassettes, amount, available) {
while (_.size(cassettes) < 4) {
cassettes.push({ denomination: 0, count: 0 })
}
const amountNum = amount.toNumber()
const solutions = makeChangeDynamicBruteForce(cassettes, amountNum, available)
const sortedSolutions = _.sortBy(it => {
const arr = []
for (let la = 0; la < 4; la++) {
arr.push(cassettes[la].count - it[la].provisioned)
}
if (arr.length < 2) return Infinity
return _.max(arr) - _.min(arr)
}, solutions)
const cleanSolution = _.filter(
it => it.denomination > 0,
_.head(sortedSolutions)
)
const response = cleanSolution
// Final sanity check
let total = 0
_.forEach(it => {
total += it.provisioned * it.denomination
}, response)
if (total === amountNum) return response
console.log(
`Failed to find a solution for ${amountNum} with cassettes ${JSON.stringify(cassettes)}`
)
return []
}
module.exports = { makeChange }

View file

@ -25,16 +25,16 @@ const BINARIES = {
BTC: { BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.0/bitcoin-0.20.0-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.0/bitcoin-0.20.0-x86_64-linux-gnu.tar.gz',
defaultDir: 'bitcoin-0.20.0/bin', defaultDir: 'bitcoin-0.20.0/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-0.21.0/bitcoin-0.21.0-x86_64-linux-gnu.tar.gz', url: 'https://bitcoincore.org/bin/bitcoin-core-22.0/bitcoin-22.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-0.21.0/bin' dir: 'bitcoin-22.0/bin'
}, },
ETH: { ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.8-26675454.tar.gz', url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.12-6c4dc6c3.tar.gz',
dir: 'geth-linux-amd64-1.10.8-26675454' dir: 'geth-linux-amd64-1.10.12-6c4dc6c3'
}, },
ZEC: { ZEC: {
url: 'https://z.cash/downloads/zcash-4.4.1-linux64-debian-stretch.tar.gz', url: 'https://z.cash/downloads/zcash-4.5.1-1-linux64-debian-stretch.tar.gz',
dir: 'zcash-4.4.1/bin' dir: 'zcash-4.5.1-1/bin'
}, },
DASH: { DASH: {
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz', url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
@ -83,6 +83,8 @@ autostart=true
autorestart=true autorestart=true
stderr_logfile=/var/log/supervisor/${cryptoCode}${isWallet ? `-wallet` : ``}.err.log stderr_logfile=/var/log/supervisor/${cryptoCode}${isWallet ? `-wallet` : ``}.err.log
stdout_logfile=/var/log/supervisor/${cryptoCode}${isWallet ? `-wallet` : ``}.out.log stdout_logfile=/var/log/supervisor/${cryptoCode}${isWallet ? `-wallet` : ``}.out.log
stderr_logfile_backups=2
stdout_logfile_backups=2
environment=HOME="/root" environment=HOME="/root"
` `
} }

View file

@ -26,6 +26,6 @@ function updateCore (coinRec, isCurrentlyRunning) {
function setup (dataDir) { function setup (dataDir) {
const coinRec = coinUtils.getCryptoCurrency('ETH') const coinRec = coinUtils.getCryptoCurrency('ETH')
common.firewall([coinRec.defaultPort]) common.firewall([coinRec.defaultPort])
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --rpc` const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --http`
common.writeSupervisorConfig(coinRec, cmd) common.writeSupervisorConfig(coinRec, cmd)
} }

View file

@ -23,8 +23,8 @@ const PLUGINS = {
DASH: require('./dash.js'), DASH: require('./dash.js'),
ETH: require('./ethereum.js'), ETH: require('./ethereum.js'),
LTC: require('./litecoin.js'), LTC: require('./litecoin.js'),
ZEC: require('./zcash.js'), XMR: require('./monero.js'),
XMR: require('./monero.js') ZEC: require('./zcash.js')
} }
module.exports = {run} module.exports = {run}

View file

@ -37,14 +37,14 @@ function mapDispense (tx) {
if (_.isEmpty(bills)) return {} if (_.isEmpty(bills)) return {}
return { const res = {}
provisioned_1: bills[0].provisioned,
provisioned_2: bills[1].provisioned, _.forEach(it => {
dispensed_1: bills[0].dispensed, res[`provisioned_${it + 1}`] = bills[it].provisioned
dispensed_2: bills[1].dispensed, res[`denomination_${it + 1}`] = bills[it].denomination
rejected_1: bills[0].rejected, res[`dispensed_${it + 1}`] = bills[it].dispensed
rejected_2: bills[1].rejected, res[`rejected_${it + 1}`] = bills[it].rejected
denomination_1: bills[0].denomination, }, _.times(_.identity(), _.size(bills)))
denomination_2: bills[1].denomination
} return res
} }

View file

@ -108,16 +108,24 @@ function updateCassettes (t, tx) {
if (!dispenseOccurred(tx.bills)) return Promise.resolve() if (!dispenseOccurred(tx.bills)) return Promise.resolve()
const sql = `update devices set const sql = `update devices set
cassette1 = cassette1 - $1, ${_.size(tx.bills) > 0 ? `cassette1 = cassette1 - $1` : ``}
cassette2 = cassette2 - $2 ${_.size(tx.bills) > 1 ? `, cassette2 = cassette2 - $2` : ``}
where device_id = $3 ${_.size(tx.bills) > 2 ? `, cassette3 = cassette3 - $3` : ``}
returning cassette1, cassette2` ${_.size(tx.bills) > 3 ? `, cassette4 = cassette4 - $4` : ``}
where device_id = $${_.size(tx.bills) + 1}
returning
${_.size(tx.bills) > 0 ? `cassette1` : ``}
${_.size(tx.bills) > 1 ? `, cassette2`: ``}
${_.size(tx.bills) > 2 ? `, cassette3` : ``}
${_.size(tx.bills) > 3 ? `, cassette4` : ``}`
const values = [ const values = []
tx.bills[0].dispensed + tx.bills[0].rejected,
tx.bills[1].dispensed + tx.bills[1].rejected, _.forEach(it => values.push(
tx.deviceId tx.bills[it].dispensed + tx.bills[it].rejected
] ), _.times(_.identity(), _.size(tx.bills)))
values.push(tx.deviceId)
return t.one(sql, values) return t.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))) .then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))

View file

@ -4,7 +4,9 @@ const db = require('../db')
const T = require('../time') const T = require('../time')
const BN = require('../bn') const BN = require('../bn')
const REDEEMABLE_AGE = T.day // FP operations on Postgres result in very big errors.
// E.g.: 1853.013808 * 1000 = 1866149.494
const REDEEMABLE_AGE = T.day / 1000
const CASH_OUT_TRANSACTION_STATES = ` const CASH_OUT_TRANSACTION_STATES = `
case case
@ -47,12 +49,18 @@ function addDbBills (tx) {
const bills = tx.bills const bills = tx.bills
if (_.isEmpty(bills)) return tx if (_.isEmpty(bills)) return tx
return _.assign(tx, { const billsObj = {
provisioned1: bills[0].provisioned, provisioned1: bills[0]?.provisioned ?? 0,
provisioned2: bills[1].provisioned, provisioned2: bills[1]?.provisioned ?? 0,
denomination1: bills[0].denomination, provisioned3: bills[2]?.provisioned ?? 0,
denomination2: bills[1].denomination provisioned4: bills[3]?.provisioned ?? 0,
}) denomination1: bills[0]?.denomination ?? 0,
denomination2: bills[1]?.denomination ?? 0,
denomination3: bills[2]?.denomination ?? 0,
denomination4: bills[3]?.denomination ?? 0
}
return _.assign(tx, billsObj)
} }
function toDb (tx) { function toDb (tx) {
@ -84,12 +92,12 @@ function toObj (row) {
newObj.direction = 'cashOut' newObj.direction = 'cashOut'
const billFields = ['denomination1', 'denomination2', 'provisioned1', 'provisioned2'] const billFields = ['denomination1', 'denomination2', 'denomination3', 'denomination4', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4']
if (_.every(_.isNil, _.at(billFields, newObj))) return newObj if (_.every(_.isNil, _.at(billFields, newObj))) return newObj
if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values') if (_.some(_.isNil, _.at(billFields, newObj))) throw new Error('Missing cassette values')
const bills = [ const billFieldsArr = [
{ {
denomination: newObj.denomination1, denomination: newObj.denomination1,
provisioned: newObj.provisioned1 provisioned: newObj.provisioned1
@ -97,9 +105,21 @@ function toObj (row) {
{ {
denomination: newObj.denomination2, denomination: newObj.denomination2,
provisioned: newObj.provisioned2 provisioned: newObj.provisioned2
},
{
denomination: newObj.denomination3,
provisioned: newObj.provisioned3
},
{
denomination: newObj.denomination4,
provisioned: newObj.provisioned4
} }
] ]
// There can't be bills with denomination === 0.
// If a bill has denomination === 0, then that cassette is not set and should be filtered out.
const bills = _.filter(it => it.denomination > 0, billFieldsArr)
return _.set('bills', bills, _.omit(billFields, newObj)) return _.set('bills', bills, _.omit(billFields, newObj))
} }
@ -109,7 +129,7 @@ function redeemableTxs (deviceId) {
and redeem=$2 and redeem=$2
and dispense=$3 and dispense=$3
and provisioned_1 is not null and provisioned_1 is not null
and (extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) < $4` and extract(epoch from (now() - greatest(created, confirmed_at))) < $4`
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE]) return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
.then(_.map(toObj)) .then(_.map(toObj))

View file

@ -63,17 +63,12 @@ function postProcess (txVector, justAuthorized, pi) {
return bills return bills
}) })
.then(bills => { .then(bills => {
const provisioned1 = bills[0].provisioned const rec = {}
const provisioned2 = bills[1].provisioned
const denomination1 = bills[0].denomination
const denomination2 = bills[1].denomination
const rec = { _.forEach(it => {
provisioned_1: provisioned1, rec[`provisioned_${it + 1}`] = bills[it].provisioned
provisioned_2: provisioned2, rec[`denomination_${it + 1}`] = bills[it].denomination
denomination_1: denomination1, }, _.times(_.identity(), _.size(bills)))
denomination_2: denomination2
}
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx) return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
.then(_.constant({ bills })) .then(_.constant({ bills }))

View file

@ -23,6 +23,9 @@ function getMachines () {
cashbox: r.cashbox, cashbox: r.cashbox,
cassette1: r.cassette1, cassette1: r.cassette1,
cassette2: r.cassette2, cassette2: r.cassette2,
cassette3: r.cassette3,
cassette4: r.cassette4,
numberOfCassettes: r.number_of_cassettes,
version: r.version, version: r.version,
model: r.model, model: r.model,
pairedAt: new Date(r.created), pairedAt: new Date(r.created),
@ -74,21 +77,7 @@ function getMachineNames (config) {
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y))) const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance) const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
const addName = r => { return machines.map(addName(pings, events, config))
const cashOutConfig = configManager.getCashOut(r.deviceId, config)
const cashOut = !!cashOutConfig.active
const statuses = [
getStatus(
_.first(pings[r.deviceId]),
_.first(checkStuckScreen(events, r.name))
)
]
return _.assign(r, { cashOut, statuses })
}
return _.map(addName, machines)
}) })
} }
@ -115,6 +104,9 @@ function getMachine (machineId, config) {
cashbox: r.cashbox, cashbox: r.cashbox,
cassette1: r.cassette1, cassette1: r.cassette1,
cassette2: r.cassette2, cassette2: r.cassette2,
cassette3: r.cassette3,
cassette4: r.cassette4,
numberOfCassettes: r.number_of_cassettes,
version: r.version, version: r.version,
model: r.model, model: r.model,
pairedAt: new Date(r.created), pairedAt: new Date(r.created),
@ -127,7 +119,7 @@ function getMachine (machineId, config) {
.then(([machine, events, config]) => { .then(([machine, events, config]) => {
const pings = checkPings([machine]) const pings = checkPings([machine])
return [machine].map(addName(pings, events, config))[0] return addName(pings, events, config)(machine)
}) })
} }
@ -138,8 +130,8 @@ function renameMachine (rec) {
function resetCashOutBills (rec) { function resetCashOutBills (rec) {
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId }) const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2 WHERE device_id=$3;` const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4 WHERE device_id=$5;`
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance')) return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
} }
function emptyCashInBills (rec) { function emptyCashInBills (rec) {
@ -148,8 +140,8 @@ function emptyCashInBills (rec) {
} }
function setCassetteBills (rec) { function setCassetteBills (rec) {
const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3 where device_id=$4' const sql = 'update devices set cashbox=$1, cassette1=$2, cassette2=$3, cassette3=$4, cassette4=$5 where device_id=$6'
return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.deviceId]) return db.none(sql, [rec.cashbox, rec.cassettes[0], rec.cassettes[1], rec.cassettes[2], rec.cassettes[3], rec.deviceId])
} }
function unpair (rec) { function unpair (rec) {

View file

@ -1,6 +1,8 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const crypto = require('crypto') const crypto = require('crypto')
const logger = require('../logger')
function sha256 (buf) { function sha256 (buf) {
const hash = crypto.createHash('sha256') const hash = crypto.createHash('sha256')
@ -9,6 +11,7 @@ function sha256 (buf) {
} }
const populateDeviceId = function (req, res, next) { const populateDeviceId = function (req, res, next) {
logger.info(`DEBUG LOG - Method: ${req.method} Path: ${req.path}`)
const deviceId = _.isFunction(req.connection.getPeerCertificate) const deviceId = _.isFunction(req.connection.getPeerCertificate)
? sha256(req.connection.getPeerCertificate().raw) ? sha256(req.connection.getPeerCertificate().raw)
: null : null

View file

@ -18,7 +18,7 @@ const resolvers = {
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId) machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId)
}, },
Mutation: { Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }, context]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }, context) machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context)
} }
} }

View file

@ -17,6 +17,9 @@ const typeDef = gql`
cashbox: Int cashbox: Int
cassette1: Int cassette1: Int
cassette2: Int cassette2: Int
cassette3: Int
cassette4: Int
numberOfCassettes: Int
statuses: [MachineStatus] statuses: [MachineStatus]
latestEvent: MachineEvent latestEvent: MachineEvent
downloadSpeed: String downloadSpeed: String
@ -51,7 +54,7 @@ const typeDef = gql`
} }
type Mutation { type Mutation {
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, newName: String): Machine @auth machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, cassette3: Int, cassette4: Int, newName: String): Machine @auth
} }
` `

View file

@ -6,14 +6,14 @@ function getMachine (machineId) {
.then(machines => machines.find(({ deviceId }) => deviceId === machineId)) .then(machines => machines.find(({ deviceId }) => deviceId === machineId))
} }
function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, newName }, context) { function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }) {
const operatorId = context.res.locals.operatorId const operatorId = context.res.locals.operatorId
return getMachine(deviceId) return getMachine(deviceId)
.then(machine => { .then(machine => {
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
return machine return machine
}) })
.then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2], newName }, operatorId)) .then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2, cassette3, cassette4], newName }, operatorId))
.then(getMachine(deviceId)) .then(getMachine(deviceId))
} }

View file

@ -82,7 +82,7 @@ function batch (
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at, txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path, txs.tx_customer_photo_path AS tx_customer_photo_path,
(extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 AS expired (NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $1) AS expired
FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress' AND actions.action = 'provisionAddress'
@ -186,7 +186,7 @@ function getCustomerTransactionsBatch (ids) {
c.name AS customer_name, c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path, c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
(extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $3 AS expired (NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $3) AS expired
FROM cash_out_txs txs FROM cash_out_txs txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress' AND actions.action = 'provisionAddress'
@ -230,7 +230,7 @@ function single (txId) {
c.front_camera_path AS customer_front_camera_path, c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
(extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $2 AS expired (NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $2) AS expired
FROM cash_out_txs txs FROM cash_out_txs txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress' AND actions.action = 'provisionAddress'

View file

@ -55,6 +55,7 @@ const getGlobalLocale = it => getLocale(null, it)
const getWalletSettings = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.WALLETS))(it) const getWalletSettings = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.WALLETS))(it)
const getCashOut = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.CASH_OUT))(it) const getCashOut = (key, it) => _.compose(fromNamespace(key), fromNamespace(namespaces.CASH_OUT))(it)
const getGlobalCashOut = fromNamespace(namespaces.CASH_OUT)
const getOperatorInfo = fromNamespace(namespaces.OPERATOR_INFO) const getOperatorInfo = fromNamespace(namespaces.OPERATOR_INFO)
const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR) const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR)
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS) const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
@ -152,6 +153,7 @@ module.exports = {
getAllCryptoCurrencies, getAllCryptoCurrencies,
getTriggers, getTriggers,
getTriggersAutomation, getTriggersAutomation,
getGlobalCashOut,
getCashOut, getCashOut,
getCryptosFromWalletNamespace, getCryptosFromWalletNamespace,
getCryptoUnits getCryptoUnits

View file

@ -6,12 +6,12 @@ const _ = require('lodash/fp')
require('dotenv').config() require('dotenv').config()
const DATABASE = process.env.LAMASSU_DB ?? 'DEV' const DATABASE = process.env.LAMASSU_DB ?? 'PROD'
const dbMapping = psqlConf => ({ const dbMapping = psqlConf => ({
STRESS_TEST: _.replace('lamassu', 'lamassu_stress', psqlConf), STRESS_TEST: _.replace('lamassu', 'lamassu_stress', psqlConf),
RELEASE: _.replace('lamassu', 'lamassu_release', psqlConf), RELEASE: _.replace('lamassu', 'lamassu_release', psqlConf),
DEV: _.replace('lamassu', 'lamassu', psqlConf) DEV: _.replace('lamassu', 'lamassu', psqlConf),
PROD: _.replace('lamassu', 'lamassu', psqlConf)
}) })
/** /**

View file

@ -5,6 +5,10 @@ const db = require('./db')
const options = require('./options') const options = require('./options')
const logger = require('./logger') const logger = require('./logger')
// A machine on an older version (no multicassette code) could be paired with a server with multicassette code.
// This makes sure that the server stores a default value
const DEFAULT_NUMBER_OF_CASSETTES = 2
function pullToken (token) { function pullToken (token) {
const sql = `delete from pairing_tokens const sql = `delete from pairing_tokens
where token=$1 where token=$1
@ -13,23 +17,26 @@ function pullToken (token) {
} }
function unpair (deviceId) { function unpair (deviceId) {
const sql = 'delete from devices where device_id=$1'
const deleteMachinePings = 'delete from machine_pings where device_id=$1'
// TODO new-admin: We should remove all configs related to that device. This can get tricky. // TODO new-admin: We should remove all configs related to that device. This can get tricky.
return Promise.all([db.none(sql, [deviceId]), db.none(deleteMachinePings, [deviceId])]) return db.tx(t => {
const q1 = t.none('DELETE FROM devices WHERE device_id=$1', [deviceId])
const q2 = t.none('DELETE FROM machine_pings WHERE device_id=$1', [deviceId])
const q3 = t.none('DELETE FROM machine_network_heartbeat WHERE device_id=$1', [deviceId])
const q4 = t.none('DELETE FROM machine_network_performance WHERE device_id=$1', [deviceId])
return Promise.all([q1, q2, q3, q4])
})
} }
function pair (token, deviceId, machineModel) { function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF_CASSETTES) {
return pullToken(token) return pullToken(token)
.then(r => { .then(r => {
if (r.expired) return false if (r.expired) return false
const insertSql = `insert into devices (device_id, name) values ($1, $2) const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
on conflict (device_id) on conflict (device_id)
do update set paired=TRUE, display=TRUE` do update set paired=TRUE, display=TRUE`
return db.none(insertSql, [deviceId, r.name]) return db.none(insertSql, [deviceId, r.name, numOfCassettes])
.then(() => true) .then(() => true)
}) })
.catch(err => { .catch(err => {

View file

@ -112,8 +112,10 @@ function plugins (settings, deviceId) {
if (_.isEmpty(redeemableTxs)) return cassettes if (_.isEmpty(redeemableTxs)) return cassettes
const sumTxs = (sum, tx) => { const sumTxs = (sum, tx) => {
const bills = tx.bills // cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
const sameDenominations = a => a[0].denomination === a[1].denomination const bills = _.filter(it => it.denomination > 0, tx.bills)
const sameDenominations = a => a[0]?.denomination === a[1]?.denomination
const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills)) const doDenominationsMatch = _.every(sameDenominations, _.zip(cassettes, bills))
if (!doDenominationsMatch) { if (!doDenominationsMatch) {
@ -123,7 +125,7 @@ function plugins (settings, deviceId) {
return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills)) return _.map(r => r[0] + r[1].provisioned, _.zip(sum, tx.bills))
} }
const provisioned = _.reduce(sumTxs, [0, 0], redeemableTxs) const provisioned = _.reduce(sumTxs, _.times(_.constant(0), _.size(cassettes)), redeemableTxs)
const zipped = _.zip(_.map('count', cassettes), provisioned) const zipped = _.zip(_.map('count', cassettes), provisioned)
const counts = _.map(r => r[0] - r[1], zipped) const counts = _.map(r => r[0] - r[1], zipped)
@ -131,16 +133,15 @@ function plugins (settings, deviceId) {
throw new Error('Negative note count: %j', counts) throw new Error('Negative note count: %j', counts)
} }
return [ const computedCassettes = []
{ _.forEach(it => {
denomination: cassettes[0].denomination, computedCassettes.push({
count: counts[0] denomination: cassettes[it].denomination,
}, count: counts[it]
{ })
denomination: cassettes[1].denomination, }, _.times(_.identity(), _.size(cassettes)))
count: counts[1]
} return computedCassettes
]
} }
function buildAvailableCassettes (excludeTxId) { function buildAvailableCassettes (excludeTxId) {
@ -148,28 +149,32 @@ function plugins (settings, deviceId) {
if (!cashOutConfig.active) return Promise.resolve() if (!cashOutConfig.active) return Promise.resolve()
const denominations = [cashOutConfig.top, cashOutConfig.bottom]
const virtualCassettes = [Math.max(cashOutConfig.top, cashOutConfig.bottom) * 2]
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)]) return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
.then(([rec, _redeemableTxs]) => { .then(([rec, _redeemableTxs]) => {
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs) const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
const denominations = []
_.forEach(it => {
denominations.push(cashOutConfig[`cassette${it + 1}`])
}, _.times(_.identity(), rec.numberOfCassettes))
const virtualCassettes = [Math.max(...denominations) * 2]
const counts = argv.cassettes const counts = argv.cassettes
? argv.cassettes.split(',') ? argv.cassettes.split(',')
: rec.counts : rec.counts
const cassettes = [ if (rec.counts.length !== denominations.length) {
{ throw new Error('Denominations and respective counts do not match!')
denomination: parseInt(denominations[0], 10),
count: parseInt(counts[0], 10)
},
{
denomination: parseInt(denominations[1], 10),
count: parseInt(counts[1], 10)
} }
]
const cassettes = []
_.forEach(it => {
cassettes.push({
denomination: parseInt(denominations[it], 10),
count: parseInt(counts[it], 10)
})
}, _.times(_.identity(), rec.numberOfCassettes))
try { try {
return { return {
@ -320,7 +325,7 @@ function plugins (settings, deviceId) {
function dispenseAck (tx) { function dispenseAck (tx) {
const cashOutConfig = configManager.getCashOut(deviceId, settings.config) const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
const cassettes = [cashOutConfig.top, cashOutConfig.bottom] const cassettes = [cashOutConfig.cassette1, cashOutConfig.cassette2, cashOutConfig.cassette3, cashOutConfig.cassette4]
return dbm.addDispense(deviceId, tx, cassettes) return dbm.addDispense(deviceId, tx, cassettes)
} }
@ -614,8 +619,10 @@ function plugins (settings, deviceId) {
function checkDeviceCashBalances (fiatCode, device) { function checkDeviceCashBalances (fiatCode, device) {
const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config) const cashOutConfig = configManager.getCashOut(device.deviceId, settings.config)
const denomination1 = cashOutConfig.top const denomination1 = cashOutConfig.cassette1
const denomination2 = cashOutConfig.bottom const denomination2 = cashOutConfig.cassette2
const denomination3 = cashOutConfig.cassette3
const denomination4 = cashOutConfig.cassette4
const cashOutEnabled = cashOutConfig.active const cashOutEnabled = cashOutConfig.active
const isCassetteLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit const isCassetteLow = (have, max, limit) => cashOutEnabled && ((have / max) * 100) < limit
@ -632,7 +639,7 @@ function plugins (settings, deviceId) {
} }
: null : null
const cassette1Alert = isCassetteLow(device.cassette1, cassetteMaxCapacity, notifications.fillingPercentageCassette1) const cassette1Alert = device.numberOfCassettes >= 1 && isCassetteLow(device.cassette1, cassetteMaxCapacity, notifications.fillingPercentageCassette1)
? { ? {
code: 'LOW_CASH_OUT', code: 'LOW_CASH_OUT',
cassette: 1, cassette: 1,
@ -644,7 +651,7 @@ function plugins (settings, deviceId) {
} }
: null : null
const cassette2Alert = isCassetteLow(device.cassette2, cassetteMaxCapacity, notifications.fillingPercentageCassette2) const cassette2Alert = device.numberOfCassettes >= 2 && isCassetteLow(device.cassette2, cassetteMaxCapacity, notifications.fillingPercentageCassette2)
? { ? {
code: 'LOW_CASH_OUT', code: 'LOW_CASH_OUT',
cassette: 2, cassette: 2,
@ -656,7 +663,31 @@ function plugins (settings, deviceId) {
} }
: null : null
return _.compact([cashInAlert, cassette1Alert, cassette2Alert]) const cassette3Alert = device.numberOfCassettes >= 3 && isCassetteLow(device.cassette3, cassetteMaxCapacity, notifications.fillingPercentageCassette3)
? {
code: 'LOW_CASH_OUT',
cassette: 3,
machineName,
deviceId: device.deviceId,
notes: device.cassette3,
denomination: denomination3,
fiatCode
}
: null
const cassette4Alert = device.numberOfCassettes >= 4 && isCassetteLow(device.cassette4, cassetteMaxCapacity, notifications.fillingPercentageCassette4)
? {
code: 'LOW_CASH_OUT',
cassette: 4,
machineName,
deviceId: device.deviceId,
notes: device.cassette4,
denomination: denomination4,
fiatCode
}
: null
return _.compact([cashInAlert, cassette1Alert, cassette2Alert, cassette3Alert, cassette4Alert])
} }
function checkCryptoBalances (fiatCode, devices) { function checkCryptoBalances (fiatCode, devices) {

View file

@ -98,3 +98,17 @@ function parseConf (confPath) {
return res return res
} }
function rpcConfig (cryptoRec) {
try {
const configPath = coinUtils.configPath(cryptoRec)
const config = parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
throw new Error('Wallet is currently not installed')
}
}

View file

@ -8,24 +8,11 @@ const logger = require('../../../logger')
const { utils: coinUtils } = require('lamassu-coins') const { utils: coinUtils } = require('lamassu-coins')
const cryptoRec = coinUtils.getCryptoCurrency('BTC') const cryptoRec = coinUtils.getCryptoCurrency('BTC')
const configPath = coinUtils.configPath(cryptoRec, options.blockchainDir)
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function rpcConfig () {
try {
const config = jsonRpc.parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
throw new Error('wallet is currently not installed')
}
}
function fetch (method, params) { function fetch (method, params) {
return jsonRpc.fetch(rpcConfig(), method, params) return jsonRpc.fetch(rpcConfig, method, params)
} }
function checkCryptoCode (cryptoCode) { function checkCryptoCode (cryptoCode) {

View file

@ -8,24 +8,11 @@ const BN = require('../../../bn')
const E = require('../../../error') const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('DASH') const cryptoRec = coinUtils.getCryptoCurrency('DASH')
const configPath = coinUtils.configPath(cryptoRec, options.blockchainDir)
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function rpcConfig () {
try {
const config = jsonRpc.parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
throw new Error('wallet is currently not installed')
}
}
function fetch (method, params) { function fetch (method, params) {
return jsonRpc.fetch(rpcConfig(), method, params) return jsonRpc.fetch(rpcConfig, method, params)
} }
function checkCryptoCode (cryptoCode) { function checkCryptoCode (cryptoCode) {

View file

@ -8,23 +8,11 @@ const BN = require('../../../bn')
const E = require('../../../error') const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('LTC') const cryptoRec = coinUtils.getCryptoCurrency('LTC')
const configPath = coinUtils.configPath(cryptoRec, options.blockchainDir)
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function rpcConfig () {
try {
const config = jsonRpc.parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
throw new Error('wallet is currently not installed')
}
}
function fetch (method, params) { function fetch (method, params) {
return jsonRpc.fetch(rpcConfig(), method, params) return jsonRpc.fetch(rpcConfig, method, params)
} }
function checkCryptoCode (cryptoCode) { function checkCryptoCode (cryptoCode) {

View file

@ -9,24 +9,11 @@ const BN = require('../../../bn')
const E = require('../../../error') const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('ZEC') const cryptoRec = coinUtils.getCryptoCurrency('ZEC')
const configPath = coinUtils.configPath(cryptoRec, options.blockchainDir)
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function rpcConfig () {
try {
const config = jsonRpc.parseConf(configPath)
return {
username: config.rpcuser,
password: config.rpcpassword,
port: config.rpcport || cryptoRec.defaultPort
}
} catch (err) {
throw new Error('wallet is currently not installed')
}
}
function fetch (method, params) { function fetch (method, params) {
return jsonRpc.fetch(rpcConfig(), method, params) return jsonRpc.fetch(rpcConfig, method, params)
} }
function checkCryptoCode (cryptoCode) { function checkCryptoCode (cryptoCode) {

View file

@ -28,7 +28,7 @@ const TRADE_INTERVAL = 60 * T.seconds
const PONG_INTERVAL = 10 * T.seconds const PONG_INTERVAL = 10 * T.seconds
const LOGS_CLEAR_INTERVAL = 1 * T.day const LOGS_CLEAR_INTERVAL = 1 * T.day
const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes
const SANCTIONS_UPDATE_INTERVAL = 1 * T.week const SANCTIONS_UPDATE_INTERVAL = 1 * T.day
const RADAR_UPDATE_INTERVAL = 5 * T.minutes const RADAR_UPDATE_INTERVAL = 5 * T.minutes
const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const PRUNE_MACHINES_HEARTBEAT = 1 * T.day

View file

@ -26,13 +26,17 @@ exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
} }
exports.cassetteCounts = function cassetteCounts (deviceId) { exports.cassetteCounts = function cassetteCounts (deviceId) {
const sql = 'SELECT cassette1, cassette2 FROM devices ' + const sql = 'SELECT cassette1, cassette2, cassette3, cassette4, number_of_cassettes FROM devices ' +
'WHERE device_id=$1' 'WHERE device_id=$1'
return db.one(sql, [deviceId]) return db.one(sql, [deviceId])
.then(row => { .then(row => {
const counts = [row.cassette1, row.cassette2] const counts = []
return {counts} _.forEach(it => {
counts.push(row[`cassette${it + 1}`])
}, _.times(_.identity(), row.number_of_cassettes))
return { numberOfCassettes: row.number_of_cassettes, counts }
}) })
} }

View file

@ -10,8 +10,9 @@ function pair (req, res, next) {
const token = req.query.token const token = req.query.token
const deviceId = req.deviceId const deviceId = req.deviceId
const model = req.query.model const model = req.query.model
const numOfCassettes = req.query.numOfCassettes
return pairing.pair(token, deviceId, model) return pairing.pair(token, deviceId, model, numOfCassettes)
.then(isValid => { .then(isValid => {
if (isValid) return res.json({ status: 'paired' }) if (isValid) return res.json({ status: 'paired' })
throw httpError('Pairing failed') throw httpError('Pairing failed')

View file

@ -11,6 +11,20 @@ const semver = require('semver')
const state = require('../middlewares/state') const state = require('../middlewares/state')
const version = require('../../package.json').version const version = require('../../package.json').version
const urlsToPing = [
`us.archive.ubuntu.com`,
`uk.archive.ubuntu.com`,
`za.archive.ubuntu.com`,
`cn.archive.ubuntu.com`
]
const speedtestFiles = [
{
url: 'https://github.com/lamassu/speed-test-assets/raw/main/python-defaults_2.7.18-3.tar.gz',
size: 44668
}
]
function checkHasLightning (settings) { function checkHasLightning (settings) {
return configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2' return configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2'
} }
@ -86,7 +100,9 @@ function poll (req, res, next) {
operatorInfo, operatorInfo,
machineInfo, machineInfo,
triggers, triggers,
triggersAutomation triggersAutomation,
speedtestFiles,
urlsToPing
} }
// BACKWARDS_COMPATIBILITY 7.6 // BACKWARDS_COMPATIBILITY 7.6

View file

@ -5,7 +5,9 @@ const CashInTx = require('./cash-in/cash-in-tx')
const CashOutTx = require('./cash-out/cash-out-tx') const CashOutTx = require('./cash-out/cash-out-tx')
const T = require('./time') const T = require('./time')
const REDEEMABLE_AGE = T.day // FP operations on Postgres result in very big errors.
// E.g.: 1853.013808 * 1000 = 1866149.494
const REDEEMABLE_AGE = T.day / 1000
function process (tx, pi) { function process (tx, pi) {
const mtx = massage(tx, pi) const mtx = massage(tx, pi)
@ -78,7 +80,7 @@ function customerHistory (customerId, thresholdDays) {
AND fiat > 0 AND fiat > 0
UNION UNION
SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction, SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction,
(extract(epoch FROM (now() - greatest(txOut.created, txOut.confirmed_at))) * 1000) >= $4 AS expired (NOT txOut.dispense AND extract(epoch FROM (now() - greatest(txOut.created, txOut.confirmed_at))) >= $4) AS expired
FROM cash_out_txs txOut FROM cash_out_txs txOut
WHERE txOut.customer_id = $1 WHERE txOut.customer_id = $1
AND txOut.created > now() - interval $2 AND txOut.created > now() - interval $2

View file

@ -121,7 +121,7 @@ function mergeStatusMode (a, b) {
} }
function getWalletStatus (settings, tx) { function getWalletStatus (settings, tx) {
const fudgeFactorEnabled = configManager.getWalletSettings(tx.cryptoCode, settings.config).fudgeFactorActive const fudgeFactorEnabled = configManager.getGlobalCashOut(settings.config).fudgeFactorActive
const fudgeFactor = fudgeFactorEnabled ? 100 : 0 const fudgeFactor = fudgeFactorEnabled ? 100 : 0
const requested = tx.cryptoAtoms.minus(fudgeFactor) const requested = tx.cryptoAtoms.minus(fudgeFactor)

View file

@ -7,7 +7,11 @@ exports.up = function (next) {
'cash-out-1-refill', 'cash-out-1-refill',
'cash-out-1-empty', 'cash-out-1-empty',
'cash-out-2-refill', 'cash-out-2-refill',
'cash-out-2-empty' 'cash-out-2-empty',
'cash-out-3-refill',
'cash-out-3-empty',
'cash-out-4-refill',
'cash-out-4-empty'
)`, )`,
`ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`, `ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`,
`ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`, `ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`,

View file

@ -7,6 +7,8 @@ exports.up = function (next) {
.then(({ config }) => { .then(({ config }) => {
const fiatBalance1 = config.notifications_fiatBalanceCassette1 const fiatBalance1 = config.notifications_fiatBalanceCassette1
const fiatBalance2 = config.notifications_fiatBalanceCassette2 const fiatBalance2 = config.notifications_fiatBalanceCassette2
const fiatBalance3 = config.notifications_fiatBalanceCassette3
const fiatBalance4 = config.notifications_fiatBalanceCassette4
const overrides = config.notifications_fiatBalanceOverrides const overrides = config.notifications_fiatBalanceOverrides
const newConfig = {} const newConfig = {}
if (fiatBalance1) { if (fiatBalance1) {
@ -17,6 +19,14 @@ exports.up = function (next) {
newConfig.notifications_fillingPercentageCassette2 = (100 * (fiatBalance2 / cassetteMaxCapacity)).toFixed(0) newConfig.notifications_fillingPercentageCassette2 = (100 * (fiatBalance2 / cassetteMaxCapacity)).toFixed(0)
newConfig.notifications_fiatBalanceCassette2 = null newConfig.notifications_fiatBalanceCassette2 = null
} }
if (fiatBalance3) {
newConfig.notifications_fillingPercentageCassette3 = (100 * (fiatBalance3 / cassetteMaxCapacity)).toFixed(0)
newConfig.notifications_fiatBalanceCassette3 = null
}
if (fiatBalance4) {
newConfig.notifications_fillingPercentageCassette4 = (100 * (fiatBalance4 / cassetteMaxCapacity)).toFixed(0)
newConfig.notifications_fiatBalanceCassette4 = null
}
if (overrides) { if (overrides) {
newConfig.notifications_fiatBalanceOverrides = _.map(override => { newConfig.notifications_fiatBalanceOverrides = _.map(override => {
@ -27,6 +37,12 @@ exports.up = function (next) {
if (override.fiatBalanceCassette2) { if (override.fiatBalanceCassette2) {
newOverride.fillingPercentageCassette2 = (100 * (override.fiatBalanceCassette2 / cassetteMaxCapacity)).toFixed(0) newOverride.fillingPercentageCassette2 = (100 * (override.fiatBalanceCassette2 / cassetteMaxCapacity)).toFixed(0)
} }
if (override.fiatBalanceCassette3) {
newOverride.fillingPercentageCassette3 = (100 * (override.fiatBalanceCassette3 / cassetteMaxCapacity)).toFixed(0)
}
if (override.fiatBalanceCassette4) {
newOverride.fillingPercentageCassette4 = (100 * (override.fiatBalanceCassette4 / cassetteMaxCapacity)).toFixed(0)
}
newOverride.machine = override.machine newOverride.machine = override.machine
newOverride.id = override.id newOverride.id = override.id

View file

@ -0,0 +1,48 @@
var db = require('./db')
const _ = require('lodash/fp')
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
const { getMachines } = require('../lib/machine-loader')
exports.up = function (next) {
var sql = [
'ALTER TABLE devices ADD COLUMN cassette3 INTEGER NOT NULL DEFAULT 0',
'ALTER TABLE devices ADD COLUMN cassette4 INTEGER NOT NULL DEFAULT 0',
'ALTER TABLE cash_out_txs ADD COLUMN provisioned_3 INTEGER',
'ALTER TABLE cash_out_txs ADD COLUMN provisioned_4 INTEGER',
'ALTER TABLE cash_out_txs ADD COLUMN denomination_3 INTEGER',
'ALTER TABLE cash_out_txs ADD COLUMN denomination_4 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN provisioned_3 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN provisioned_4 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN dispensed_3 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN dispensed_4 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN rejected_3 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN rejected_4 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN denomination_3 INTEGER',
'ALTER TABLE cash_out_actions ADD COLUMN denomination_4 INTEGER',
'ALTER TABLE devices ADD COLUMN number_of_cassettes INTEGER NOT NULL DEFAULT 2'
]
return Promise.all([loadLatest(), getMachines()])
.then(([config, machines]) => {
const formattedMachines = _.map(it => _.pick(['deviceId'], it), machines)
const newConfig = _.reduce((acc, value) => {
if(_.includes(`cashOut_${value.deviceId}_top`, _.keys(config.config))) {
acc[`cashOut_${value.deviceId}_cassette1`] = config.config[`cashOut_${value.deviceId}_top`]
}
if(_.includes(`cashOut_${value.deviceId}_bottom`, _.keys(config.config))) {
acc[`cashOut_${value.deviceId}_cassette2`] = config.config[`cashOut_${value.deviceId}_bottom`]
}
return acc
}, {}, formattedMachines)
return saveConfig(newConfig)
.then(() => db.multi(sql, next))
.catch(err => next(err))
})
}
exports.down = function (next) {
next()
}

View file

@ -34,6 +34,7 @@ const Header = () => {
const { const {
elements, elements,
enableEdit, enableEdit,
enableEditText,
editWidth, editWidth,
enableDelete, enableDelete,
deleteWidth, deleteWidth,
@ -72,7 +73,7 @@ const Header = () => {
{innerElements.map(mapElement2)} {innerElements.map(mapElement2)}
{enableEdit && ( {enableEdit && (
<Td header width={editWidth} textAlign="center"> <Td header width={editWidth} textAlign="center">
Edit {enableEditText ?? `Edit`}
</Td> </Td>
)} )}
{enableDelete && ( {enableDelete && (

View file

@ -131,6 +131,7 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
suffix, suffix,
SuffixComponent = Label2, SuffixComponent = Label2,
textStyle = it => {}, textStyle = it => {},
isHidden = it => false,
view = it => it?.toString(), view = it => it?.toString(),
inputProps = {} inputProps = {}
} = config } = config
@ -168,22 +169,25 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
size={size} size={size}
bold={bold} bold={bold}
textAlign={textAlign}> textAlign={textAlign}>
{isEditing && isField && ( {isEditing && isField && !isHidden(values) && (
<Field name={name} component={input} {...innerProps} /> <Field name={name} component={input} {...innerProps} />
)} )}
{isEditing && !isField && <config.input name={name} />} {isEditing && !isField && !isHidden(values) && (
{!isEditing && values && ( <config.input name={name} />
)}
{!isEditing && values && !isHidden(values) && (
<div style={textStyle(values, isEditing)}> <div style={textStyle(values, isEditing)}>
{view(values[name], values)} {view(values[name], values)}
</div> </div>
)} )}
{suffix && ( {suffix && !isHidden(values) && (
<SuffixComponent <SuffixComponent
className={classes.suffix} className={classes.suffix}
style={isEditing ? {} : textStyle(values, isEditing)}> style={isEditing ? {} : textStyle(values, isEditing)}>
{suffix} {suffix}
</SuffixComponent> </SuffixComponent>
)} )}
{isHidden(values) && <StripesSvg />}
</Td> </Td>
) )
} }

View file

@ -15,7 +15,7 @@ export default {
paddingRight: 39 paddingRight: 39
}, },
withSuffix: ({ textAlign }) => { withSuffix: ({ textAlign }) => {
const justifyContent = textAlign === 'right' ? 'end' : textAlign const justifyContent = textAlign === 'right' ? 'flex-end' : textAlign
return { return {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',

View file

@ -37,6 +37,7 @@ const ETable = ({
validationSchema, validationSchema,
enableCreate, enableCreate,
enableEdit, enableEdit,
enableEditText,
editWidth: outerEditWidth, editWidth: outerEditWidth,
enableDelete, enableDelete,
deleteWidth = ACTION_COL_SIZE, deleteWidth = ACTION_COL_SIZE,
@ -87,7 +88,6 @@ const ETable = ({
} }
setAdding(false) setAdding(false)
setEditingId(null)
setEditing && setEditing(false) setEditing && setEditing(false)
setSaving(false) setSaving(false)
} }
@ -139,6 +139,7 @@ const ETable = ({
const ctxValue = { const ctxValue = {
elements, elements,
enableEdit, enableEdit,
enableEditText,
onEdit, onEdit,
clearError: () => setError(null), clearError: () => setError(null),
error: error, error: error,

View file

@ -13,6 +13,7 @@ const gridClasses = makeStyles(gridStyles)
const Cashbox = ({ const Cashbox = ({
percent = 0, percent = 0,
cashOut = false, cashOut = false,
width,
className, className,
emptyPartClassName, emptyPartClassName,
labelClassName, labelClassName,
@ -21,7 +22,13 @@ const Cashbox = ({
omitInnerPercentage, omitInnerPercentage,
isLow isLow
}) => { }) => {
const classes = cashboxClasses({ percent, cashOut, applyColorVariant, isLow }) const classes = cashboxClasses({
percent,
cashOut,
width,
applyColorVariant,
isLow
})
const ltHalf = percent <= 51 const ltHalf = percent <= 51
const showCashBox = { const showCashBox = {
@ -76,7 +83,8 @@ const CashOut = ({
notes, notes,
className, className,
editingMode = false, editingMode = false,
threshold threshold,
width
}) => { }) => {
const percent = (100 * notes) / capacity const percent = (100 * notes) / capacity
const isLow = percent < threshold const isLow = percent < threshold
@ -90,6 +98,7 @@ const CashOut = ({
percent={percent} percent={percent}
cashOut cashOut
isLow={isLow} isLow={isLow}
width={width}
/> />
</div> </div>
{!editingMode && ( {!editingMode && (

View file

@ -22,7 +22,7 @@ const cashboxStyles = {
borderColor: colorPicker, borderColor: colorPicker,
backgroundColor: colorPicker, backgroundColor: colorPicker,
height: 118, height: 118,
width: 80, width: ({ width }) => width ?? 80,
border: '2px solid', border: '2px solid',
textAlign: 'end', textAlign: 'end',
display: 'inline-block' display: 'inline-block'
@ -65,7 +65,7 @@ const gridStyles = {
justifyContent: 'flex-start' justifyContent: 'flex-start'
}, },
col2: { col2: {
marginLeft: 16 marginLeft: 14
}, },
noMarginText: { noMarginText: {
marginTop: 0, marginTop: 0,

View file

@ -1,4 +1,5 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import classNames from 'classnames'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { CashOut } from 'src/components/inputs/cashbox/Cashbox' import { CashOut } from 'src/components/inputs/cashbox/Cashbox'
@ -9,13 +10,13 @@ const useStyles = makeStyles({
display: 'flex' display: 'flex'
}, },
cashCassette: { cashCassette: {
width: 80,
height: 36, height: 36,
marginRight: 16 marginRight: 14
} }
}) })
const CashCassetteInput = memo(({ decimalPlaces, threshold, ...props }) => { const CashCassetteInput = memo(
({ decimalPlaces, width, threshold, inputClassName, ...props }) => {
const classes = useStyles() const classes = useStyles()
const { name, onChange, onBlur, value } = props.field const { name, onChange, onBlur, value } = props.field
const { touched, errors } = props.form const { touched, errors } = props.form
@ -24,9 +25,10 @@ const CashCassetteInput = memo(({ decimalPlaces, threshold, ...props }) => {
return ( return (
<div className={classes.flex}> <div className={classes.flex}>
<CashOut <CashOut
className={classes.cashCassette} className={classNames(classes.cashCassette, inputClassName)}
notes={notes} notes={notes}
editingMode={true} editingMode={true}
width={width}
threshold={threshold} threshold={threshold}
/> />
<NumberInput <NumberInput
@ -43,6 +45,7 @@ const CashCassetteInput = memo(({ decimalPlaces, threshold, ...props }) => {
/> />
</div> </div>
) )
}) }
)
export default CashCassetteInput export default CashCassetteInput

View file

@ -18,8 +18,7 @@ import { DenominationsSchema, getElements } from './helper'
const useStyles = makeStyles({ const useStyles = makeStyles({
fudgeFactor: { fudgeFactor: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center'
marginRight: 156
}, },
switchLabel: { switchLabel: {
margin: 6, margin: 6,
@ -44,6 +43,9 @@ const GET_INFO = gql`
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
numberOfCassettes
} }
config config
} }
@ -65,6 +67,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
} }
const config = data?.config && fromNamespace(SCREEN_KEY)(data.config) const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
const fudgeFactorActive = config?.fudgeFactorActive ?? false const fudgeFactorActive = config?.fudgeFactorActive ?? false
const locale = data?.config && fromNamespace('locale')(data.config) const locale = data?.config && fromNamespace('locale')(data.config)
const machines = data?.machines ?? [] const machines = data?.machines ?? []

View file

@ -7,16 +7,17 @@ import { Autocomplete } from 'src/components/inputs/formik'
import denominations from 'src/utils/bill-denominations' import denominations from 'src/utils/bill-denominations'
import { getBillOptions } from 'src/utils/bill-options' import { getBillOptions } from 'src/utils/bill-options'
import { toNamespace } from 'src/utils/config' import { toNamespace } from 'src/utils/config'
import { transformNumber } from 'src/utils/number'
import WizardSplash from './WizardSplash' import WizardSplash from './WizardSplash'
import WizardStep from './WizardStep' import WizardStep from './WizardStep'
import { DenominationsSchema } from './helper' import { DenominationsSchema } from './helper'
const LAST_STEP = 3
const MODAL_WIDTH = 554 const MODAL_WIDTH = 554
const MODAL_HEIGHT = 520 const MODAL_HEIGHT = 520
const Wizard = ({ machine, locale, onClose, save, error }) => { const Wizard = ({ machine, locale, onClose, save, error }) => {
const LAST_STEP = machine.numberOfCassettes + 2
const [{ step, config }, setState] = useState({ const [{ step, config }, setState] = useState({
step: 0, step: 0,
config: { active: true } config: { active: true }
@ -30,7 +31,10 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
const onContinue = async it => { const onContinue = async it => {
if (isLastStep) { if (isLastStep) {
return save( return save(
toNamespace(machine.deviceId, DenominationsSchema.cast(config)) toNamespace(
machine.deviceId,
DenominationsSchema.cast(config, { assert: false })
)
) )
} }
@ -42,33 +46,55 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
}) })
} }
const steps = [ const steps = []
{
type: 'top', R.until(
display: 'Cassette 1 (Top)', R.gt(R.__, machine.numberOfCassettes),
it => {
steps.push({
type: `cassette${it}`,
display: `Cassette ${it}`,
component: Autocomplete, component: Autocomplete,
inputProps: { inputProps: {
options: R.map(it => ({ code: it, display: it }))(options), options: R.map(it => ({ code: it, display: it }))(options),
labelProp: 'display', labelProp: 'display',
valueProp: 'code' valueProp: 'code'
} }
})
return R.add(1, it)
}, },
{ 1
type: 'bottom', )
display: 'Cassette 2',
component: Autocomplete, steps.push({
inputProps: { type: 'zeroConfLimit',
options: R.map(it => ({ code: it, display: it }))(options), display: '0-conf Limit',
labelProp: 'display', schema: Yup.object().shape({
valueProp: 'code' zeroConfLimit: Yup.number().required()
} })
} })
]
const schema = () => const schema = () =>
Yup.object().shape({ Yup.object().shape({
top: Yup.number().required(), cassette1: Yup.number().required(),
bottom: step >= 2 ? Yup.number().required() : Yup.number() cassette2:
machine.numberOfCassettes > 1 && step >= 2
? Yup.number().required()
: Yup.number()
.transform(transformNumber)
.nullable(),
cassette3:
machine.numberOfCassettes > 2 && step >= 3
? Yup.number().required()
: Yup.number()
.transform(transformNumber)
.nullable(),
cassette4:
machine.numberOfCassettes > 3 && step >= 4
? Yup.number().required()
: Yup.number()
.transform(transformNumber)
.nullable()
}) })
return ( return (
@ -85,6 +111,7 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
<WizardStep <WizardStep
step={step} step={step}
name={machine.name} name={machine.name}
numberOfCassettes={machine.numberOfCassettes}
error={error} error={error}
lastStep={isLastStep} lastStep={isLastStep}
steps={steps} steps={steps}

View file

@ -9,11 +9,36 @@ import { NumberInput } from 'src/components/inputs/formik'
import { Info2, H4, P, Info1, Label1 } from 'src/components/typography' import { Info2, H4, P, Info1, Label1 } from 'src/components/typography'
import cassetteOne from 'src/styling/icons/cassettes/cashout-cassette-1.svg' import cassetteOne from 'src/styling/icons/cassettes/cashout-cassette-1.svg'
import cassetteTwo from 'src/styling/icons/cassettes/cashout-cassette-2.svg' import cassetteTwo from 'src/styling/icons/cassettes/cashout-cassette-2.svg'
import tejo3CassetteOne from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-1-left.svg'
import tejo3CassetteTwo from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-2-left.svg'
import tejo3CassetteThree from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-3-left.svg'
import tejo4CassetteOne from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-1-left.svg'
import tejo4CassetteTwo from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-2-left.svg'
import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-3-left.svg'
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg' import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
import styles from './WizardStep.styles' import styles from './WizardStep.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const getCassetesArtworks = () => ({
2: {
1: cassetteOne,
2: cassetteTwo
},
3: {
1: tejo3CassetteOne,
2: tejo3CassetteTwo,
3: tejo3CassetteThree
},
4: {
1: tejo4CassetteOne,
2: tejo4CassetteTwo,
3: tejo4CassetteThree,
4: tejo4CassetteFour
}
})
const WizardStep = ({ const WizardStep = ({
name, name,
step, step,
@ -23,30 +48,31 @@ const WizardStep = ({
onContinue, onContinue,
steps, steps,
fiatCurrency, fiatCurrency,
options options,
numberOfCassettes
}) => { }) => {
const classes = useStyles() const classes = useStyles()
const label = lastStep ? 'Finish' : 'Next' const label = lastStep ? 'Finish' : 'Next'
const cassetesArtworks = {
1: cassetteOne,
2: cassetteTwo
}
return ( return (
<div className={classes.content}> <>
<div className={classes.titleDiv}> <div className={classes.titleDiv}>
<Info2 className={classes.title}>{name}</Info2> <Info2 className={classes.title}>{name}</Info2>
<Stepper steps={3} currentStep={step} /> <Stepper steps={steps.length + 1} currentStep={step} />
</div> </div>
{step <= 2 && ( {step <= numberOfCassettes && (
<Formik <Formik
validateOnBlur={false} validateOnBlur={false}
validateOnChange={false} validateOnChange={false}
onSubmit={onContinue} onSubmit={onContinue}
initialValues={{ top: '', bottom: '' }} initialValues={{
cassette1: '',
cassette2: '',
cassette3: '',
cassette4: ''
}}
enableReinitialize enableReinitialize
validationSchema={schema}> validationSchema={schema}>
<Form> <Form>
@ -84,8 +110,8 @@ const WizardStep = ({
className={classes.stepImage} className={classes.stepImage}
alt="cassette" alt="cassette"
width="148" width="148"
height="196" height="205"
src={cassetesArtworks[step]}></img> src={getCassetesArtworks()[numberOfCassettes][step]}></img>
</div> </div>
<Button className={classes.submit} type="submit"> <Button className={classes.submit} type="submit">
@ -94,6 +120,46 @@ const WizardStep = ({
</Form> </Form>
</Formik> </Formik>
)} )}
{step === numberOfCassettes + 1 && (
<Formik
validateOnBlur={false}
validateOnChange={false}
onSubmit={onContinue}
initialValues={{ zeroConfLimit: '' }}
enableReinitialize
validationSchema={steps[step - 1].schema}>
<Form>
<div className={classes.thirdStepHeader}>
<div className={classes.step}>
<H4 className={classes.edit}>Edit 0-conf Limit</H4>
<Label1>Choose a limit</Label1>
<div className={classes.bill}>
<Field
className={classes.billInput}
type="text"
size="lg"
autoFocus={true}
component={NumberInput}
fullWidth
decimalPlaces={0}
name={steps[step - 1].type}
/>
<Info1 noMargin className={classes.suffix}>
{fiatCurrency}
</Info1>
</div>
</div>
</div>
<Button className={classes.submit} type="submit">
{label}
</Button>
</Form>
</Formik>
)}
{lastStep && ( {lastStep && (
<div className={classes.disclaimer}> <div className={classes.disclaimer}>
<Info2 className={classes.title}>Cash-out Bill Count</Info2> <Info2 className={classes.title}>Cash-out Bill Count</Info2>
@ -122,7 +188,7 @@ const WizardStep = ({
</div> </div>
</div> </div>
)} )}
</div> </>
) )
} }

View file

@ -41,7 +41,7 @@ export default {
}, },
header: { header: {
display: 'flex', display: 'flex',
paddingBottom: 95 marginBottom: 95
}, },
thirdStepHeader: { thirdStepHeader: {
display: 'flex', display: 'flex',

View file

@ -6,21 +6,44 @@ import { bold } from 'src/styling/helpers'
import denominations from 'src/utils/bill-denominations' import denominations from 'src/utils/bill-denominations'
import { getBillOptions } from 'src/utils/bill-options' import { getBillOptions } from 'src/utils/bill-options'
import { CURRENCY_MAX } from 'src/utils/constants' import { CURRENCY_MAX } from 'src/utils/constants'
import { transformNumber } from 'src/utils/number'
const DenominationsSchema = Yup.object().shape({ const DenominationsSchema = Yup.object().shape({
top: Yup.number() cassette1: Yup.number()
.label('Cassette 1 (Top)') .label('Cassette 1')
.required() .required()
.min(1) .min(1)
.max(CURRENCY_MAX), .max(CURRENCY_MAX),
bottom: Yup.number() cassette2: Yup.number()
.label('Cassette 2 (Bottom)') .label('Cassette 2')
.required() .required()
.min(1) .min(1)
.max(CURRENCY_MAX),
cassette3: Yup.number()
.label('Cassette 3')
.min(1)
.max(CURRENCY_MAX)
.nullable()
.transform(transformNumber),
cassette4: Yup.number()
.label('Cassette 4')
.min(1)
.max(CURRENCY_MAX)
.nullable()
.transform(transformNumber),
zeroConfLimit: Yup.number()
.label('0-conf Limit')
.required()
.min(0)
.max(CURRENCY_MAX) .max(CURRENCY_MAX)
}) })
const getElements = (machines, locale = {}, classes) => { const getElements = (machines, locale = {}, classes) => {
const fiatCurrency = R.prop('fiatCurrency')(locale)
const maxNumberOfCassettes = Math.max(
...R.map(it => it.numberOfCassettes, machines)
)
const options = getBillOptions(locale, denominations) const options = getBillOptions(locale, denominations)
const cassetteProps = const cassetteProps =
options?.length > 0 options?.length > 0
@ -32,7 +55,7 @@ const getElements = (machines, locale = {}, classes) => {
} }
: { decimalPlaces: 0 } : { decimalPlaces: 0 }
return [ const elements = [
{ {
name: 'id', name: 'id',
header: 'Machine', header: 'Machine',
@ -40,32 +63,50 @@ const getElements = (machines, locale = {}, classes) => {
view: it => machines.find(({ deviceId }) => deviceId === it).name, view: it => machines.find(({ deviceId }) => deviceId === it).name,
size: 'sm', size: 'sm',
editable: false editable: false
},
{
name: 'top',
header: 'Cassette 1 (Top)',
stripe: true,
width: 250,
textAlign: 'right',
view: it => it,
input: options?.length > 0 ? Autocomplete : NumberInput,
inputProps: cassetteProps,
suffix: R.prop('fiatCurrency')(locale),
bold: bold
},
{
name: 'bottom',
header: 'Cassette 2 (Bottom)',
stripe: true,
textAlign: 'right',
width: 250,
view: it => it,
input: options?.length > 0 ? Autocomplete : NumberInput,
inputProps: cassetteProps,
suffix: R.prop('fiatCurrency')(locale),
bold: bold
} }
] ]
R.until(
R.gt(R.__, maxNumberOfCassettes),
it => {
elements.push({
name: `cassette${it}`,
header: `Cassette ${it}`,
size: 'sm',
stripe: true,
textAlign: 'right',
width: (maxNumberOfCassettes > 2 ? 600 : 460) / maxNumberOfCassettes,
suffix: fiatCurrency,
bold: bold,
view: it => it,
input: options?.length > 0 ? Autocomplete : NumberInput,
inputProps: cassetteProps,
doubleHeader: 'Denominations',
isHidden: machine =>
it >
machines.find(({ deviceId }) => deviceId === machine.id)
.numberOfCassettes
})
return R.add(1, it)
},
1
)
elements.push({
name: 'zeroConfLimit',
header: '0-conf Limit',
size: 'sm',
stripe: true,
textAlign: 'right',
width: maxNumberOfCassettes > 2 ? 150 : 290,
input: NumberInput,
inputProps: {
decimalPlaces: 0
},
suffix: fiatCurrency
})
return elements
} }
export { DenominationsSchema, getElements } export { DenominationsSchema, getElements }

View file

@ -6,6 +6,7 @@ import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead' import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow' import TableRow from '@material-ui/core/TableRow'
import classnames from 'classnames' import classnames from 'classnames'
import * as R from 'ramda'
import React from 'react' import React from 'react'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
@ -60,6 +61,10 @@ const MachinesTable = ({ machines, numToRender }) => {
}) })
} }
const maxNumberOfCassettes = Math.max(
...R.map(it => it.numberOfCassettes, machines)
)
return ( return (
<TableContainer className={classes.table}> <TableContainer className={classes.table}>
<Table> <Table>
@ -80,18 +85,17 @@ const MachinesTable = ({ machines, numToRender }) => {
<TxInIcon /> <TxInIcon />
</div> </div>
</HeaderCell> */} </HeaderCell> */}
{R.map(
it => (
<HeaderCell> <HeaderCell>
<div className={classes.header}> <div className={classes.header}>
<TxOutIcon /> <TxOutIcon />
<Label2 className={classes.label}> 1</Label2> <Label2 className={classes.label}> {it + 1}</Label2>
</div>
</HeaderCell>
<HeaderCell>
<div className={classes.header}>
<TxOutIcon />
<Label2 className={classes.label}> 2</Label2>
</div> </div>
</HeaderCell> </HeaderCell>
),
R.times(R.identity, maxNumberOfCassettes)
)}
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
@ -120,12 +124,19 @@ const MachinesTable = ({ machines, numToRender }) => {
{/* <StyledCell align="left"> {/* <StyledCell align="left">
{makePercentageText(machine.cashbox)} {makePercentageText(machine.cashbox)}
</StyledCell> */} </StyledCell> */}
{R.map(
it =>
machine.numberOfCassettes > it ? (
<StyledCell align="left"> <StyledCell align="left">
{makePercentageText(machine.cassette1)} {makePercentageText(machine[`cassette${it + 1}`])}
</StyledCell> </StyledCell>
) : (
<StyledCell align="left"> <StyledCell align="left">
{makePercentageText(machine.cassette2)} <TL2>{`— %`}</TL2>
</StyledCell> </StyledCell>
),
R.times(R.identity, maxNumberOfCassettes)
)}
</TableRow> </TableRow>
) )
} }

View file

@ -27,6 +27,9 @@ const GET_DATA = gql`
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
numberOfCassettes
statuses { statuses {
label label
type type

View file

@ -1,18 +1,25 @@
import { useMutation } from '@apollo/react-hooks' import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda'
import React from 'react' import React from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Table as EditableTable } from 'src/components/editableTable' import { Table as EditableTable } from 'src/components/editableTable'
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox' import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput } from 'src/components/inputs/formik' import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import styles from './Cassettes.styles' import styles from './Cassettes.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const widthsByNumberOfCassettes = {
2: { cashbox: 116, cassette: 280, cassetteGraph: 80, editWidth: 174 },
3: { cashbox: 106, cassette: 200, cassetteGraph: 60, editWidth: 145 },
4: { cashbox: 106, cassette: 164, cassetteGraph: 40, editWidth: 90 }
}
const ValidationSchema = Yup.object().shape({ const ValidationSchema = Yup.object().shape({
name: Yup.string().required('Required'), name: Yup.string().required('Required'),
cashbox: Yup.number() cashbox: Yup.number()
@ -27,6 +34,16 @@ const ValidationSchema = Yup.object().shape({
.min(0) .min(0)
.max(500), .max(500),
cassette2: Yup.number() cassette2: Yup.number()
.required('Required')
.integer()
.min(0)
.max(500),
cassette3: Yup.number()
.required('Required')
.integer()
.min(0)
.max(500),
cassette4: Yup.number()
.required('Required') .required('Required')
.integer() .integer()
.min(0) .min(0)
@ -40,6 +57,8 @@ const SET_CASSETTE_BILLS = gql`
$cashbox: Int! $cashbox: Int!
$cassette1: Int! $cassette1: Int!
$cassette2: Int! $cassette2: Int!
$cassette3: Int!
$cassette4: Int!
) { ) {
machineAction( machineAction(
deviceId: $deviceId deviceId: $deviceId
@ -47,11 +66,15 @@ const SET_CASSETTE_BILLS = gql`
cashbox: $cashbox cashbox: $cashbox
cassette1: $cassette1 cassette1: $cassette1
cassette2: $cassette2 cassette2: $cassette2
cassette3: $cassette3
cassette4: $cassette4
) { ) {
deviceId deviceId
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
} }
} }
` `
@ -64,6 +87,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
const fillingPercentageSettings = const fillingPercentageSettings =
config && fromNamespace('notifications', config) config && fromNamespace('notifications', config)
const fiatCurrency = locale?.fiatCurrency const fiatCurrency = locale?.fiatCurrency
const numberOfCassettes = machine.numberOfCassettes
const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout) const getCashoutSettings = deviceId => fromNamespace(deviceId)(cashout)
const isCashOutDisabled = ({ deviceId }) => const isCashOutDisabled = ({ deviceId }) =>
@ -73,7 +97,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
{ {
name: 'cashbox', name: 'cashbox',
header: 'Cashbox', header: 'Cashbox',
width: 240, width: widthsByNumberOfCassettes[numberOfCassettes].cashbox,
stripe: false, stripe: false,
view: value => ( view: value => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} /> <CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
@ -82,61 +106,63 @@ const CashCassettes = ({ machine, config, refetchData }) => {
inputProps: { inputProps: {
decimalPlaces: 0 decimalPlaces: 0
} }
},
{
name: 'cassette1',
header: 'Cash-out 1',
width: 265,
stripe: true,
view: (value, { deviceId }) => (
<CashOut
className={classes.cashbox}
denomination={getCashoutSettings(deviceId)?.top}
currency={{ code: fiatCurrency }}
notes={value}
threshold={fillingPercentageSettings.fillingPercentageCassette1}
/>
),
input: NumberInput,
inputProps: {
decimalPlaces: 0
} }
}, ]
{
name: 'cassette2', R.until(
header: 'Cash-out 2', R.gt(R.__, numberOfCassettes),
width: 265, it => {
elements.push({
name: `cassette${it}`,
header: `Cash-out ${it}`,
width: widthsByNumberOfCassettes[numberOfCassettes].cassette,
stripe: true, stripe: true,
view: (value, { deviceId }) => { doubleHeader: 'Cash-out',
view: value => {
return ( return (
<CashOut <CashOut
className={classes.cashbox} className={classes.cashbox}
denomination={getCashoutSettings(deviceId)?.bottom} denomination={
getCashoutSettings(machine.deviceId)?.[`cassette${it}`]
}
currency={{ code: fiatCurrency }} currency={{ code: fiatCurrency }}
notes={value} notes={value}
threshold={fillingPercentageSettings.fillingPercentageCassette2} width={widthsByNumberOfCassettes[numberOfCassettes].cassetteGraph}
threshold={
fillingPercentageSettings[`fillingPercentageCassette${it}`]
}
/> />
) )
}, },
input: NumberInput, isHidden: ({ numberOfCassettes }) => it > numberOfCassettes,
input: CashCassetteInput,
inputProps: { inputProps: {
decimalPlaces: 0 decimalPlaces: 0,
width: widthsByNumberOfCassettes[numberOfCassettes].cassetteGraph,
inputClassName: classes.cashbox
} }
} })
] return R.add(1, it)
},
1
)
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, { const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
refetchQueries: () => refetchData() refetchQueries: () => refetchData()
}) })
const onSave = (...[, { deviceId, cashbox, cassette1, cassette2 }]) => { const onSave = (
...[, { deviceId, cashbox, cassette1, cassette2, cassette3, cassette4 }]
) => {
return setCassetteBills({ return setCassetteBills({
variables: { variables: {
action: 'setCassetteBills', action: 'setCassetteBills',
deviceId: deviceId, deviceId: deviceId,
cashbox, cashbox,
cassette1, cassette1,
cassette2 cassette2,
cassette3,
cassette4
} }
}) })
} }
@ -144,6 +170,8 @@ const CashCassettes = ({ machine, config, refetchData }) => {
return machine.name ? ( return machine.name ? (
<EditableTable <EditableTable
error={error?.message} error={error?.message}
enableEdit
editWidth={widthsByNumberOfCassettes[numberOfCassettes].editWidth}
stripeWhen={isCashOutDisabled} stripeWhen={isCashOutDisabled}
disableRowEdit={isCashOutDisabled} disableRowEdit={isCashOutDisabled}
name="cashboxes" name="cashboxes"

View file

@ -1,6 +1,5 @@
const styles = { const styles = {
cashbox: { cashbox: {
width: 80,
height: 36 height: 36
} }
} }

View file

@ -32,6 +32,9 @@ const GET_INFO = gql`
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
numberOfCassettes
statuses { statuses {
label label
type type
@ -84,15 +87,6 @@ const Machines = () => {
<Overview data={machine} onActionSuccess={refetch} /> <Overview data={machine} onActionSuccess={refetch} />
</div> </div>
</Grid> </Grid>
<Grid item xs={12}>
{/* on hold for now <Sidebar
isSelected={R.equals(selectedMachine)}
selectItem={setSelectedMachine}
data={machines}
getText={R.prop('name')}
getKey={R.prop('deviceId')}
/> */}
</Grid>
</Grid> </Grid>
<Grid item xs={9}> <Grid item xs={9}>
<div className={classes.content}> <div className={classes.content}>

View file

@ -37,19 +37,39 @@ const ValidationSchema = Yup.object().shape({
.min(0) .min(0)
.max(1000), .max(1000),
cassette1: Yup.number() cassette1: Yup.number()
.label('Cassette 1 (top)') .label('Cassette 1')
.required() .required()
.integer() .integer()
.min(0) .min(0)
.max(500), .max(500),
cassette2: Yup.number() cassette2: Yup.number()
.label('Cassette 2 (bottom)') .label('Cassette 2')
.required()
.integer()
.min(0)
.max(500),
cassette3: Yup.number()
.label('Cassette 3')
.required()
.integer()
.min(0)
.max(500),
cassette4: Yup.number()
.label('Cassette 4')
.required() .required()
.integer() .integer()
.min(0) .min(0)
.max(500) .max(500)
}) })
const CREATE_BATCH = gql`
mutation createBatch($deviceId: ID, $cashboxCount: Int) {
createBatch(deviceId: $deviceId, cashboxCount: $cashboxCount) {
id
}
}
`
const GET_MACHINES_AND_CONFIG = gql` const GET_MACHINES_AND_CONFIG = gql`
query getData { query getData {
machines { machines {
@ -58,6 +78,9 @@ const GET_MACHINES_AND_CONFIG = gql`
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
numberOfCassettes
} }
config config
} }
@ -85,6 +108,8 @@ const SET_CASSETTE_BILLS = gql`
$cashbox: Int! $cashbox: Int!
$cassette1: Int! $cassette1: Int!
$cassette2: Int! $cassette2: Int!
$cassette3: Int!
$cassette4: Int!
) { ) {
machineAction( machineAction(
deviceId: $deviceId deviceId: $deviceId
@ -92,11 +117,15 @@ const SET_CASSETTE_BILLS = gql`
cashbox: $cashbox cashbox: $cashbox
cassette1: $cassette1 cassette1: $cassette1
cassette2: $cassette2 cassette2: $cassette2
cassette3: $cassette3
cassette4: $cassette4
) { ) {
deviceId deviceId
cashbox cashbox
cassette1 cassette1
cassette2 cassette2
cassette3
cassette4
} }
} }
` `
@ -117,6 +146,7 @@ const CashCassettes = () => {
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, { const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const [createBatch] = useMutation(CREATE_BATCH)
const [saveConfig] = useMutation(SAVE_CONFIG, { const [saveConfig] = useMutation(SAVE_CONFIG, {
onCompleted: () => setEditingSchema(false), onCompleted: () => setEditingSchema(false),
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
@ -129,6 +159,37 @@ const CashCassettes = () => {
const cashout = data?.config && fromNamespace('cashOut')(data.config) const cashout = data?.config && fromNamespace('cashOut')(data.config)
const locale = data?.config && fromNamespace('locale')(data.config) const locale = data?.config && fromNamespace('locale')(data.config)
const fiatCurrency = locale?.fiatCurrency const fiatCurrency = locale?.fiatCurrency
const maxNumberOfCassettes = Math.max(
...R.map(it => it.numberOfCassettes, machines)
)
const cashboxCounts = R.reduce(
(ret, m) => R.assoc(m.id, m.cashbox, ret),
{},
machines
)
const onSave = (id, cashbox, cassette1, cassette2, cassette3, cassette4) => {
const oldCashboxCount = cashboxCounts[id]
if (cashbox < oldCashboxCount) {
createBatch({
variables: {
deviceId: id,
cashboxCount: oldCashboxCount
}
})
}
return setCassetteBills({
variables: {
action: 'setCassetteBills',
deviceId: id,
cashbox,
cassette1,
cassette2,
cassette3,
cassette4
}
})
}
const cashboxReset = const cashboxReset =
data?.config && fromNamespace('cashIn')(data.config).cashboxReset data?.config && fromNamespace('cashIn')(data.config).cashboxReset
@ -144,17 +205,7 @@ const CashCassettes = () => {
setEditingSchema(false) setEditingSchema(false)
} }
} }
const onSave = (id, cashbox, cassette1, cassette2) => {
return setCassetteBills({
variables: {
action: 'setCassetteBills',
deviceId: id,
cashbox,
cassette1,
cassette2
}
})
}
const getCashoutSettings = id => fromNamespace(id)(cashout) const getCashoutSettings = id => fromNamespace(id)(cashout)
const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active const isCashOutDisabled = ({ id }) => !getCashoutSettings(id).active
@ -172,14 +223,14 @@ const CashCassettes = () => {
{ {
name: 'name', name: 'name',
header: 'Machine', header: 'Machine',
width: 254, width: 184,
view: name => <>{name}</>, view: name => <>{name}</>,
input: ({ field: { value: name } }) => <>{name}</> input: ({ field: { value: name } }) => <>{name}</>
}, },
{ {
name: 'cashbox', name: 'cashbox',
header: 'Cashbox', header: 'Cash-in',
width: 240, width: maxNumberOfCassettes > 2 ? 140 : 280,
view: value => ( view: value => (
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} /> <CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
), ),
@ -187,54 +238,47 @@ const CashCassettes = () => {
inputProps: { inputProps: {
decimalPlaces: 0 decimalPlaces: 0
} }
}, }
{ ]
name: 'cassette1',
header: 'Cassette 1 (Top)', R.until(
width: 265, R.gt(R.__, maxNumberOfCassettes),
it => {
elements.push({
name: `cassette${it}`,
header: `Cassette ${it}`,
width: (maxNumberOfCassettes > 2 ? 700 : 560) / maxNumberOfCassettes,
stripe: true, stripe: true,
doubleHeader: 'Cash-out',
view: (value, { id }) => ( view: (value, { id }) => (
<CashOut <CashOut
className={classes.cashbox} className={classes.cashbox}
denomination={getCashoutSettings(id)?.top} denomination={getCashoutSettings(id)?.[`cassette${it}`]}
currency={{ code: fiatCurrency }} currency={{ code: fiatCurrency }}
notes={value} notes={value}
threshold={fillingPercentageSettings.fillingPercentageCassette1} width={50}
threshold={
fillingPercentageSettings[`fillingPercentageCassette${it}`]
}
/> />
), ),
isHidden: ({ numberOfCassettes }) => it > numberOfCassettes,
input: CashCassetteInput, input: CashCassetteInput,
inputProps: { inputProps: {
decimalPlaces: 0, decimalPlaces: 0,
threshold: fillingPercentageSettings.fillingPercentageCassette1 width: 50,
inputClassName: classes.cashbox
} }
})
return R.add(1, it)
}, },
{ 1
name: 'cassette2',
header: 'Cassette 2 (Bottom)',
width: 265,
stripe: true,
view: (value, { id }) => {
return (
<CashOut
className={classes.cashbox}
denomination={getCashoutSettings(id)?.bottom}
currency={{ code: fiatCurrency }}
notes={value}
threshold={fillingPercentageSettings.fillingPercentageCassette2}
/>
) )
},
input: CashCassetteInput, elements.push({
inputProps: {
decimalPlaces: 0,
threshold: fillingPercentageSettings.fillingPercentageCassette2
}
},
{
name: 'edit', name: 'edit',
header: 'Edit', header: 'Edit',
width: 175, width: 87,
textAlign: 'center',
view: (value, { id }) => { view: (value, { id }) => {
return ( return (
<IconButton <IconButton
@ -246,8 +290,7 @@ const CashCassettes = () => {
</IconButton> </IconButton>
) )
} }
} })
]
return ( return (
<> <>
@ -292,7 +335,6 @@ const CashCassettes = () => {
stripeWhen={isCashOutDisabled} stripeWhen={isCashOutDisabled}
elements={elements} elements={elements}
data={machines} data={machines}
save={onSave}
validationSchema={ValidationSchema} validationSchema={ValidationSchema}
tbodyWrapperClass={classes.tBody} tbodyWrapperClass={classes.tBody}
/> />

View file

@ -2,7 +2,6 @@ import { offColor } from 'src/styling/variables'
export default { export default {
cashbox: { cashbox: {
width: 80,
height: 36 height: 36
}, },
tableContainer: { tableContainer: {

View file

@ -25,16 +25,23 @@ const CashCassettesFooter = ({
const classes = useStyles() const classes = useStyles()
const cashout = config && fromNamespace('cashOut')(config) const cashout = config && fromNamespace('cashOut')(config)
const getCashoutSettings = id => fromNamespace(id)(cashout) const getCashoutSettings = id => fromNamespace(id)(cashout)
const reducerFn = (acc, { cassette1, cassette2, id }) => { const reducerFn = (
const topDenomination = getCashoutSettings(id).top ?? 0 acc,
const bottomDenomination = getCashoutSettings(id).bottom ?? 0 { cassette1, cassette2, cassette3, cassette4, id }
) => {
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
const cassette3Denomination = getCashoutSettings(id).cassette3 ?? 0
const cassette4Denomination = getCashoutSettings(id).cassette4 ?? 0
return [ return [
(acc[0] += cassette1 * topDenomination), (acc[0] += cassette1 * cassette1Denomination),
(acc[1] += cassette2 * bottomDenomination) (acc[1] += cassette2 * cassette2Denomination),
(acc[2] += cassette3 * cassette3Denomination),
(acc[3] += cassette4 * cassette4Denomination)
] ]
} }
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0], machines)) const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0, 0, 0], machines))
/* const totalInCashBox = R.sum( /* const totalInCashBox = R.sum(
R.flatten( R.flatten(

View file

@ -76,38 +76,34 @@ const CashboxHistory = ({ machines, currency }) => {
const batches = R.path(['cashboxBatches'])(data) const batches = R.path(['cashboxBatches'])(data)
const getOperationRender = { const getOperationRender = R.reduce(
(ret, i) =>
R.pipe(
R.assoc(
`cash-out-${i}-refill`,
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out {i} refill</span>
</>
),
R.assoc(
`cash-out-${i}-empty`,
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out {i} emptied</span>
</>
)
)(ret),
{
'cash-in-empty': ( 'cash-in-empty': (
<> <>
<TxInIcon /> <TxInIcon />
<span className={classes.operationType}>Cash-in emptied</span> <span className={classes.operationType}>Cash-in emptied</span>
</> </>
),
'cash-out-1-refill': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 1 refill</span>
</>
),
'cash-out-1-empty': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 1 emptied</span>
</>
),
'cash-out-2-refill': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 2 refill</span>
</>
),
'cash-out-2-empty': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 2 emptied</span>
</>
) )
} },
R.range(1, 5)
)
const save = row => { const save = row => {
const field = R.find(f => f.id === row.id, fields) const field = R.find(f => f.id === row.id, fields)

View file

@ -32,7 +32,9 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
const isCashOutDisabled = const isCashOutDisabled =
R.isEmpty(cashoutSettings) || !cashoutSettings?.active R.isEmpty(cashoutSettings) || !cashoutSettings?.active
const LAST_STEP = isCashOutDisabled ? 1 : 3 const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
const LAST_STEP = numberOfCassettes + 1
const title = `Update counts` const title = `Update counts`
const isLastStep = step === LAST_STEP const isLastStep = step === LAST_STEP
@ -57,12 +59,8 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
}) })
} }
save( const { cassette1, cassette2, cassette3, cassette4 } = R.map(parseInt, it)
machine.id, save(machine.id, cashbox, cassette1, cassette2, cassette3, cassette4)
parseInt(cashbox),
parseInt(it.cassette1Count ?? 0),
parseInt(it.cassette2Count ?? 0)
)
return onClose() return onClose()
} }
@ -72,7 +70,24 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
}) })
} }
const steps = [ const makeCassetteSteps = R.pipe(
R.add(1),
R.range(1),
R.map(i => ({
type: `cassette ${i}`,
schema: Yup.object().shape({
[`cassette${i}`]: Yup.number()
.label('Bill count')
.positive()
.integer()
.required()
.min(0)
.max(CASHBOX_DEFAULT_CAPACITY)
})
}))
)
const steps = R.prepend(
{ {
type: 'cashbox', type: 'cashbox',
schema: Yup.object().shape({ schema: Yup.object().shape({
@ -80,33 +95,8 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
}), }),
cashoutRequired: false cashoutRequired: false
}, },
{ makeCassetteSteps(numberOfCassettes)
type: 'cassette 1', )
schema: Yup.object().shape({
cassette1Count: Yup.number()
.label('Bill count')
.required()
.min(0)
.max(CASHBOX_DEFAULT_CAPACITY)
}),
cashoutRequired: true
},
{
type: 'cassette 2',
schema: Yup.object().shape({
cassette2Count: Yup.number()
.label('Bill count')
.required()
.min(0)
.max(CASHBOX_DEFAULT_CAPACITY)
}),
cashoutRequired: true
}
]
const filteredSteps = R.filter(it => {
return !it.cashoutRequired || (!isCashOutDisabled && it.cashoutRequired)
}, steps)
return ( return (
<Modal <Modal
@ -127,7 +117,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
cassetteCapacity={CASHBOX_DEFAULT_CAPACITY} cassetteCapacity={CASHBOX_DEFAULT_CAPACITY}
error={error} error={error}
lastStep={isLastStep} lastStep={isLastStep}
steps={filteredSteps} steps={steps}
fiatCurrency={locale.fiatCurrency} fiatCurrency={locale.fiatCurrency}
onContinue={onContinue} onContinue={onContinue}
/> />

View file

@ -13,6 +13,13 @@ import { Info2, H4, P, Info1 } from 'src/components/typography'
import cashbox from 'src/styling/icons/cassettes/acceptor-left.svg' import cashbox from 'src/styling/icons/cassettes/acceptor-left.svg'
import cassetteOne from 'src/styling/icons/cassettes/dispenser-1.svg' import cassetteOne from 'src/styling/icons/cassettes/dispenser-1.svg'
import cassetteTwo from 'src/styling/icons/cassettes/dispenser-2.svg' import cassetteTwo from 'src/styling/icons/cassettes/dispenser-2.svg'
import tejo3CassetteOne from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-1-left.svg'
import tejo3CassetteTwo from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-2-left.svg'
import tejo3CassetteThree from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-3-left.svg'
import tejo4CassetteOne from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-1-left.svg'
import tejo4CassetteTwo from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-2-left.svg'
import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-3-left.svg'
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { comet, errorColor } from 'src/styling/variables' import { comet, errorColor } from 'src/styling/variables'
@ -91,6 +98,13 @@ const styles = {
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const cassetesArtworks = (numberOfCassettes, step) =>
[
[cassetteOne, cassetteTwo],
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
][numberOfCassettes - 2][step - 2]
const WizardStep = ({ const WizardStep = ({
step, step,
name, name,
@ -107,28 +121,23 @@ const WizardStep = ({
const label = lastStep ? 'Finish' : 'Confirm' const label = lastStep ? 'Finish' : 'Confirm'
const cassetesArtworks = {
1: cashbox,
2: cassetteOne,
3: cassetteTwo
}
const stepOneRadioOptions = [ const stepOneRadioOptions = [
{ display: 'Yes', code: 'YES' }, { display: 'Yes', code: 'YES' },
{ display: 'No', code: 'NO' } { display: 'No', code: 'NO' }
] ]
const cassetteInfo = { const cassetteField = `cassette${step - 1}`
amount: step === 2 ? machine?.cassette1 : machine?.cassette2, const numberOfCassettes = machine.numberOfCassettes
denomination: step === 2 ? cashoutSettings.top : cashoutSettings.bottom const originalCassetteCount = machine?.[cassetteField]
} const cassetteDenomination = cashoutSettings?.[cassetteField]
const getPercentage = values => { const cassetteCount = values => values[cassetteField] || originalCassetteCount
const cassetteCount = const cassetteTotal = values => cassetteCount(values) * cassetteDenomination
step === 2 ? values.cassette1Count : values.cassette2Count const getPercentage = R.pipe(
const value = cassetteCount ?? cassetteInfo.amount cassetteCount,
return R.clamp(0, 100, 100 * (value / cassetteCapacity)) count => 100 * (count / cassetteCapacity),
} R.clamp(0, 100)
)
return ( return (
<div className={classes.content}> <div className={classes.content}>
@ -144,7 +153,7 @@ const WizardStep = ({
onSubmit={onContinue} onSubmit={onContinue}
initialValues={{ wasCashboxEmptied: '' }} initialValues={{ wasCashboxEmptied: '' }}
enableReinitialize enableReinitialize
validationSchema={steps[step - 1].schema}> validationSchema={steps[0].schema}>
{({ values, errors }) => ( {({ values, errors }) => (
<Form> <Form>
<div <div
@ -152,7 +161,7 @@ const WizardStep = ({
<img <img
className={classes.stepImage} className={classes.stepImage}
alt="cassette" alt="cassette"
src={cassetesArtworks[step]}></img> src={cashbox}></img>
<div className={classes.formWrapper}> <div className={classes.formWrapper}>
<div <div
className={classnames( className={classnames(
@ -205,12 +214,17 @@ const WizardStep = ({
</Formik> </Formik>
)} )}
{(step === 2 || step === 3) && ( {step > 1 && (
<Formik <Formik
validateOnBlur={false} validateOnBlur={false}
validateOnChange={false} validateOnChange={false}
onSubmit={onContinue} onSubmit={onContinue}
initialValues={{ cassette1Count: '', cassette2Count: '' }} initialValues={{
cassette1: '',
cassette2: '',
cassette3: '',
cassette4: ''
}}
enableReinitialize enableReinitialize
validationSchema={steps[step - 1].schema}> validationSchema={steps[step - 1].schema}>
{({ values, errors }) => ( {({ values, errors }) => (
@ -220,7 +234,7 @@ const WizardStep = ({
<img <img
className={classes.stepImage} className={classes.stepImage}
alt="cassette" alt="cassette"
src={cassetesArtworks[step]}></img> src={cassetesArtworks(numberOfCassettes, step)}></img>
<div className={classes.formWrapper}> <div className={classes.formWrapper}>
<div <div
className={classnames( className={classnames(
@ -260,27 +274,16 @@ const WizardStep = ({
component={NumberInput} component={NumberInput}
decimalPlaces={0} decimalPlaces={0}
width={50} width={50}
placeholder={cassetteInfo.amount.toString()} placeholder={originalCassetteCount.toString()}
name={`cassette${step - 1}Count`} name={cassetteField}
className={classes.cashboxBills} className={classes.cashboxBills}
/> />
<P> <P>
{cassetteInfo.denomination} {fiatCurrency} bills loaded {cassetteDenomination} {fiatCurrency} bills loaded
</P> </P>
</div> </div>
<P noMargin className={classes.fiatTotal}> <P noMargin className={classes.fiatTotal}>
={' '} = {cassetteTotal(values)} {fiatCurrency}
{step === 2
? (values.cassette1Count ?? 0) *
cassetteInfo.denomination
: (values.cassette2Count ?? 0) *
cassetteInfo.denomination}{' '}
{fiatCurrency}
</P>
<P className={classes.errorMessage}>
{step === 2
? errors.cassette1Count
: errors.cassette2Count}
</P> </P>
</div> </div>
</div> </div>

View file

@ -22,6 +22,7 @@ const GET_INFO = gql`
machines { machines {
name name
deviceId deviceId
numberOfCassettes
} }
cryptoCurrencies { cryptoCurrencies {
code code

View file

@ -18,13 +18,23 @@ import styles from './FiatBalanceAlerts.styles.js'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const NAME = 'fiatBalanceAlerts' const NAME = 'fiatBalanceAlerts'
const DEFAULT_NUMBER_OF_CASSETTES = 2
const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => { const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
const { isEditing, isDisabled, setEditing, data, save } = useContext( const {
NotificationsCtx isEditing,
) isDisabled,
setEditing,
data,
save,
machines
} = useContext(NotificationsCtx)
const classes = useStyles() const classes = useStyles()
const maxNumberOfCassettes =
Math.max(...R.map(it => it.numberOfCassettes, machines)) ??
DEFAULT_NUMBER_OF_CASSETTES
const editing = isEditing(NAME) const editing = isEditing(NAME)
const schema = Yup.object().shape({ const schema = Yup.object().shape({
@ -35,6 +45,18 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
.max(max) .max(max)
.nullable(), .nullable(),
fillingPercentageCassette2: Yup.number() fillingPercentageCassette2: Yup.number()
.transform(transformNumber)
.integer()
.min(min)
.max(max)
.nullable(),
fiatBalanceCassette3: Yup.number()
.transform(transformNumber)
.integer()
.min(min)
.max(max)
.nullable(),
fiatBalanceCassette4: Yup.number()
.transform(transformNumber) .transform(transformNumber)
.integer() .integer()
.min(min) .min(min)
@ -48,10 +70,10 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
validateOnChange={false} validateOnChange={false}
enableReinitialize enableReinitialize
initialValues={{ initialValues={{
fillingPercentageCassette1: fillingPercentageCassette1: data?.fillingPercentageCassette1 ?? '',
R.path(['fillingPercentageCassette1'])(data) ?? '', fillingPercentageCassette2: data?.fillingPercentageCassette2 ?? '',
fillingPercentageCassette2: fillingPercentageCassette3: data?.fillingPercentageCassette3 ?? '',
R.path(['fillingPercentageCassette2'])(data) ?? '' fillingPercentageCassette4: data?.fillingPercentageCassette4 ?? ''
}} }}
validationSchema={schema} validationSchema={schema}
onSubmit={it => save(section, schema.cast(it))} onSubmit={it => save(section, schema.cast(it))}
@ -68,14 +90,16 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
setEditing={it => setEditing(NAME, it)} setEditing={it => setEditing(NAME, it)}
/> />
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div className={classes.first}> {R.map(
it => (
<>
<div className={classes.row}> <div className={classes.row}>
<Cashbox <Cashbox
labelClassName={classes.cashboxLabel} labelClassName={classes.cashboxLabel}
emptyPartClassName={classes.cashboxEmptyPart} emptyPartClassName={classes.cashboxEmptyPart}
percent={ percent={
values.fillingPercentageCassette1 ?? values[`fillingPercentageCassette${it + 1}`] ??
data?.fillingPercentageCassette1 data[`cassette${it + 1}`]
} }
applyColorVariant applyColorVariant
applyFiatBalanceAlertsStyling applyFiatBalanceAlertsStyling
@ -83,36 +107,10 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
cashOut cashOut
/> />
<div className={classes.col2}> <div className={classes.col2}>
<TL2 className={classes.title}>Cassette 1 (Top)</TL2> <TL2 className={classes.title}>Cassette {it + 1}</TL2>
<EditableNumber <EditableNumber
label="Alert me under" label="Alert me under"
name="fillingPercentageCassette1" name={`fillingPercentageCassette${it + 1}`}
editing={editing}
displayValue={x => (x === '' ? '-' : x)}
decoration="%"
width={fieldWidth}
/>
</div>
</div>
</div>
<div className={classes.row}>
<Cashbox
labelClassName={classes.cashboxLabel}
emptyPartClassName={classes.cashboxEmptyPart}
percent={
values.fillingPercentageCassette2 ??
data?.fillingPercentageCassette2
}
applyColorVariant
applyFiatBalanceAlertsStyling
omitInnerPercentage
cashOut
/>
<div className={classes.col2}>
<TL2 className={classes.title}>Cassette 2 (Bottom)</TL2>
<EditableNumber
label="Alert me under"
name="fillingPercentageCassette2"
editing={editing} editing={editing}
displayValue={x => (x === '' ? '-' : x)} displayValue={x => (x === '' ? '-' : x)}
decoration="%" decoration="%"
@ -120,6 +118,10 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
/> />
</div> </div>
</div> </div>
</>
),
R.times(R.identity, maxNumberOfCassettes)
)}
</div> </div>
</Form> </Form>
)} )}

View file

@ -7,14 +7,11 @@ export default {
form: { form: {
marginBottom: 36 marginBottom: 36
}, },
first: {
width: 236
},
title: { title: {
marginTop: 0 marginTop: 0
}, },
row: { row: {
width: 183, width: 236,
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(2,1fr)', gridTemplateColumns: 'repeat(2,1fr)',
gridTemplateRows: '1fr', gridTemplateRows: '1fr',

View file

@ -11,9 +11,18 @@ import NotificationsCtx from '../NotificationsContext'
const CASSETTE_1_KEY = 'fillingPercentageCassette1' const CASSETTE_1_KEY = 'fillingPercentageCassette1'
const CASSETTE_2_KEY = 'fillingPercentageCassette2' const CASSETTE_2_KEY = 'fillingPercentageCassette2'
const CASSETTE_3_KEY = 'fillingPercentageCassette3'
const CASSETTE_4_KEY = 'fillingPercentageCassette4'
const MACHINE_KEY = 'machine' const MACHINE_KEY = 'machine'
const NAME = 'fiatBalanceOverrides' const NAME = 'fiatBalanceOverrides'
const CASSETTE_LIST = [
CASSETTE_1_KEY,
CASSETTE_2_KEY,
CASSETTE_3_KEY,
CASSETTE_4_KEY
]
const FiatBalanceOverrides = ({ section }) => { const FiatBalanceOverrides = ({ section }) => {
const { const {
machines = [], machines = [],
@ -41,42 +50,62 @@ const FiatBalanceOverrides = ({ section }) => {
const initialValues = { const initialValues = {
[MACHINE_KEY]: null, [MACHINE_KEY]: null,
[CASSETTE_1_KEY]: '', [CASSETTE_1_KEY]: '',
[CASSETTE_2_KEY]: '' [CASSETTE_2_KEY]: '',
[CASSETTE_3_KEY]: '',
[CASSETTE_4_KEY]: ''
} }
const maxNumberOfCassettes = Math.max(
...R.map(it => it.numberOfCassettes, machines)
)
const percentMin = 0 const percentMin = 0
const percentMax = 100 const percentMax = 100
const validationSchema = Yup.object().shape( const validationSchema = Yup.object()
{ .shape({
[MACHINE_KEY]: Yup.string() [MACHINE_KEY]: Yup.string()
.label('Machine') .label('Machine')
.nullable() .nullable()
.required(), .required(),
[CASSETTE_1_KEY]: Yup.number() [CASSETTE_1_KEY]: Yup.number()
.label('Cassette 1 (top)') .label('Cassette 1')
.when(CASSETTE_2_KEY, {
is: CASSETTE_2_KEY => !CASSETTE_2_KEY,
then: Yup.number().required()
})
.transform(transformNumber) .transform(transformNumber)
.integer() .integer()
.min(percentMin) .min(percentMin)
.max(percentMax) .max(percentMax)
.nullable(), .nullable(),
[CASSETTE_2_KEY]: Yup.number() [CASSETTE_2_KEY]: Yup.number()
.label('Cassette 2 (bottom)') .label('Cassette 2')
.when(CASSETTE_1_KEY, { .transform(transformNumber)
is: CASSETTE_1_KEY => !CASSETTE_1_KEY, .integer()
then: Yup.number().required() .min(percentMin)
}) .max(percentMax)
.nullable(),
[CASSETTE_3_KEY]: Yup.number()
.label('Cassette 3')
.transform(transformNumber)
.integer()
.min(percentMin)
.max(percentMax)
.nullable(),
[CASSETTE_4_KEY]: Yup.number()
.label('Cassette 4')
.transform(transformNumber) .transform(transformNumber)
.integer() .integer()
.min(percentMin) .min(percentMin)
.max(percentMax) .max(percentMax)
.nullable() .nullable()
}, })
[CASSETTE_1_KEY, CASSETTE_2_KEY] .test((values, context) => {
) const picked = R.pick(CASSETTE_LIST, values)
if (CASSETTE_LIST.some(it => !R.isNil(picked[it]))) return
return context.createError({
path: CASSETTE_1_KEY,
message: 'At least one of the cassettes must have a value'
})
})
const viewMachine = it => const viewMachine = it =>
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines) R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
@ -93,35 +122,35 @@ const FiatBalanceOverrides = ({ section }) => {
valueProp: 'deviceId', valueProp: 'deviceId',
labelProp: 'name' labelProp: 'name'
} }
},
{
name: CASSETTE_1_KEY,
header: 'Cash-out 1',
width: 155,
textAlign: 'right',
doubleHeader: 'Cash-out (Cassette Empty)',
bold: true,
input: NumberInput,
suffix: '%',
inputProps: {
decimalPlaces: 0
}
},
{
name: CASSETTE_2_KEY,
header: 'Cash-out 2',
width: 155,
textAlign: 'right',
doubleHeader: 'Cash-out (Cassette Empty)',
bold: true,
input: NumberInput,
suffix: '%',
inputProps: {
decimalPlaces: 0
}
} }
] ]
R.until(
R.gt(R.__, maxNumberOfCassettes),
it => {
elements.push({
name: `fillingPercentageCassette${it}`,
display: `Cash-out ${it}`,
width: 155,
textAlign: 'right',
doubleHeader: 'Cash-out (Cassette Empty)',
bold: true,
input: NumberInput,
suffix: '%',
inputProps: {
decimalPlaces: 0
},
view: it => it?.toString() ?? '—',
isHidden: value =>
it >
machines.find(({ deviceId }) => deviceId === value.machine)
?.numberOfCassettes
})
return R.add(1, it)
},
1
)
return ( return (
<EditableTable <EditableTable
name={NAME} name={NAME}

View file

@ -306,7 +306,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
) : ( ) : (
errorElements errorElements
)} )}
{getStatus(tx) === 'Pending' && ( {tx.txClass === 'cashOut' && getStatus(tx) === 'Pending' && (
<ActionButton <ActionButton
color="primary" color="primary"
Icon={CancelIcon} Icon={CancelIcon}
@ -362,5 +362,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
export default memo( export default memo(
DetailsRow, DetailsRow,
(prev, next) => (prev, next) =>
prev.it.id === next.it.id && getStatus(prev.it) === getStatus(next.it) prev.it.id === next.it.id &&
prev.it.hasError === next.it.hasError &&
getStatus(prev.it) === getStatus(next.it)
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-10, .cls-4 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-4, .cls-8 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #ebefff;
}
.cls-9 {
fill: #1b2559;
}
.cls-10 {
fill: #ccd8ff;
}
.cls-11 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="30.32 222.37 141.47 158 181.89 181.41 70.74 245.78 30.32 222.37"/>
<polyline class="cls-2" points="141.48 202.52 68.77 244.64 32.31 223.53 141.48 160.3"/>
<polygon class="cls-3" points="70.74 292.59 181.9 228.22 181.89 181.41 70.74 245.78 70.74 292.59"/>
<polygon class="cls-4" points="141.48 160.3 141.48 202.52 159.71 191.97 177.93 181.41 141.48 160.3"/>
</g>
<g>
<polygon class="cls-1" points="30.32 163.85 141.47 99.48 181.89 122.89 70.74 187.26 30.32 163.85"/>
<polyline class="cls-2" points="141.48 144 68.77 186.12 32.31 165.01 141.48 101.78"/>
<polygon class="cls-3" points="70.74 234.07 181.9 169.71 181.89 122.89 70.74 187.26 70.74 234.07"/>
<polygon class="cls-4" points="141.48 101.78 141.48 144 159.71 133.45 177.93 122.89 141.48 101.78"/>
</g>
<polyline class="cls-5" points="121.27 97.18 68.77 127.6 32.31 106.49 121.27 54.96"/>
<polygon class="cls-6" points="30.32 105.33 121.26 52.67 161.68 76.07 70.74 128.74 30.32 105.33"/>
<polygon class="cls-7" points="10.1 70.22 10.11 280.89 70.74 316 70.74 105.33 10.1 70.22"/>
<polygon class="cls-3" points="20.21 169.71 60.63 193.11 60.63 239.93 20.21 216.52 20.21 169.71"/>
<polygon class="cls-3" points="20.21 228.22 60.63 251.63 60.63 298.45 20.21 275.04 20.21 228.22"/>
<polygon class="cls-8" points="0 122.89 40.42 146.3 40.42 193.11 0 169.7 0 122.89"/>
<polygon class="cls-5" points="40.42 193.11 60.63 181.41 60.63 134.59 40.42 146.3 40.42 193.11"/>
<polygon class="cls-5" points="70.74 175.56 161.69 122.89 161.68 76.07 70.74 128.74 70.74 175.56"/>
<polyline class="cls-6" points="60.63 134.59 40.42 146.3 0 122.89 20.21 111.19"/>
<polyline class="cls-5" points="58.65 133.44 40.42 144 3.97 122.89 22.2 112.33"/>
<polygon class="cls-9" points="22.2 112.33 22.2 133.44 40.42 144 58.65 133.44 22.2 112.33"/>
<polygon class="cls-9" points="121.27 54.96 121.27 97.18 139.49 86.63 157.72 76.07 121.27 54.96"/>
<g>
<polygon class="cls-10" points="70.74 105.33 192 35.11 192 245.78 70.74 316 70.74 105.33"/>
<polygon class="cls-11" points="10.1 70.22 131.37 0 192 35.11 70.74 105.33 10.1 70.22"/>
</g>
<path class="cls-12" d="M13.08,161.89l4.4,2.94,0-11.26c0-.68,0-1.35,0-1.35l-.06,0a1.79,1.79,0,0,1-.88.55l-1.63.46-2.17-3.74L17.88,148l3.22,2.15v17.13l4.4,2.95v3.18l-12.42-8.31Z"/>
<path class="cls-7" d="M32.61,210.69c0-7.57,9.51-3.1,9.51-7.41a5.94,5.94,0,0,0-3.07-4.79c-2.36-1.36-3.65.41-3.65.41l-2.73-3.49s1.87-3.15,6.7-.43c3.56,2,6.57,6.06,6.57,10.2,0,7-9.08,2.71-9.18,6.67l9.5,5.92v3.44l-13.48-8.55A16.93,16.93,0,0,1,32.61,210.69Z"/>
<path class="cls-7" d="M33.74,265.64a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.37-4.34-5.85l-1.4-.88-.82-2.4,3.71-2.09a7.93,7.93,0,0,1,1.46-.63v-.06s-.6-.29-1.8-1l-6-3.78v-3.17l12.38,7.72V260l-5,2.65c2.8,2.15,5.54,5.86,5.54,9.38s-2.64,5-7.16,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-10, .cls-4 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-4, .cls-8 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #ebefff;
}
.cls-9 {
fill: #1b2559;
}
.cls-10 {
fill: #ccd8ff;
}
.cls-11 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="161.68 222.37 50.52 158 10.11 181.41 121.26 245.78 161.68 222.37"/>
<polyline class="cls-2" points="50.52 202.52 123.23 244.64 159.69 223.53 50.52 160.3"/>
<polygon class="cls-3" points="121.26 292.59 10.1 228.22 10.11 181.41 121.26 245.78 121.26 292.59"/>
<polygon class="cls-4" points="50.52 160.3 50.52 202.52 32.29 191.97 14.07 181.41 50.52 160.3"/>
</g>
<g>
<polygon class="cls-1" points="161.68 163.85 50.53 99.48 10.11 122.89 121.26 187.26 161.68 163.85"/>
<polyline class="cls-2" points="50.52 144 123.23 186.12 159.69 165.01 50.52 101.78"/>
<polygon class="cls-3" points="121.26 234.07 10.1 169.71 10.11 122.89 121.26 187.26 121.26 234.07"/>
<polygon class="cls-4" points="50.52 101.78 50.52 144 32.29 133.45 14.07 122.89 50.52 101.78"/>
</g>
<polyline class="cls-5" points="70.73 97.18 123.23 127.6 159.69 106.49 70.73 54.96"/>
<polygon class="cls-6" points="161.68 105.33 70.74 52.67 30.32 76.07 121.26 128.74 161.68 105.33"/>
<polygon class="cls-7" points="181.9 70.22 181.89 280.89 121.26 316 121.26 105.33 181.9 70.22"/>
<polygon class="cls-3" points="171.79 169.71 131.37 193.11 131.37 239.93 171.79 216.52 171.79 169.71"/>
<polygon class="cls-3" points="171.79 228.22 131.37 251.63 131.37 298.45 171.79 275.04 171.79 228.22"/>
<polygon class="cls-8" points="192 122.89 151.58 146.3 151.58 193.11 192 169.7 192 122.89"/>
<polygon class="cls-5" points="151.58 193.11 131.37 181.41 131.37 134.59 151.58 146.3 151.58 193.11"/>
<polygon class="cls-5" points="121.26 175.56 30.32 122.89 30.32 76.07 121.26 128.74 121.26 175.56"/>
<polyline class="cls-6" points="131.37 134.59 151.58 146.3 192 122.89 171.79 111.19"/>
<polyline class="cls-5" points="133.35 133.44 151.58 144 188.03 122.89 169.81 112.33"/>
<polygon class="cls-9" points="169.81 112.33 169.81 133.44 151.58 144 133.35 133.44 169.81 112.33"/>
<polygon class="cls-9" points="70.73 54.96 70.73 97.18 52.51 86.63 34.28 76.07 70.73 54.96"/>
<g>
<polygon class="cls-10" points="121.26 105.33 0 35.11 0 245.78 121.26 316 121.26 105.33"/>
<polygon class="cls-11" points="181.9 70.22 60.63 0 0 35.11 121.26 105.33 181.9 70.22"/>
</g>
<path class="cls-12" d="M166.87,169.18l4.35-2.47-.05-11.22c0-.68,0-1.38,0-1.38l-.05,0a10,10,0,0,1-.87,1.62l-1.59,2.44-2.12-1.11,4.95-7.64,3.14-1.74.12,17,4.35-2.47,0,3.13-12.29,7Z"/>
<path class="cls-7" d="M145.58,218.78c0-7.34,9.32-13.48,9.29-17.48,0-1.75-1.37-2.09-3-1.21-2.32,1.23-3.57,4.29-3.57,4.29l-2.68-.41a15.18,15.18,0,0,1,6.54-7.51c3.47-1.83,6.45-1.23,6.49,2.55.07,6.43-8.86,12.32-8.93,16.19l9.37-5,0,3.13-13.34,7.21A13,13,0,0,1,145.58,218.78Z"/>
<path class="cls-7" d="M147.85,270a4.94,4.94,0,0,0,4.55-.34c2.09-1.11,3.6-3.19,3.59-4.92,0-2.12-2-2.1-4.3-.87l-1.38.72-.81-1.46,3.61-6.28c.78-1.34,1.41-2.27,1.41-2.27v-.06s-.58.39-1.75,1l-5.91,3.07v-3.17l12.07-6.25,0,2.29-4.81,8.22c2.75-1,5.46-.48,5.51,3s-2.53,7.95-7,10.33c-4.28,2.28-6.66.82-6.66.82Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="30.32 222.37 141.47 158 181.89 181.41 70.74 245.78 30.32 222.37"/>
<polyline class="cls-2" points="141.48 202.52 68.77 244.64 32.31 223.53 141.48 160.3"/>
<polygon class="cls-3" points="70.74 292.59 181.9 228.22 181.89 181.41 70.74 245.78 70.74 292.59"/>
<polygon class="cls-4" points="141.48 160.3 141.48 202.52 159.71 191.97 177.93 181.41 141.48 160.3"/>
</g>
<polyline class="cls-5" points="121.27 155.7 68.77 186.12 32.31 165.01 121.27 113.48"/>
<polygon class="cls-6" points="30.32 163.85 121.26 111.18 161.68 134.59 70.74 187.26 30.32 163.85"/>
<polygon class="cls-5" points="70.74 234.07 161.69 181.41 161.68 134.59 70.74 187.26 70.74 234.07"/>
<polygon class="cls-7" points="121.27 113.48 121.27 155.7 139.5 145.15 157.72 134.59 121.27 113.48"/>
<g>
<polygon class="cls-1" points="30.32 105.33 141.47 40.96 181.89 64.37 70.74 128.74 30.32 105.33"/>
<polyline class="cls-2" points="141.48 85.48 68.77 127.6 32.31 106.49 141.48 43.26"/>
<polygon class="cls-3" points="70.74 175.55 181.9 111.18 181.89 64.37 70.74 128.74 70.74 175.55"/>
<polygon class="cls-4" points="141.48 43.26 141.48 85.48 159.71 74.92 177.93 64.37 141.48 43.26"/>
</g>
<polygon class="cls-8" points="10.11 70.22 10.11 280.89 70.74 316 70.74 105.33 10.11 70.22"/>
<g>
<polygon class="cls-9" points="70.74 105.33 192 35.11 192 245.78 70.74 316 70.74 105.33"/>
<polygon class="cls-10" points="10.11 70.22 131.37 0 192 35.11 70.74 105.33 10.11 70.22"/>
</g>
<g>
<polygon class="cls-3" points="20.21 228.22 60.63 251.63 60.63 298.45 20.21 275.04 20.21 228.22"/>
<path class="cls-8" d="M33.74,265.64a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.37-4.34-5.85l-1.4-.88-.82-2.4,3.71-2.09a7.93,7.93,0,0,1,1.46-.63v-.06s-.6-.29-1.8-1l-6-3.78v-3.17l12.38,7.72V260l-5,2.65c2.8,2.15,5.54,5.86,5.54,9.38s-2.64,5-7.16,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
<polygon class="cls-3" points="20.21 111.18 60.63 134.59 60.63 181.41 20.21 158 20.21 111.18"/>
<g id="front-drawerr">
<polyline class="cls-6" points="60.63 193.11 40.42 204.82 0 181.41 20.21 169.71"/>
<polygon class="cls-11" points="0 181.41 40.42 204.82 40.42 251.63 0 228.22 0 181.41"/>
<polygon class="cls-5" points="40.42 251.63 60.63 239.93 60.63 193.11 40.42 204.82 40.42 251.63"/>
<polyline class="cls-5" points="58.65 191.96 40.42 202.52 3.97 181.41 22.19 170.85"/>
<polygon class="cls-7" points="22.19 170.85 22.19 191.96 40.42 202.52 58.65 191.96 22.19 170.85"/>
<path class="cls-12" d="M13.39,220.52c0-7.56,9.51-3.1,9.51-7.4a6,6,0,0,0-3.07-4.79c-2.36-1.36-3.65.4-3.65.4l-2.73-3.48s1.87-3.16,6.7-.43c3.56,2,6.57,6.05,6.57,10.19,0,7.05-9.08,2.71-9.18,6.68L27,227.61V231l-13.48-8.55A19,19,0,0,1,13.39,220.52Z"/>
</g>
<path class="cls-8" d="M32.29,147.62l4.4,2.95,0-11.26c0-.69,0-1.36,0-1.36l-.06,0a1.72,1.72,0,0,1-.88.55l-1.63.45L32,135.19l5.09-1.49,3.22,2.15V153l4.4,3v3.17l-12.42-8.31Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,92 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="161.68 222.37 50.52 158 10.11 181.41 121.26 245.78 161.68 222.37"/>
<polyline class="cls-2" points="50.52 202.52 123.23 244.64 159.69 223.53 50.52 160.3"/>
<polygon class="cls-3" points="121.26 292.59 10.1 228.22 10.11 181.41 121.26 245.78 121.26 292.59"/>
<polygon class="cls-4" points="50.52 160.3 50.52 202.52 32.29 191.97 14.07 181.41 50.52 160.3"/>
</g>
<polyline class="cls-5" points="70.73 155.7 123.23 186.12 159.69 165.01 70.73 113.48"/>
<polygon class="cls-6" points="161.68 163.85 70.74 111.18 30.32 134.59 121.26 187.26 161.68 163.85"/>
<polygon class="cls-5" points="121.26 234.07 30.31 181.41 30.32 134.59 121.26 187.26 121.26 234.07"/>
<polygon class="cls-7" points="70.73 113.48 70.73 155.7 52.51 145.15 34.28 134.59 70.73 113.48"/>
<g>
<polygon class="cls-1" points="161.68 105.33 50.52 40.96 10.11 64.37 121.26 128.74 161.68 105.33"/>
<polyline class="cls-2" points="50.52 85.48 123.23 127.6 159.69 106.49 50.52 43.26"/>
<polygon class="cls-3" points="121.26 175.55 10.1 111.18 10.11 64.37 121.26 128.74 121.26 175.55"/>
<polygon class="cls-4" points="50.52 43.26 50.52 85.48 32.29 74.92 14.07 64.37 50.52 43.26"/>
</g>
<polygon class="cls-8" points="181.9 70.22 181.89 280.89 121.26 316 121.26 105.33 181.9 70.22"/>
<g>
<polygon class="cls-9" points="121.26 105.33 0 35.11 0 245.78 121.26 316 121.26 105.33"/>
<polygon class="cls-10" points="181.9 70.22 60.63 0 0 35.11 121.26 105.33 181.9 70.22"/>
</g>
<g>
<polygon class="cls-3" points="171.79 228.22 131.37 251.63 131.37 298.45 171.79 275.04 171.79 228.22"/>
<path class="cls-8" d="M147.85,270a4.94,4.94,0,0,0,4.55-.34c2.09-1.11,3.6-3.19,3.58-4.92,0-2.12-2-2.1-4.29-.87l-1.38.72-.82-1.46,3.62-6.28c.78-1.34,1.41-2.27,1.41-2.27v-.06s-.58.39-1.75,1l-5.92,3.07v-3.17l12.07-6.25,0,2.29-4.81,8.22c2.75-1,5.46-.48,5.5,3s-2.52,7.95-7,10.33c-4.27,2.28-6.65.82-6.65.82Z"/>
</g>
<polygon class="cls-3" points="171.79 111.18 131.37 134.59 131.37 181.41 171.79 158 171.79 111.18"/>
<g id="front-drawerr">
<polyline class="cls-6" points="131.37 193.11 151.58 204.82 192 181.41 171.79 169.71"/>
<polygon class="cls-11" points="192 181.41 151.58 204.82 151.58 251.63 192 228.22 192 181.41"/>
<polygon class="cls-5" points="151.58 251.63 131.37 239.93 131.37 193.11 151.58 204.82 151.58 251.63"/>
<polyline class="cls-5" points="133.35 191.96 151.58 202.52 188.03 181.41 169.81 170.85"/>
<polygon class="cls-7" points="169.81 170.85 169.81 191.96 151.58 202.52 133.35 191.96 169.81 170.85"/>
<g>
<path class="cls-12" d="M165.65,229.37c0-7.33,9.32-13.48,9.29-17.47,0-1.76-1.37-2.1-3-1.21-2.32,1.22-3.57,4.28-3.57,4.28l-2.68-.41a15.13,15.13,0,0,1,6.54-7.5c3.47-1.83,6.45-1.24,6.49,2.54.07,6.43-8.86,12.32-8.93,16.19l9.37-5,0,3.13-13.34,7.21A13,13,0,0,1,165.65,229.37Z"/>
<path class="cls-8" d="M165.65,229.37c0-7.33,9.32-13.48,9.29-17.47,0-1.76-1.37-2.1-3-1.21-2.32,1.22-3.57,4.28-3.57,4.28l-2.68-.41a15.13,15.13,0,0,1,6.54-7.5c3.47-1.83,6.45-1.24,6.49,2.54.07,6.43-8.86,12.32-8.93,16.19l9.37-5,0,3.13-13.34,7.21A13,13,0,0,1,165.65,229.37Z"/>
</g>
</g>
<path class="cls-8" d="M147.65,155,152,152.5l0-11.22c0-.68,0-1.38,0-1.38l-.06,0a9.35,9.35,0,0,1-.86,1.62L149.47,144l-2.13-1.1,4.95-7.65,3.15-1.74.12,17,4.35-2.47,0,3.13-12.29,7Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1 {
fill: #4b5fef;
}
.cls-2 {
fill: #7687ff;
}
.cls-3 {
fill: #1b2559;
}
.cls-12, .cls-4 {
fill: #fff;
}
.cls-4, .cls-7, .cls-9 {
opacity: 0.74;
}
.cls-5 {
fill: #b8c2e6;
}
.cls-6 {
fill: #d2d8ff;
}
.cls-11, .cls-7 {
fill: #5a67ff;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polyline class="cls-1" points="121.27 214.22 68.77 244.64 32.31 223.53 121.27 172"/>
<polygon class="cls-2" points="30.32 222.37 121.26 169.7 161.68 193.11 70.74 245.78 30.32 222.37"/>
<polygon class="cls-1" points="70.74 292.59 161.69 239.93 161.68 193.11 70.74 245.78 70.74 292.59"/>
<polygon class="cls-3" points="121.27 172 121.27 214.22 139.5 203.67 157.72 193.11 121.27 172"/>
</g>
<g>
<polygon class="cls-4" points="30.32 163.85 141.47 99.48 181.89 122.89 70.74 187.26 30.32 163.85"/>
<polyline class="cls-5" points="141.48 144 68.77 186.12 32.31 165.01 141.48 101.78"/>
<polygon class="cls-6" points="70.74 234.07 181.9 169.7 181.89 122.89 70.74 187.26 70.74 234.07"/>
<polygon class="cls-7" points="141.48 101.78 141.48 144 159.71 133.44 177.93 122.89 141.48 101.78"/>
</g>
<g>
<polygon class="cls-4" points="30.32 105.33 141.47 40.96 181.89 64.37 70.74 128.74 30.32 105.33"/>
<polyline class="cls-5" points="141.48 85.48 68.77 127.6 32.31 106.49 141.48 43.26"/>
<polygon class="cls-6" points="70.74 175.55 181.9 111.18 181.89 64.37 70.74 128.74 70.74 175.55"/>
<polygon class="cls-7" points="141.48 43.26 141.48 85.48 159.71 74.92 177.93 64.37 141.48 43.26"/>
</g>
<polygon class="cls-8" points="10.1 70.22 10.11 280.89 70.74 316 70.74 105.33 10.1 70.22"/>
<g>
<polygon class="cls-9" points="70.74 105.33 192 35.11 192 245.78 70.74 316 70.74 105.33"/>
<polygon class="cls-10" points="10.1 70.22 131.37 0 192 35.11 70.74 105.33 10.1 70.22"/>
</g>
<g>
<polygon class="cls-6" points="20.21 169.71 60.63 193.11 60.63 239.93 20.21 216.52 20.21 169.71"/>
<path class="cls-8" d="M32.61,210.69c0-7.57,9.51-3.1,9.51-7.41a5.94,5.94,0,0,0-3.07-4.79c-2.36-1.36-3.65.41-3.65.41l-2.73-3.49s1.87-3.15,6.7-.43c3.56,2,6.57,6.06,6.57,10.2,0,7-9.08,2.71-9.18,6.67l9.5,5.92v3.44l-13.48-8.55A16.93,16.93,0,0,1,32.61,210.69Z"/>
</g>
<polygon class="cls-6" points="20.21 111.18 60.63 134.59 60.63 181.41 20.21 158 20.21 111.18"/>
<g id="front-drawerr">
<polyline class="cls-2" points="60.63 251.63 40.42 263.33 0 239.93 20.21 228.22"/>
<polygon class="cls-11" points="0 239.93 40.42 263.33 40.42 310.15 0 286.74 0 239.93"/>
<polygon class="cls-1" points="40.42 310.15 60.63 298.44 60.63 251.63 40.42 263.33 40.42 310.15"/>
<polyline class="cls-1" points="58.65 250.48 40.42 261.04 3.97 239.93 22.2 229.37"/>
<polygon class="cls-3" points="22.2 229.37 22.2 250.48 40.42 261.04 58.65 250.48 22.2 229.37"/>
<path class="cls-12" d="M14.55,279a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.38-4.34-5.86l-1.4-.87-.82-2.4L20,271.93a8,8,0,0,1,1.45-.63v-.06s-.59-.29-1.79-1l-6-3.77v-3.17L26,271v2.32L21,276c2.8,2.14,5.54,5.86,5.54,9.38s-2.63,5-7.15,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
<path class="cls-8" d="M32.29,147.62l4.4,2.95,0-11.26c0-.69,0-1.36,0-1.36l-.06,0a1.72,1.72,0,0,1-.88.55l-1.63.45L32,135.19l5.09-1.49,3.22,2.15V153l4.4,3v3.17l-12.42-8.31Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="316" viewBox="0 0 192 316">
<defs>
<style>
.cls-1 {
fill: #4b5fef;
}
.cls-2 {
fill: #7687ff;
}
.cls-3 {
fill: #1b2559;
}
.cls-12, .cls-4 {
fill: #fff;
}
.cls-4, .cls-7, .cls-9 {
opacity: 0.74;
}
.cls-5 {
fill: #b8c2e6;
}
.cls-6 {
fill: #d2d8ff;
}
.cls-11, .cls-7 {
fill: #5a67ff;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polyline class="cls-1" points="70.73 214.22 123.23 244.64 159.69 223.53 70.73 172"/>
<polygon class="cls-2" points="161.68 222.37 70.74 169.7 30.32 193.11 121.26 245.78 161.68 222.37"/>
<polygon class="cls-1" points="121.26 292.59 30.31 239.93 30.32 193.11 121.26 245.78 121.26 292.59"/>
<polygon class="cls-3" points="70.73 172 70.73 214.22 52.51 203.67 34.28 193.11 70.73 172"/>
</g>
<g>
<polygon class="cls-4" points="161.68 163.85 50.52 99.48 10.11 122.89 121.26 187.26 161.68 163.85"/>
<polyline class="cls-5" points="50.52 144 123.23 186.12 159.69 165.01 50.52 101.78"/>
<polygon class="cls-6" points="121.26 234.07 10.1 169.7 10.11 122.89 121.26 187.26 121.26 234.07"/>
<polygon class="cls-7" points="50.52 101.78 50.52 144 32.29 133.44 14.07 122.89 50.52 101.78"/>
</g>
<g>
<polygon class="cls-4" points="161.68 105.33 50.52 40.96 10.11 64.37 121.26 128.74 161.68 105.33"/>
<polyline class="cls-5" points="50.52 85.48 123.23 127.6 159.69 106.49 50.52 43.26"/>
<polygon class="cls-6" points="121.26 175.55 10.1 111.18 10.11 64.37 121.26 128.74 121.26 175.55"/>
<polygon class="cls-7" points="50.52 43.26 50.52 85.48 32.29 74.92 14.07 64.37 50.52 43.26"/>
</g>
<polygon class="cls-8" points="181.9 70.22 181.89 280.89 121.26 316 121.26 105.33 181.9 70.22"/>
<g>
<polygon class="cls-9" points="121.26 105.33 0 35.11 0 245.78 121.26 316 121.26 105.33"/>
<polygon class="cls-10" points="181.9 70.22 60.63 0 0 35.11 121.26 105.33 181.9 70.22"/>
</g>
<g>
<polygon class="cls-6" points="171.79 169.71 131.37 193.11 131.37 239.93 171.79 216.52 171.79 169.71"/>
<path class="cls-8" d="M145.58,218.78c0-7.34,9.32-13.48,9.28-17.48,0-1.75-1.36-2.09-3-1.21-2.32,1.23-3.57,4.29-3.57,4.29l-2.68-.41a15.18,15.18,0,0,1,6.54-7.51c3.47-1.83,6.44-1.23,6.49,2.55.07,6.43-8.86,12.32-8.93,16.19l9.37-5,0,3.13-13.34,7.21A13,13,0,0,1,145.58,218.78Z"/>
</g>
<polygon class="cls-6" points="171.79 111.18 131.37 134.59 131.37 181.41 171.79 158 171.79 111.18"/>
<g id="front-drawerr">
<polyline class="cls-2" points="131.37 251.63 151.58 263.33 192 239.93 171.79 228.22"/>
<polygon class="cls-11" points="192 239.93 151.58 263.33 151.58 310.15 192 286.74 192 239.93"/>
<polygon class="cls-1" points="151.58 310.15 131.37 298.44 131.37 251.63 151.58 263.33 151.58 310.15"/>
<polyline class="cls-1" points="133.35 250.48 151.58 261.04 188.03 239.93 169.81 229.37"/>
<polygon class="cls-3" points="169.81 229.37 169.81 250.48 151.58 261.04 133.35 250.48 169.81 229.37"/>
<path class="cls-12" d="M167.44,283.32A5,5,0,0,0,172,283c2.08-1.11,3.6-3.19,3.58-4.92,0-2.12-2-2.09-4.29-.87l-1.38.72-.82-1.45,3.62-6.29c.77-1.34,1.41-2.27,1.41-2.27v-.06a19.72,19.72,0,0,1-1.76,1l-5.91,3.07v-3.17l12.07-6.25,0,2.29L173.73,273c2.75-1,5.46-.48,5.5,3s-2.52,8-7,10.34c-4.27,2.27-6.65.81-6.65.81Z"/>
</g>
<path class="cls-8" d="M147.66,155,152,152.5l0-11.22c0-.68,0-1.38,0-1.38l-.06,0a9.35,9.35,0,0,1-.86,1.62L149.47,144l-2.13-1.1,5-7.65,3.14-1.74.12,17,4.35-2.47,0,3.13-12.29,7Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-10, .cls-4 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-4, .cls-8 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #ebefff;
}
.cls-9 {
fill: #1b2559;
}
.cls-10 {
fill: #ccd8ff;
}
.cls-11 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="30.32 282 141.47 217.37 181.89 240.87 70.74 305.5 30.32 282"/>
<polyline class="cls-2" points="141.48 262.07 68.77 304.35 32.31 283.16 141.48 219.68"/>
<polygon class="cls-3" points="70.74 352.5 181.9 287.87 181.89 240.87 70.74 305.5 70.74 352.5"/>
<polygon class="cls-4" points="141.48 219.68 141.48 262.07 159.71 251.47 177.93 240.87 141.48 219.68"/>
</g>
<g>
<polygon class="cls-1" points="30.32 223.25 141.47 158.63 181.89 182.13 70.74 246.75 30.32 223.25"/>
<polyline class="cls-2" points="141.48 203.32 68.77 245.6 32.31 224.41 141.48 160.93"/>
<polygon class="cls-3" points="70.74 293.75 181.9 229.13 181.89 182.13 70.74 246.75 70.74 293.75"/>
<polygon class="cls-4" points="141.48 160.93 141.48 203.32 159.71 192.72 177.93 182.13 141.48 160.93"/>
</g>
<g>
<polygon class="cls-1" points="30.32 164.5 141.47 99.88 181.89 123.38 70.74 188 30.32 164.5"/>
<polyline class="cls-2" points="141.48 144.57 68.77 186.85 32.31 165.66 141.48 102.18"/>
<polygon class="cls-3" points="70.74 235 181.9 170.38 181.89 123.38 70.74 188 70.74 235"/>
<polygon class="cls-4" points="141.48 102.18 141.48 144.57 159.7 133.97 177.93 123.38 141.48 102.18"/>
</g>
<polyline class="cls-5" points="121.27 97.57 68.77 128.1 32.31 106.91 121.27 55.18"/>
<polygon class="cls-6" points="30.32 105.75 121.26 52.87 161.68 76.38 70.73 129.25 30.32 105.75"/>
<polygon class="cls-7" points="10.11 70.5 10.11 340.75 70.75 376 70.74 105.75 10.11 70.5"/>
<polygon class="cls-3" points="20.21 170.38 60.63 193.88 60.63 240.88 20.21 217.38 20.21 170.38"/>
<polygon class="cls-8" points="0 123.38 40.42 146.88 40.42 193.88 0 170.37 0 123.38"/>
<polygon class="cls-5" points="40.42 193.88 60.63 182.13 60.63 135.13 40.42 146.88 40.42 193.88"/>
<polygon class="cls-5" points="70.74 176.25 161.68 123.38 161.68 76.38 70.74 129.25 70.74 176.25"/>
<polyline class="cls-6" points="60.63 135.13 40.42 146.88 0 123.38 20.21 111.63"/>
<polyline class="cls-5" points="58.65 133.97 40.42 144.57 3.97 123.38 22.2 112.78"/>
<polygon class="cls-9" points="22.2 112.78 22.2 133.97 40.42 144.57 58.65 133.97 22.2 112.78"/>
<polygon class="cls-9" points="121.27 55.18 121.27 97.57 139.49 86.97 157.72 76.37 121.27 55.18"/>
<g>
<polygon class="cls-10" points="70.73 105.75 192 35.25 192 305.5 70.74 376 70.73 105.75"/>
<polygon class="cls-11" points="10.1 70.5 131.37 0 192 35.25 70.73 105.75 10.1 70.5"/>
</g>
<g>
<polygon class="cls-3" points="20.21 229.12 60.63 252.62 60.63 299.62 20.21 276.12 20.21 229.12"/>
<path class="cls-7" d="M33.74,265.64a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.37-4.34-5.85l-1.4-.88-.82-2.4,3.71-2.09a7.93,7.93,0,0,1,1.46-.63v-.06s-.6-.29-1.8-1l-6-3.78v-3.17l12.38,7.72V260l-5,2.65c2.8,2.15,5.54,5.86,5.54,9.38s-2.64,5-7.16,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
<polygon class="cls-3" points="20.21 287.87 60.63 311.37 60.63 358.37 20.21 334.87 20.21 287.87"/>
<path class="cls-7" d="M41.49,327.94v-12l-4.36-2.35-8.76,8.08v2.26L37.86,329v5.23l3.63,2V331l2.63,1.42v-3.06Zm-3.62-8.25V326l-5.61-3v-.05l4.7-4a5.87,5.87,0,0,0,1-1.32l.06,0A16.49,16.49,0,0,0,37.87,319.69Z"/>
<path class="cls-7" d="M32.61,210.69c0-7.57,9.51-3.1,9.51-7.41a5.94,5.94,0,0,0-3.07-4.79c-2.36-1.36-3.65.41-3.65.41l-2.73-3.49s1.87-3.15,6.7-.43c3.56,2,6.57,6.06,6.57,10.2,0,7-9.08,2.71-9.18,6.67l9.5,5.92v3.44l-13.48-8.55A16.93,16.93,0,0,1,32.61,210.69Z"/>
<path class="cls-12" d="M13.08,161.89l4.4,2.94,0-11.26c0-.68,0-1.35,0-1.35l-.06,0a1.79,1.79,0,0,1-.88.55l-1.63.46-2.17-3.74L17.88,148l3.22,2.15v17.13l4.4,2.95v3.18l-12.42-8.31Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-10, .cls-4 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-4, .cls-8 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #ebefff;
}
.cls-9 {
fill: #1b2559;
}
.cls-10 {
fill: #ccd8ff;
}
.cls-11 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="161.68 282 50.53 217.37 10.11 240.87 121.26 305.5 161.68 282"/>
<polyline class="cls-2" points="50.52 262.07 123.23 304.35 159.69 283.16 50.52 219.68"/>
<polygon class="cls-3" points="121.26 352.5 10.1 287.87 10.11 240.87 121.26 305.5 121.26 352.5"/>
<polygon class="cls-4" points="50.52 219.68 50.52 262.07 32.3 251.47 14.07 240.87 50.52 219.68"/>
</g>
<g>
<polygon class="cls-1" points="161.68 223.25 50.53 158.63 10.11 182.13 121.26 246.75 161.68 223.25"/>
<polyline class="cls-2" points="50.52 203.32 123.23 245.6 159.69 224.41 50.52 160.93"/>
<polygon class="cls-3" points="121.26 293.75 10.1 229.13 10.11 182.13 121.26 246.75 121.26 293.75"/>
<polygon class="cls-4" points="50.52 160.93 50.52 203.32 32.3 192.72 14.07 182.13 50.52 160.93"/>
</g>
<g>
<polygon class="cls-1" points="161.68 164.5 50.53 99.88 10.11 123.38 121.26 188 161.68 164.5"/>
<polyline class="cls-2" points="50.52 144.57 123.23 186.85 159.69 165.66 50.52 102.18"/>
<polygon class="cls-3" points="121.26 235 10.1 170.38 10.11 123.38 121.26 188 121.26 235"/>
<polygon class="cls-4" points="50.52 102.18 50.52 144.57 32.3 133.97 14.07 123.38 50.52 102.18"/>
</g>
<polyline class="cls-5" points="70.73 97.57 123.23 128.1 159.69 106.91 70.73 55.18"/>
<polygon class="cls-6" points="161.68 105.75 70.74 52.87 30.32 76.38 121.27 129.25 161.68 105.75"/>
<polygon class="cls-7" points="181.89 70.5 181.89 340.75 121.25 376 121.26 105.75 181.89 70.5"/>
<polygon class="cls-3" points="171.79 170.38 131.37 193.88 131.37 240.88 171.79 217.38 171.79 170.38"/>
<polygon class="cls-8" points="192 123.38 151.58 146.88 151.58 193.88 192 170.37 192 123.38"/>
<polygon class="cls-5" points="151.58 193.88 131.37 182.13 131.37 135.13 151.58 146.88 151.58 193.88"/>
<polygon class="cls-5" points="121.26 176.25 30.32 123.38 30.32 76.38 121.26 129.25 121.26 176.25"/>
<polyline class="cls-6" points="131.37 135.13 151.58 146.88 192 123.38 171.79 111.63"/>
<polyline class="cls-5" points="133.35 133.97 151.58 144.57 188.03 123.38 169.81 112.78"/>
<polygon class="cls-9" points="169.81 112.78 169.81 133.97 151.58 144.57 133.35 133.97 169.81 112.78"/>
<polygon class="cls-9" points="70.73 55.18 70.73 97.57 52.51 86.97 34.28 76.37 70.73 55.18"/>
<g>
<polygon class="cls-10" points="121.27 105.75 0 35.25 0 305.5 121.26 376 121.27 105.75"/>
<polygon class="cls-11" points="181.9 70.5 60.63 0 0 35.25 121.27 105.75 181.9 70.5"/>
</g>
<path class="cls-12" d="M166.87,169.85l4.35-2.48-.05-11.27c0-.68,0-1.38,0-1.38l-.05,0a9.32,9.32,0,0,1-.87,1.63l-1.59,2.44-2.12-1.11,4.95-7.67,3.14-1.75.12,17.05,4.35-2.48,0,3.14-12.29,7Z"/>
<path class="cls-7" d="M145.58,219.64c0-7.36,9.32-13.53,9.29-17.54,0-1.76-1.37-2.11-3-1.22-2.32,1.23-3.56,4.3-3.56,4.3l-2.69-.41a15.23,15.23,0,0,1,6.54-7.53c3.47-1.84,6.45-1.24,6.49,2.55.07,6.46-8.86,12.38-8.93,16.26l9.37-5.07,0,3.14-13.34,7.24A13.34,13.34,0,0,1,145.58,219.64Z"/>
<g>
<polygon class="cls-3" points="171.79 229.12 131.37 252.62 131.37 299.62 171.79 276.12 171.79 229.12"/>
<path class="cls-7" d="M147.85,271.1a4.93,4.93,0,0,0,4.55-.35c2.09-1.11,3.6-3.2,3.59-4.93,0-2.13-2-2.11-4.3-.88l-1.38.73-.81-1.47,3.61-6.3c.78-1.35,1.41-2.29,1.41-2.29v-.05s-.58.39-1.75,1l-5.91,3.08v-3.19l12.07-6.27,0,2.3-4.81,8.25c2.75-1,5.46-.47,5.51,3s-2.53,8-7,10.37c-4.28,2.28-6.66.82-6.66.82Z"/>
</g>
<polygon class="cls-3" points="171.79 287.87 131.37 311.37 131.37 358.37 171.79 334.87 171.79 287.87"/>
<path class="cls-7" d="M145.25,330.84l8.51-16.71,4.26-1.91.14,11.92,2.58-1.18,0,3-2.59,1.19.06,5.2-3.6,1.66,0-5.21-9.36,4.29Zm9.34-5.07-.06-6.27a20.14,20.14,0,0,1,.13-2.17l-.06,0a20.39,20.39,0,0,1-1,2.29l-4.56,8.58v.06Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="30.32 282 141.47 217.37 181.89 240.87 70.74 305.5 30.32 282"/>
<polyline class="cls-2" points="141.48 262.06 68.77 304.35 32.31 283.16 141.48 219.68"/>
<polygon class="cls-3" points="70.74 352.5 181.9 287.87 181.89 240.87 70.74 305.5 70.74 352.5"/>
<polygon class="cls-4" points="141.48 219.68 141.48 262.06 159.71 251.47 177.93 240.87 141.48 219.68"/>
</g>
<g>
<polygon class="cls-1" points="30.32 223.25 141.47 158.63 181.89 182.13 70.74 246.75 30.32 223.25"/>
<polyline class="cls-2" points="141.48 203.32 68.77 245.6 32.31 224.41 141.48 160.93"/>
<polygon class="cls-3" points="70.74 293.75 181.9 229.13 181.89 182.13 70.74 246.75 70.74 293.75"/>
<polygon class="cls-4" points="141.48 160.93 141.48 203.32 159.71 192.72 177.93 182.13 141.48 160.93"/>
</g>
<polyline class="cls-5" points="121.27 156.31 68.77 186.85 32.31 165.66 121.27 113.93"/>
<polygon class="cls-6" points="30.32 164.5 121.26 111.62 161.68 135.12 70.74 188 30.32 164.5"/>
<polygon class="cls-5" points="70.74 235 161.69 182.12 161.68 135.12 70.74 188 70.74 235"/>
<polygon class="cls-7" points="121.27 113.93 121.27 156.31 139.5 145.72 157.72 135.12 121.27 113.93"/>
<g>
<polygon class="cls-1" points="30.32 105.75 141.47 41.12 181.89 64.62 70.74 129.25 30.32 105.75"/>
<polyline class="cls-2" points="141.48 85.81 68.77 128.1 32.31 106.91 141.48 43.43"/>
<polygon class="cls-3" points="70.74 176.25 181.9 111.62 181.89 64.62 70.74 129.25 70.74 176.25"/>
<polygon class="cls-4" points="141.48 43.43 141.48 85.81 159.71 75.22 177.93 64.62 141.48 43.43"/>
</g>
<polygon class="cls-8" points="10.11 70.5 10.12 340.75 70.75 376 70.74 105.75 10.11 70.5"/>
<g>
<polygon class="cls-9" points="70.74 105.75 192 35.25 192 305.5 70.74 376 70.74 105.75"/>
<polygon class="cls-10" points="10.11 70.5 131.37 0 192 35.25 70.74 105.75 10.11 70.5"/>
</g>
<g>
<polygon class="cls-3" points="20.21 229.12 60.63 252.62 60.63 299.62 20.21 276.12 20.21 229.12"/>
<path class="cls-8" d="M33.74,265.64a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.37-4.34-5.85l-1.4-.88-.82-2.4,3.71-2.09a7.93,7.93,0,0,1,1.46-.63v-.06s-.6-.29-1.8-1l-6-3.78v-3.17l12.38,7.72V260l-5,2.65c2.8,2.15,5.54,5.86,5.54,9.38s-2.64,5-7.16,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
<polygon class="cls-3" points="20.21 111.62 60.63 135.12 60.63 182.12 20.21 158.62 20.21 111.62"/>
<g id="front-drawerr">
<polyline class="cls-6" points="60.63 193.88 40.42 205.63 0 182.13 20.21 170.38"/>
<polygon class="cls-11" points="0 182.13 40.42 205.63 40.42 252.63 0 229.13 0 182.13"/>
<polygon class="cls-5" points="40.42 252.63 60.63 240.88 60.63 193.88 40.42 205.63 40.42 252.63"/>
<polyline class="cls-5" points="58.65 192.72 40.42 203.32 3.97 182.13 22.19 171.53"/>
<polygon class="cls-7" points="22.19 171.53 22.19 192.72 40.42 203.32 58.65 192.72 22.19 171.53"/>
<path class="cls-12" d="M13.39,220.52c0-7.56,9.51-3.1,9.51-7.4a6,6,0,0,0-3.07-4.79c-2.36-1.36-3.65.4-3.65.4l-2.73-3.48s1.87-3.16,6.7-.43c3.56,2,6.57,6.05,6.57,10.19,0,7.05-9.08,2.71-9.18,6.68L27,227.61V231l-13.48-8.55A19,19,0,0,1,13.39,220.52Z"/>
</g>
<polygon class="cls-3" points="20.21 287.87 60.64 311.37 60.64 358.37 20.21 334.87 20.21 287.87"/>
<path class="cls-8" d="M32.29,147.62l4.4,2.95,0-11.26c0-.69,0-1.36,0-1.36l-.06,0a1.72,1.72,0,0,1-.88.55l-1.63.45L32,135.19l5.09-1.49,3.22,2.15V153l4.4,3v3.17l-12.42-8.31Z"/>
<path class="cls-8" d="M41.49,327.94v-12l-4.36-2.35-8.76,8.08v2.26L37.86,329v5.23l3.63,2V331l2.63,1.42v-3.06Zm-3.62-8.25V326l-5.61-3v-.05l4.7-4a5.87,5.87,0,0,0,1-1.32l.06,0A16.49,16.49,0,0,0,37.87,319.69Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,100 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="161.68 282 50.52 217.37 10.11 240.87 121.26 305.5 161.68 282"/>
<polyline class="cls-2" points="50.52 262.06 123.23 304.35 159.69 283.16 50.52 219.68"/>
<polygon class="cls-3" points="121.26 352.5 10.1 287.87 10.11 240.87 121.26 305.5 121.26 352.5"/>
<polygon class="cls-4" points="50.52 219.68 50.52 262.06 32.29 251.47 14.07 240.87 50.52 219.68"/>
</g>
<g>
<polygon class="cls-1" points="161.68 223.25 50.52 158.63 10.11 182.13 121.26 246.75 161.68 223.25"/>
<polyline class="cls-2" points="50.52 203.32 123.23 245.6 159.69 224.41 50.52 160.93"/>
<polygon class="cls-3" points="121.26 293.75 10.1 229.13 10.11 182.13 121.26 246.75 121.26 293.75"/>
<polygon class="cls-4" points="50.52 160.93 50.52 203.32 32.29 192.72 14.07 182.13 50.52 160.93"/>
</g>
<polyline class="cls-5" points="70.73 156.31 123.23 186.85 159.69 165.66 70.73 113.93"/>
<polygon class="cls-6" points="161.68 164.5 70.74 111.62 30.32 135.12 121.26 188 161.68 164.5"/>
<polygon class="cls-5" points="121.26 235 30.31 182.12 30.32 135.12 121.26 188 121.26 235"/>
<polygon class="cls-7" points="70.73 113.93 70.73 156.31 52.51 145.72 34.28 135.12 70.73 113.93"/>
<g>
<polygon class="cls-1" points="161.68 105.75 50.52 41.12 10.11 64.62 121.26 129.25 161.68 105.75"/>
<polyline class="cls-2" points="50.52 85.81 123.23 128.1 159.69 106.91 50.52 43.43"/>
<polygon class="cls-3" points="121.26 176.25 10.1 111.62 10.11 64.62 121.26 129.25 121.26 176.25"/>
<polygon class="cls-4" points="50.52 43.43 50.52 85.81 32.29 75.22 14.07 64.62 50.52 43.43"/>
</g>
<polygon class="cls-8" points="181.89 70.5 181.88 340.75 121.25 376 121.26 105.75 181.89 70.5"/>
<g>
<polygon class="cls-9" points="121.26 105.75 0 35.25 0 305.5 121.26 376 121.26 105.75"/>
<polygon class="cls-10" points="181.9 70.5 60.63 0 0 35.25 121.26 105.75 181.9 70.5"/>
</g>
<g>
<polygon class="cls-3" points="171.79 229.12 131.37 252.62 131.37 299.62 171.79 276.12 171.79 229.12"/>
<path class="cls-8" d="M147.85,271.1a4.92,4.92,0,0,0,4.55-.35c2.09-1.11,3.6-3.2,3.58-4.93,0-2.13-2-2.11-4.29-.88l-1.38.73-.82-1.47,3.62-6.3c.78-1.35,1.41-2.29,1.41-2.29v-.05s-.58.39-1.75,1l-5.92,3.08v-3.19l12.07-6.27,0,2.3-4.81,8.25c2.75-1,5.46-.47,5.5,3s-2.52,8-7,10.37c-4.27,2.28-6.65.82-6.65.82Z"/>
</g>
<polygon class="cls-3" points="171.79 111.62 131.37 135.12 131.37 182.12 171.79 158.62 171.79 111.62"/>
<g id="front-drawerr">
<polyline class="cls-6" points="131.37 193.88 151.58 205.63 192 182.13 171.79 170.38"/>
<polygon class="cls-11" points="192 182.13 151.58 205.63 151.58 252.63 192 229.13 192 182.13"/>
<polygon class="cls-5" points="151.58 252.63 131.37 240.88 131.37 193.88 151.58 205.63 151.58 252.63"/>
<polyline class="cls-5" points="133.35 192.72 151.58 203.32 188.03 182.13 169.81 171.53"/>
<polygon class="cls-7" points="169.81 171.53 169.81 192.72 151.58 203.32 133.35 192.72 169.81 171.53"/>
<g>
<path class="cls-12" d="M165.65,230.28c0-7.37,9.32-13.53,9.29-17.55,0-1.76-1.37-2.1-3-1.21-2.32,1.23-3.57,4.3-3.57,4.3l-2.68-.41a15.23,15.23,0,0,1,6.54-7.54c3.47-1.83,6.45-1.24,6.49,2.56.07,6.45-8.86,12.37-8.93,16.25l9.37-5.07,0,3.15L165.82,232A13.34,13.34,0,0,1,165.65,230.28Z"/>
<path class="cls-8" d="M165.65,230.28c0-7.37,9.32-13.53,9.29-17.55,0-1.76-1.37-2.1-3-1.21-2.32,1.23-3.57,4.3-3.57,4.3l-2.68-.41a15.23,15.23,0,0,1,6.54-7.54c3.47-1.83,6.45-1.24,6.49,2.56.07,6.45-8.86,12.37-8.93,16.25l9.37-5.07,0,3.15L165.82,232A13.34,13.34,0,0,1,165.65,230.28Z"/>
</g>
</g>
<path class="cls-8" d="M147.65,155.58,152,153.1l0-11.26c0-.69,0-1.39,0-1.39l-.06,0a9.6,9.6,0,0,1-.86,1.63l-1.59,2.45-2.13-1.11,4.95-7.67,3.15-1.75.12,17.05,4.35-2.48,0,3.14-12.29,7Z"/>
<polygon class="cls-3" points="171.78 287.87 131.36 311.37 131.36 358.37 171.78 334.87 171.78 287.87"/>
<path class="cls-8" d="M145.25,330.84l8.51-16.71,4.25-1.91.14,11.92,2.59-1.18,0,3-2.59,1.19.06,5.19-3.6,1.66,0-5.21-9.36,4.29Zm9.33-5.07-.05-6.27a21.54,21.54,0,0,1,.12-2.17l-.05,0a19.36,19.36,0,0,1-1,2.29l-4.56,8.58v.06Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="30.33 282 141.48 217.38 181.89 240.88 70.74 305.5 30.33 282"/>
<polyline class="cls-2" points="141.48 262.07 68.77 304.35 32.32 283.16 141.48 219.68"/>
<polygon class="cls-3" points="70.74 352.5 181.9 287.88 181.89 240.88 70.74 305.5 70.74 352.5"/>
<polygon class="cls-4" points="141.48 219.68 141.48 262.07 159.71 251.47 177.93 240.88 141.48 219.68"/>
</g>
<g>
<polyline class="cls-5" points="121.26 215.07 68.77 245.6 32.31 224.41 121.26 172.68"/>
<polygon class="cls-6" points="30.32 223.25 121.26 170.37 161.68 193.88 70.73 246.75 30.32 223.25"/>
<polygon class="cls-5" points="70.74 293.75 161.68 240.88 161.68 193.88 70.74 246.75 70.74 293.75"/>
<polygon class="cls-7" points="121.26 172.68 121.26 215.07 139.49 204.47 157.72 193.87 121.26 172.68"/>
</g>
<g>
<polygon class="cls-1" points="30.32 164.5 141.47 99.87 181.89 123.37 70.73 188 30.32 164.5"/>
<polyline class="cls-2" points="141.47 144.56 68.77 186.85 32.31 165.66 141.47 102.18"/>
<polygon class="cls-3" points="70.74 235 181.89 170.37 181.89 123.37 70.74 188 70.74 235"/>
<polygon class="cls-4" points="141.47 102.18 141.47 144.56 159.7 133.97 177.93 123.37 141.47 102.18"/>
</g>
<g>
<polygon class="cls-1" points="30.32 105.75 141.47 41.12 181.89 64.62 70.73 129.25 30.32 105.75"/>
<polyline class="cls-2" points="141.47 85.81 68.77 128.1 32.31 106.91 141.47 43.43"/>
<polygon class="cls-3" points="70.74 176.25 181.89 111.62 181.89 64.62 70.74 129.25 70.74 176.25"/>
<polygon class="cls-4" points="141.47 43.43 141.47 85.81 159.7 75.22 177.93 64.62 141.47 43.43"/>
</g>
<polygon class="cls-8" points="10.1 70.5 10.11 340.75 70.74 376 70.73 105.75 10.1 70.5"/>
<polygon class="cls-3" points="20.21 170.38 60.63 193.88 60.63 240.88 20.21 217.38 20.21 170.38"/>
<polygon class="cls-3" points="20.21 111.62 60.63 135.12 60.63 182.12 20.21 158.62 20.21 111.62"/>
<g>
<polygon class="cls-9" points="70.74 105.75 192 35.25 192 305.5 70.74 376 70.74 105.75"/>
<polygon class="cls-10" points="10.11 70.5 131.37 0 192 35.25 70.74 105.75 10.11 70.5"/>
</g>
<polygon class="cls-3" points="20.22 287.88 60.64 311.38 60.64 358.38 20.22 334.88 20.22 287.88"/>
<path class="cls-8" d="M41.49,327.94v-12l-4.36-2.35-8.76,8.08v2.26L37.86,329v5.23l3.63,2V331l2.63,1.42v-3.06Zm-3.62-8.25V326l-5.61-3v-.05l4.7-4a5.87,5.87,0,0,0,1-1.32l.06,0A16.49,16.49,0,0,0,37.87,319.69Z"/>
<g id="front-drawerr">
<polyline class="cls-6" points="60.63 252.62 40.42 264.37 0 240.87 20.21 229.12"/>
<polygon class="cls-11" points="0 240.87 40.42 264.37 40.42 311.37 0 287.87 0 240.87"/>
<polygon class="cls-5" points="40.42 311.37 60.63 299.62 60.63 252.62 40.42 264.37 40.42 311.37"/>
<polyline class="cls-5" points="58.65 251.47 40.42 262.07 3.97 240.87 22.19 230.28"/>
<polygon class="cls-7" points="22.19 230.28 22.19 251.47 40.42 262.07 58.65 251.47 22.19 230.28"/>
</g>
<path class="cls-8" d="M32.29,147.62l4.4,2.95,0-11.26c0-.69,0-1.36,0-1.36l-.06,0a1.72,1.72,0,0,1-.88.55l-1.63.45L32,135.19l5.09-1.49,3.22,2.15V153l4.4,3v3.17l-12.42-8.31Z"/>
<path class="cls-12" d="M14.55,279a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.38-4.34-5.86l-1.4-.87-.82-2.4L20,271.93a8,8,0,0,1,1.45-.63v-.06s-.59-.29-1.79-1l-6-3.77v-3.17L26,271v2.32L21,276c2.8,2.14,5.54,5.86,5.54,9.38s-2.63,5-7.15,2.2a22,22,0,0,1-6.72-6.91Z"/>
<path class="cls-8" d="M32.61,210.69c0-7.57,9.51-3.1,9.51-7.41a5.94,5.94,0,0,0-3.07-4.79c-2.36-1.36-3.65.41-3.65.41l-2.73-3.49s1.87-3.15,6.7-.43c3.56,2,6.57,6.06,6.57,10.2,0,7-9.08,2.71-9.18,6.67l9.5,5.92v3.44l-13.48-8.55A16.93,16.93,0,0,1,32.61,210.69Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,99 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376" viewBox="0 0 192 376">
<defs>
<style>
.cls-1, .cls-12 {
fill: #fff;
}
.cls-1, .cls-4, .cls-9 {
opacity: 0.74;
}
.cls-2 {
fill: #b8c2e6;
}
.cls-3 {
fill: #d2d8ff;
}
.cls-11, .cls-4 {
fill: #5a67ff;
}
.cls-5 {
fill: #4b5fef;
}
.cls-6 {
fill: #7687ff;
}
.cls-7 {
fill: #1b2559;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polygon class="cls-1" points="161.67 282 50.52 217.38 10.11 240.88 121.26 305.5 161.67 282"/>
<polyline class="cls-2" points="50.52 262.07 123.23 304.35 159.68 283.16 50.52 219.68"/>
<polygon class="cls-3" points="121.26 352.5 10.1 287.88 10.11 240.88 121.26 305.5 121.26 352.5"/>
<polygon class="cls-4" points="50.52 219.68 50.52 262.07 32.29 251.47 14.07 240.88 50.52 219.68"/>
</g>
<g>
<polyline class="cls-5" points="70.74 215.07 123.23 245.6 159.69 224.41 70.74 172.68"/>
<polygon class="cls-6" points="161.68 223.25 70.74 170.37 30.32 193.88 121.27 246.75 161.68 223.25"/>
<polygon class="cls-5" points="121.26 293.75 30.32 240.88 30.32 193.88 121.26 246.75 121.26 293.75"/>
<polygon class="cls-7" points="70.74 172.68 70.74 215.07 52.51 204.47 34.28 193.87 70.74 172.68"/>
</g>
<g>
<polygon class="cls-1" points="161.68 164.5 50.53 99.87 10.11 123.37 121.27 188 161.68 164.5"/>
<polyline class="cls-2" points="50.53 144.56 123.23 186.85 159.69 165.66 50.53 102.18"/>
<polygon class="cls-3" points="121.26 235 10.11 170.37 10.11 123.37 121.26 188 121.26 235"/>
<polygon class="cls-4" points="50.53 102.18 50.53 144.56 32.3 133.97 14.07 123.37 50.53 102.18"/>
</g>
<g>
<polygon class="cls-1" points="161.68 105.75 50.53 41.12 10.11 64.62 121.27 129.25 161.68 105.75"/>
<polyline class="cls-2" points="50.53 85.81 123.23 128.1 159.69 106.91 50.53 43.43"/>
<polygon class="cls-3" points="121.26 176.25 10.11 111.62 10.11 64.62 121.26 129.25 121.26 176.25"/>
<polygon class="cls-4" points="50.53 43.43 50.53 85.81 32.3 75.22 14.07 64.62 50.53 43.43"/>
</g>
<polygon class="cls-8" points="181.9 70.5 181.89 340.75 121.26 376 121.27 105.75 181.9 70.5"/>
<g>
<polygon class="cls-3" points="171.79 170.38 131.37 193.88 131.37 240.88 171.79 217.38 171.79 170.38"/>
<path class="cls-8" d="M145.58,219.64c0-7.36,9.32-13.53,9.29-17.54,0-1.76-1.37-2.11-3-1.22-2.31,1.23-3.56,4.31-3.56,4.31l-2.69-.42a15.23,15.23,0,0,1,6.54-7.53c3.47-1.84,6.45-1.24,6.49,2.55.07,6.46-8.86,12.38-8.93,16.26l9.37-5.07,0,3.14-13.34,7.24A13.17,13.17,0,0,1,145.58,219.64Z"/>
</g>
<polygon class="cls-3" points="171.79 111.62 131.37 135.12 131.37 182.12 171.79 158.62 171.79 111.62"/>
<path class="cls-8" d="M147.66,155.58,152,153.1l0-11.26c0-.69,0-1.39,0-1.39l0,0a10.31,10.31,0,0,1-.87,1.63l-1.59,2.45-2.12-1.11,5-7.67,3.14-1.75.12,17.05,4.35-2.48,0,3.14-12.29,7Z"/>
<g>
<polygon class="cls-9" points="121.26 105.75 0 35.25 0 305.5 121.26 376 121.26 105.75"/>
<polygon class="cls-10" points="181.89 70.5 60.63 0 0 35.25 121.26 105.75 181.89 70.5"/>
</g>
<polygon class="cls-3" points="171.78 287.88 131.36 311.38 131.36 358.38 171.78 334.88 171.78 287.88"/>
<path class="cls-8" d="M145.25,330.84l8.5-16.71,4.26-1.91.14,11.92,2.58-1.18.05,3-2.6,1.19.06,5.19L154.65,334l-.05-5.21-9.35,4.29Zm9.33-5.07-.05-6.26a21.61,21.61,0,0,1,.12-2.18l-.06,0a20.39,20.39,0,0,1-1,2.29l-4.56,8.58v.06Z"/>
<g id="front-drawerr">
<polyline class="cls-6" points="131.37 252.62 151.58 264.37 192 240.87 171.79 229.12"/>
<polygon class="cls-11" points="192 240.87 151.58 264.37 151.58 311.37 192 287.87 192 240.87"/>
<polygon class="cls-5" points="151.58 311.37 131.37 299.62 131.37 252.62 151.58 264.37 151.58 311.37"/>
<polyline class="cls-5" points="133.35 251.47 151.58 262.07 188.03 240.87 169.81 230.28"/>
<polygon class="cls-7" points="169.81 230.28 169.81 251.47 151.58 262.07 133.35 251.47 169.81 230.28"/>
<path class="cls-12" d="M167.44,284.44a4.92,4.92,0,0,0,4.55-.35c2.09-1.11,3.6-3.2,3.58-4.94,0-2.13-2-2.1-4.29-.87l-1.38.72-.82-1.46,3.62-6.31c.77-1.35,1.41-2.28,1.41-2.28v-.06s-.58.39-1.75,1L166.44,273v-3.19l12.07-6.27,0,2.29-4.81,8.26c2.75-1.05,5.46-.48,5.5,3s-2.52,8-7,10.38c-4.27,2.28-6.65.82-6.65.82Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376.47" viewBox="0 0 192 376.47">
<defs>
<style>
.cls-1 {
fill: #4b5fef;
}
.cls-2 {
fill: #7687ff;
}
.cls-3 {
fill: #1b2559;
}
.cls-12, .cls-4 {
fill: #fff;
}
.cls-4, .cls-7, .cls-9 {
opacity: 0.74;
}
.cls-5 {
fill: #b8c2e6;
}
.cls-6 {
fill: #d2d8ff;
}
.cls-11, .cls-7 {
fill: #5a67ff;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polyline class="cls-1" points="121.26 274.16 68.77 304.73 32.31 283.51 121.26 231.72"/>
<polygon class="cls-2" points="30.32 282.35 121.26 229.41 161.68 252.94 70.73 305.88 30.32 282.35"/>
<polygon class="cls-1" points="70.74 352.94 161.68 300 161.68 252.94 70.74 305.88 70.74 352.94"/>
<polygon class="cls-3" points="121.26 231.72 121.26 274.16 139.49 263.55 157.72 252.94 121.26 231.72"/>
</g>
<g>
<polygon class="cls-4" points="30.33 223.53 141.48 158.82 181.89 182.35 70.74 247.06 30.33 223.53"/>
<polyline class="cls-5" points="141.48 203.57 68.77 245.91 32.32 224.69 141.48 161.13"/>
<polygon class="cls-6" points="70.74 294.12 181.9 229.41 181.89 182.35 70.74 247.06 70.74 294.12"/>
<polygon class="cls-7" points="141.48 161.13 141.48 203.57 159.71 192.96 177.93 182.35 141.48 161.13"/>
</g>
<g>
<polygon class="cls-4" points="30.32 164.7 141.47 100 181.89 123.53 70.73 188.23 30.32 164.7"/>
<polyline class="cls-5" points="141.47 144.74 68.77 187.08 32.31 165.86 141.47 102.31"/>
<polygon class="cls-6" points="70.74 235.29 181.89 170.59 181.89 123.53 70.74 188.23 70.74 235.29"/>
<polygon class="cls-7" points="141.47 102.31 141.47 144.74 159.7 134.14 177.93 123.53 141.47 102.31"/>
</g>
<g>
<polygon class="cls-4" points="30.32 105.88 141.47 41.17 181.89 64.7 70.73 129.41 30.32 105.88"/>
<polyline class="cls-5" points="141.47 85.92 68.77 128.26 32.31 107.04 141.47 43.48"/>
<polygon class="cls-6" points="70.74 176.47 181.89 111.76 181.89 64.7 70.74 129.41 70.74 176.47"/>
<polygon class="cls-7" points="141.47 43.48 141.47 85.92 159.7 75.31 177.93 64.7 141.47 43.48"/>
</g>
<polygon class="cls-8" points="10.1 70.58 10.11 341.17 70.74 376.47 70.73 105.88 10.1 70.58"/>
<polygon class="cls-6" points="20.21 170.59 60.63 194.12 60.63 241.18 20.21 217.65 20.21 170.59"/>
<polygon class="cls-6" points="20.21 111.76 60.63 135.29 60.63 182.35 20.21 158.82 20.21 111.76"/>
<g>
<polygon class="cls-9" points="70.74 105.88 192 35.29 192 305.88 70.74 376.47 70.74 105.88"/>
<polygon class="cls-10" points="10.11 70.59 131.37 0 192 35.29 70.74 105.88 10.11 70.59"/>
</g>
<polygon class="cls-6" points="20.22 288.23 60.64 311.76 60.64 358.82 20.22 335.29 20.22 288.23"/>
<path class="cls-8" d="M46.75,333.53l-9.35-4.3,0,5.22-3.59-1.67.06-5.2-2.6-1.19.05-3,2.58,1.18L34,312.61l4.26,1.91,8.5,16.73Zm-3.82-4.83v-.06l-4.56-8.59a20.17,20.17,0,0,1-1-2.3l-.06,0a21.54,21.54,0,0,1,.12,2.17l0,6.28Z"/>
<g id="front-drawerr">
<polyline class="cls-2" points="60.63 311.76 40.42 323.53 0 300 20.21 288.23"/>
<polygon class="cls-11" points="0 300 40.42 323.53 40.42 370.59 0 347.06 0 300"/>
<polygon class="cls-1" points="40.42 370.59 60.63 358.82 60.63 311.76 40.42 323.53 40.42 370.59"/>
<polyline class="cls-1" points="58.65 310.61 40.42 321.22 3.97 300 22.19 289.39"/>
<polygon class="cls-3" points="22.19 289.39 22.19 310.61 40.42 321.22 58.65 310.61 22.19 289.39"/>
<path class="cls-12" d="M23.23,339.27v-12l-4.36-2.36L10.11,333v2.26l9.5,5.12v5.24l3.63,2v-5.24l2.63,1.43v-3.07ZM19.61,331v6.29l-5.6-3v-.06l4.69-4a5.54,5.54,0,0,0,1-1.32l.06,0A17.65,17.65,0,0,0,19.61,331Z"/>
</g>
<g>
<polygon class="cls-6" points="20.22 229.41 60.64 252.94 60.64 300 20.22 276.47 20.22 229.41"/>
<path class="cls-8" d="M33.74,265.64a16.52,16.52,0,0,0,4.6,4.93c2.12,1.32,3.66,1,3.66-.75,0-2.14-2-4.37-4.34-5.85l-1.4-.88-.82-2.4,3.71-2.09a7.93,7.93,0,0,1,1.46-.63v-.06s-.6-.29-1.8-1l-6-3.78v-3.17l12.38,7.72V260l-5,2.65c2.8,2.15,5.54,5.86,5.54,9.38s-2.64,5-7.16,2.2a22,22,0,0,1-6.72-6.91Z"/>
</g>
<path class="cls-8" d="M32.29,147.62l4.4,2.95,0-11.26c0-.69,0-1.36,0-1.36l-.06,0a1.72,1.72,0,0,1-.88.55l-1.63.45L32,135.19l5.09-1.49,3.22,2.15V153l4.4,3v3.17l-12.42-8.31Z"/>
<path class="cls-8" d="M32.61,210.69c0-7.57,9.51-3.1,9.51-7.41a5.94,5.94,0,0,0-3.07-4.79c-2.36-1.36-3.65.41-3.65.41l-2.73-3.49s1.87-3.15,6.7-.43c3.56,2,6.57,6.06,6.57,10.2,0,7-9.08,2.71-9.18,6.67l9.5,5.92v3.44l-13.48-8.55A16.93,16.93,0,0,1,32.61,210.69Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,103 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="376.47" viewBox="0 0 192 376.47">
<defs>
<style>
.cls-1 {
fill: #4b5fef;
}
.cls-2 {
fill: #7687ff;
}
.cls-3 {
fill: #1b2559;
}
.cls-12, .cls-4 {
fill: #fff;
}
.cls-4, .cls-7, .cls-9 {
opacity: 0.74;
}
.cls-5 {
fill: #b8c2e6;
}
.cls-6 {
fill: #d2d8ff;
}
.cls-11, .cls-7 {
fill: #5a67ff;
}
.cls-8 {
fill: #ebefff;
}
.cls-9 {
fill: #ccd8ff;
}
.cls-10 {
fill: #dee5fc;
opacity: 0.8;
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<g>
<polyline class="cls-1" points="70.74 274.16 123.23 304.73 159.69 283.51 70.74 231.72"/>
<polygon class="cls-2" points="161.68 282.35 70.74 229.41 30.32 252.94 121.27 305.88 161.68 282.35"/>
<polygon class="cls-1" points="121.26 352.94 30.32 300 30.32 252.94 121.26 305.88 121.26 352.94"/>
<polygon class="cls-3" points="70.74 231.72 70.74 274.16 52.51 263.55 34.28 252.94 70.74 231.72"/>
</g>
<g>
<polygon class="cls-4" points="161.67 223.53 50.52 158.82 10.11 182.35 121.26 247.06 161.67 223.53"/>
<polyline class="cls-5" points="50.52 203.57 123.23 245.91 159.68 224.69 50.52 161.13"/>
<polygon class="cls-6" points="121.26 294.12 10.1 229.41 10.11 182.35 121.26 247.06 121.26 294.12"/>
<polygon class="cls-7" points="50.52 161.13 50.52 203.57 32.29 192.96 14.07 182.35 50.52 161.13"/>
</g>
<g>
<polygon class="cls-4" points="161.68 164.7 50.53 100 10.11 123.53 121.27 188.23 161.68 164.7"/>
<polyline class="cls-5" points="50.53 144.74 123.23 187.08 159.69 165.86 50.53 102.31"/>
<polygon class="cls-6" points="121.26 235.29 10.11 170.59 10.11 123.53 121.26 188.23 121.26 235.29"/>
<polygon class="cls-7" points="50.53 102.31 50.53 144.74 32.3 134.14 14.07 123.53 50.53 102.31"/>
</g>
<g>
<polygon class="cls-4" points="161.68 105.88 50.53 41.17 10.11 64.7 121.27 129.41 161.68 105.88"/>
<polyline class="cls-5" points="50.53 85.92 123.23 128.26 159.69 107.04 50.53 43.48"/>
<polygon class="cls-6" points="121.26 176.47 10.11 111.76 10.11 64.7 121.26 129.41 121.26 176.47"/>
<polygon class="cls-7" points="50.53 43.48 50.53 85.92 32.3 75.31 14.07 64.7 50.53 43.48"/>
</g>
<polygon class="cls-8" points="181.9 70.58 181.89 341.17 121.26 376.47 121.27 105.88 181.9 70.58"/>
<g>
<polygon class="cls-6" points="171.79 170.59 131.37 194.12 131.37 241.18 171.79 217.65 171.79 170.59"/>
<path class="cls-8" d="M145.58,219.91c0-7.37,9.32-13.54,9.29-17.56,0-1.77-1.37-2.11-3-1.22-2.31,1.23-3.56,4.31-3.56,4.31l-2.69-.41a15.23,15.23,0,0,1,6.54-7.55c3.47-1.83,6.45-1.24,6.49,2.56.07,6.46-8.86,12.39-8.93,16.28l9.37-5.08,0,3.15-13.34,7.25A13.31,13.31,0,0,1,145.58,219.91Z"/>
</g>
<polygon class="cls-6" points="171.79 111.76 131.37 135.29 131.37 182.35 171.79 158.82 171.79 111.76"/>
<path class="cls-8" d="M147.66,155.77l4.35-2.48L152,142c0-.69,0-1.39,0-1.39l0,0a10.31,10.31,0,0,1-.87,1.63l-1.59,2.45-2.12-1.11,5-7.68,3.14-1.76.12,17.07,4.35-2.48,0,3.15-12.29,7Z"/>
<g>
<polygon class="cls-9" points="121.26 105.88 0 35.29 0 305.88 121.26 376.47 121.26 105.88"/>
<polygon class="cls-10" points="181.89 70.59 60.63 0 0 35.29 121.26 105.88 181.89 70.59"/>
</g>
<polygon class="cls-6" points="171.78 288.23 131.36 311.76 131.36 358.82 171.78 335.29 171.78 288.23"/>
<path class="cls-8" d="M145.25,331.25l8.5-16.73,4.26-1.91.14,11.93,2.58-1.18.05,3-2.6,1.19.06,5.2-3.59,1.67-.05-5.22-9.35,4.3Zm9.33-5.07-.05-6.28a21.54,21.54,0,0,1,.12-2.17l-.06,0a20.17,20.17,0,0,1-1,2.3l-4.56,8.59v.06Z"/>
<g id="front-drawerr">
<polyline class="cls-2" points="131.37 311.76 151.58 323.53 192 300 171.79 288.23"/>
<polygon class="cls-11" points="192 300 151.58 323.53 151.58 370.59 192 347.06 192 300"/>
<polygon class="cls-1" points="151.58 370.59 131.37 358.82 131.37 311.76 151.58 323.53 151.58 370.59"/>
<polyline class="cls-1" points="133.35 310.61 151.58 321.22 188.03 300 169.81 289.39"/>
<polygon class="cls-3" points="169.81 289.39 169.81 310.61 151.58 321.22 133.35 310.61 169.81 289.39"/>
<path class="cls-12" d="M165.58,341.32l8.51-16.73,4.25-1.92.14,11.94,2.59-1.18,0,3-2.59,1.19.06,5.2-3.6,1.66,0-5.22-9.36,4.3Zm9.33-5.08,0-6.27a21.61,21.61,0,0,1,.12-2.18l-.06,0a18.3,18.3,0,0,1-1,2.3l-4.56,8.59v.05Z"/>
</g>
<g>
<polygon class="cls-6" points="171.78 229.41 131.36 252.94 131.36 300 171.78 276.47 171.78 229.41"/>
<path class="cls-8" d="M147.85,271.44a4.92,4.92,0,0,0,4.55-.35c2.08-1.11,3.6-3.2,3.58-4.94,0-2.14-2-2.11-4.29-.88l-1.38.73-.82-1.47,3.62-6.31c.77-1.35,1.41-2.29,1.41-2.29v-.05s-.59.39-1.76,1L146.85,260v-3.19l12.07-6.28,0,2.29-4.8,8.27c2.75-1.05,5.46-.48,5.5,3s-2.52,8-7,10.38c-4.27,2.29-6.65.83-6.65.83Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5 KiB

6
package-lock.json generated
View file

@ -14459,6 +14459,7 @@
"from": "git+https://github.com/lamassu/lamassu-coins.git", "from": "git+https://github.com/lamassu/lamassu-coins.git",
"requires": { "requires": {
"bech32": "2.0.0", "bech32": "2.0.0",
"big-integer": "^1.6.48",
"bignumber.js": "^9.0.0", "bignumber.js": "^9.0.0",
"bitcoinjs-lib": "4.0.3", "bitcoinjs-lib": "4.0.3",
"bs58check": "^2.0.2", "bs58check": "^2.0.2",
@ -14474,6 +14475,11 @@
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
}, },
"big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
},
"bip32": { "bip32": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/bip32/-/bip32-1.0.4.tgz", "resolved": "https://registry.npmjs.org/bip32/-/bip32-1.0.4.tgz",

View file

@ -1,13 +1,20 @@
{ {
"files": { "files": {
"main.js": "/static/js/main.ff8544f4.chunk.js", "main.js": "/static/js/main.144ef1be.chunk.js",
"main.js.map": "/static/js/main.ff8544f4.chunk.js.map", "main.js.map": "/static/js/main.144ef1be.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.5b925903.js", "runtime-main.js": "/static/js/runtime-main.5b925903.js",
"runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map", "runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map",
"static/js/2.cd718274.chunk.js": "/static/js/2.cd718274.chunk.js", "static/js/2.e38b81ec.chunk.js": "/static/js/2.e38b81ec.chunk.js",
"static/js/2.cd718274.chunk.js.map": "/static/js/2.cd718274.chunk.js.map", "static/js/2.e38b81ec.chunk.js.map": "/static/js/2.e38b81ec.chunk.js.map",
"index.html": "/index.html", "index.html": "/index.html",
"static/js/2.cd718274.chunk.js.LICENSE.txt": "/static/js/2.cd718274.chunk.js.LICENSE.txt", "static/js/2.e38b81ec.chunk.js.LICENSE.txt": "/static/js/2.e38b81ec.chunk.js.LICENSE.txt",
"static/media/3-cassettes-open-1-left.d6d9aa73.svg": "/static/media/3-cassettes-open-1-left.d6d9aa73.svg",
"static/media/3-cassettes-open-2-left.a9ee8d4c.svg": "/static/media/3-cassettes-open-2-left.a9ee8d4c.svg",
"static/media/3-cassettes-open-3-left.08fed660.svg": "/static/media/3-cassettes-open-3-left.08fed660.svg",
"static/media/4-cassettes-open-1-left.7b00c51f.svg": "/static/media/4-cassettes-open-1-left.7b00c51f.svg",
"static/media/4-cassettes-open-2-left.b3d9541c.svg": "/static/media/4-cassettes-open-2-left.b3d9541c.svg",
"static/media/4-cassettes-open-3-left.e8f1667c.svg": "/static/media/4-cassettes-open-3-left.e8f1667c.svg",
"static/media/4-cassettes-open-4-left.bc1a9829.svg": "/static/media/4-cassettes-open-4-left.bc1a9829.svg",
"static/media/acceptor-left.f37bcb1a.svg": "/static/media/acceptor-left.f37bcb1a.svg", "static/media/acceptor-left.f37bcb1a.svg": "/static/media/acceptor-left.f37bcb1a.svg",
"static/media/both-filled.7af80d5f.svg": "/static/media/both-filled.7af80d5f.svg", "static/media/both-filled.7af80d5f.svg": "/static/media/both-filled.7af80d5f.svg",
"static/media/carousel-left-arrow.04e38344.svg": "/static/media/carousel-left-arrow.04e38344.svg", "static/media/carousel-left-arrow.04e38344.svg": "/static/media/carousel-left-arrow.04e38344.svg",
@ -125,7 +132,7 @@
}, },
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.5b925903.js", "static/js/runtime-main.5b925903.js",
"static/js/2.cd718274.chunk.js", "static/js/2.e38b81ec.chunk.js",
"static/js/main.ff8544f4.chunk.js" "static/js/main.144ef1be.chunk.js"
] ]
} }

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="robots" content="noindex"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Lamassu Admin</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="root"></div><script>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.cd718274.chunk.js"></script><script src="/static/js/main.ff8544f4.chunk.js"></script></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="robots" content="noindex"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Lamassu Admin</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="root"></div><script>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.e38b81ec.chunk.js"></script><script src="/static/js/main.144ef1be.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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