Merge pull request #933 from SiIky/feat/iCfPSCAQ/cashbox-history-backport

feat: cashbox history backport
This commit is contained in:
Rafael Taranto 2021-11-23 09:08:27 +00:00 committed by GitHub
commit 60e939a4b9
16 changed files with 952 additions and 23 deletions

37
lib/cashbox-batches.js Normal file
View file

@ -0,0 +1,37 @@
const db = require('./db')
const _ = require('lodash/fp')
const uuid = require('uuid')
function createCashboxBatch (deviceId, cashboxCount) {
if (_.isEqual(0, cashboxCount)) throw new Error('Cashbox is empty. Cashbox batch could not be created.')
const sql = `INSERT INTO cashbox_batches (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-in-empty') RETURNING *`
const sql2 = `
UPDATE bills SET cashbox_batch_id=$1
FROM cash_in_txs
WHERE bills.cash_in_txs_id = cash_in_txs.id AND
cash_in_txs.device_id = $2 AND
bills.cashbox_batch_id IS NULL
`
return db.tx(async t => {
const newBatch = await t.oneOrNone(sql, [uuid.v4(), deviceId])
return t.oneOrNone(sql2, [newBatch.id, newBatch.device_id])
})
}
function getBatches () {
const sql = `SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by,
json_agg(b.*) AS bills FROM cashbox_batches cb LEFT JOIN bills b ON cb.id=b.cashbox_batch_id GROUP BY cb.id`
return db.any(sql).then(res => _.map(it => _.mapKeys(ite => _.camelCase(ite), it), res))
}
function editBatchById (id, performedBy) {
const sql = `UPDATE cashbox_batches SET performed_by=$1 WHERE id=$2`
return db.none(sql, [performedBy, id])
}
function getBillsByBatchId (id) {
const sql = `SELECT * FROM bills WHERE cashbox_batch_id=$1`
return db.any(sql, [id])
}
module.exports = { createCashboxBatch, getBatches, getBillsByBatchId, editBatchById }

View file

@ -118,7 +118,7 @@ function getMachine (machineId, config) {
.then(([machine, events, config]) => {
const pings = checkPings([machine])
return [machine].map(addName(pings, events, config))[0]
return addName(pings, events, config)(machine)
})
}

View file

@ -5,6 +5,7 @@ const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
const got = require('got')
const DataLoader = require('dataloader')
const cashbox = require('../../cashbox-batches')
const machineLoader = require('../../machine-loader')
const customers = require('../../customers')
const { machineAction } = require('../machines')
@ -273,14 +274,25 @@ const typeDefs = gql`
valid: Boolean
}
type Bills {
type Bill {
fiat: Int
deviceId: ID
created: Date
cashbox: Int
}
type CashboxBatch {
id: ID
deviceId: ID
created: Date
operationType: String
customBillCount: Int
performedBy: String
bills: [Bill]
}
type Query {
cashboxBatches: [CashboxBatch]
countries: [Country]
currencies: [Currency]
languages: [Language]
@ -315,7 +327,7 @@ const typeDefs = gql`
notifications: [Notification]
alerts: [Notification]
hasUnreadNotifications: Boolean
bills: [Bills]
bills: [Bill]
}
type SupportLogsResponse {
@ -352,6 +364,8 @@ const typeDefs = gql`
toggleClearNotification(id: ID!, read: Boolean!): Notification
clearAllNotifications: Notification
cancelCashOutTransaction(id: ID): Transaction
createBatch(deviceId: ID, cashboxCount: Int): CashboxBatch
editBatch(id: ID, performedBy: String): CashboxBatch
}
`
@ -378,6 +392,7 @@ const resolvers = {
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
},
Query: {
cashboxBatches: () => cashbox.getBatches(),
countries: () => countries,
currencies: () => currencies,
languages: () => languages,
@ -449,7 +464,9 @@ const resolvers = {
deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId),
toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read),
clearAllNotifications: () => notifierQueries.markAllAsRead(),
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id)
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id),
createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount),
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
}
}

View file

@ -10,6 +10,7 @@ const express = require('express')
const nmd = require('nano-markdown')
const semver = require('semver')
const cashbox = require('./cashbox-batches')
const dbErrorCodes = require('./db-error-codes')
const options = require('./options')
const logger = require('./logger')
@ -222,6 +223,14 @@ function stateChange (req, res, next) {
.catch(next)
}
function notifyCashboxRemoval (req, res, next) {
return machineLoader.getMachine(req.deviceId)
.then(machineLoader => cashbox.createCashboxBatch(req.deviceId, machineLoader.cashbox))
.then(() => machineLoader.setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }))
.then(() => res.status(200).send({ status: 'OK' }))
.catch(next)
}
function verifyUser (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.verifyUser(req.body)
@ -546,6 +555,7 @@ app.use(filterOldRequests)
app.get('/poll', poll)
app.get('/terms_conditions', getTermsConditions)
app.post('/state', stateChange)
app.post('/cashbox/removal', notifyCashboxRemoval)
app.post('/network/heartbeat', networkHeartbeat)
app.post('/network/performance', networkPerformance)

View file

@ -0,0 +1,26 @@
var db = require('./db')
exports.up = function (next) {
var sqls = [
`create table cashbox_batches (
id uuid PRIMARY KEY,
device_id text REFERENCES devices (device_id),
created timestamptz NOT NULL default now()
)`,
`ALTER TABLE bills ADD COLUMN legacy boolean DEFAULT false`,
`ALTER TABLE bills ADD COLUMN cashbox_batch_id uuid`,
`ALTER TABLE bills ADD CONSTRAINT cashbox_batch_id
FOREIGN KEY (cashbox_batch_id)
REFERENCES cashbox_batches (id)`,
`UPDATE bills SET legacy = 'true'`
]
db.multi(sqls, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,25 @@
var db = require('./db')
exports.up = function (next) {
var sqls = [
`CREATE TYPE cashbox_batch_type AS ENUM(
'cash-in-empty',
'cash-out-1-refill',
'cash-out-1-empty',
'cash-out-2-refill',
'cash-out-2-empty',
'cash-out-3-refill',
'cash-out-3-empty',
'cash-out-4-refill',
'cash-out-4-empty'
)`,
`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 performed_by VARCHAR(64)`
]
db.multi(sqls, next)
}
exports.down = function (next) {
next()
}

View file

@ -2,18 +2,24 @@ import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag'
import * as R from 'ramda'
import React from 'react'
import React, { useState } from 'react'
import * as Yup from 'yup'
import { IconButton } from 'src/components/buttons'
import { Table as EditableTable } from 'src/components/editableTable'
import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
import TitleSection from 'src/components/layout/TitleSection'
import { EmptyTable } from 'src/components/table'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as ReverseHistoryIcon } from 'src/styling/icons/circle buttons/history/white.svg'
import { ReactComponent as HistoryIcon } from 'src/styling/icons/circle buttons/history/zodiac.svg'
import { fromNamespace } from 'src/utils/config'
import styles from './CashCassettes.styles.js'
import CashCassettesFooter from './CashCassettesFooter'
import CashboxHistory from './CashboxHistory'
import Wizard from './Wizard/Wizard'
const useStyles = makeStyles(styles)
@ -106,16 +112,28 @@ const SET_CASSETTE_BILLS = gql`
}
`
const CREATE_BATCH = gql`
mutation createBatch($deviceId: ID, $cashboxCount: Int) {
createBatch(deviceId: $deviceId, cashboxCount: $cashboxCount) {
id
}
}
`
const CashCassettes = () => {
const classes = useStyles()
const [showHistory, setShowHistory] = useState(false)
const { data } = useQuery(GET_MACHINES_AND_CONFIG)
const [wizard, setWizard] = useState(false)
const [machineId, setMachineId] = useState('')
const machines = R.path(['machines'])(data) ?? []
const config = R.path(['config'])(data) ?? {}
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
refetchQueries: () => ['getData']
})
const [createBatch] = useMutation(CREATE_BATCH)
const bills = R.groupBy(bill => bill.deviceId)(R.path(['bills'])(data) ?? [])
const deviceIds = R.uniq(
R.map(R.prop('deviceId'))(R.path(['bills'])(data) ?? [])
@ -126,10 +144,23 @@ const CashCassettes = () => {
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
}
})
}
const onSave = (
...[, { id, cashbox, cassette1, cassette2, cassette3, cassette4 }]
) => {
return setCassetteBills({
variables: {
action: 'setCassetteBills',
@ -198,19 +229,44 @@ const CashCassettes = () => {
1
)
elements.push({
name: 'edit',
header: 'Edit',
width: 87,
view: (value, { id }) => {
return (
<IconButton
onClick={() => {
setMachineId(id)
setWizard(true)
}}>
<EditIcon />
</IconButton>
)
}
})
return (
<>
<TitleSection title="Cash Cassettes" />
<TitleSection
title="Cash Cassettes"
button={{
text: 'Cashbox history',
icon: HistoryIcon,
inverseIcon: ReverseHistoryIcon,
toggle: setShowHistory
}}
iconClassName={classes.listViewButton}
/>
<div className={classes.tableContainer}>
{!showHistory && (
<>
<EditableTable
error={error?.message}
name="cashboxes"
enableEdit
enableEditText="Update"
stripeWhen={isCashOutDisabled}
elements={elements}
data={machines}
save={onSave}
validationSchema={ValidationSchema}
tbodyWrapperClass={classes.tBody}
/>
@ -218,6 +274,11 @@ const CashCassettes = () => {
{data && R.isEmpty(machines) && (
<EmptyTable message="No machines so far" />
)}
</>
)}
{showHistory && (
<CashboxHistory machines={machines} currency={fiatCurrency} />
)}
</div>
<CashCassettesFooter
currencyCode={fiatCurrency}
@ -226,6 +287,18 @@ const CashCassettes = () => {
bills={bills}
deviceIds={deviceIds}
/>
{wizard && (
<Wizard
machine={R.find(R.propEq('id', machineId), machines)}
cashoutSettings={getCashoutSettings(machineId)}
onClose={() => {
setWizard(false)
}}
error={error?.message}
save={onSave}
locale={locale}
/>
)}
</>
)
}

View file

@ -0,0 +1,263 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag'
import moment from 'moment'
import * as R from 'ramda'
import React, { useState } from 'react'
import * as Yup from 'yup'
import { Link, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs'
import { NumberInput } from 'src/components/inputs/formik'
import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
const GET_BATCHES = gql`
query cashboxBatches {
cashboxBatches {
id
deviceId
created
operationType
customBillCount
performedBy
bills {
fiat
deviceId
created
cashbox
}
}
}
`
const EDIT_BATCH = gql`
mutation editBatch($id: ID, $performedBy: String) {
editBatch(id: $id, performedBy: $performedBy) {
id
}
}
`
const styles = {
operationType: {
marginLeft: 8
},
operationTypeWrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
saveAndCancel: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
}
}
const schema = Yup.object().shape({
performedBy: Yup.string().nullable()
})
const useStyles = makeStyles(styles)
const CashboxHistory = ({ machines, currency }) => {
const classes = useStyles()
const [error, setError] = useState(false)
const [fields, setFields] = useState([])
const { data, loading } = useQuery(GET_BATCHES)
const [editBatch] = useMutation(EDIT_BATCH, {
refetchQueries: () => ['cashboxBatches']
})
const batches = R.path(['cashboxBatches'])(data)
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': (
<>
<TxInIcon />
<span className={classes.operationType}>Cash-in emptied</span>
</>
)
},
R.range(1, 5)
)
const save = row => {
const field = R.find(f => f.id === row.id, fields)
const performedBy = field.performedBy === '' ? null : field.performedBy
schema
.isValid(field)
.then(() => {
setError(false)
editBatch({
variables: { id: row.id, performedBy: performedBy }
})
})
.catch(setError(true))
return close(row.id)
}
const close = id => {
setFields(R.filter(f => f.id !== id, fields))
}
const notEditing = id => !R.any(R.propEq('id', id), fields)
const elements = [
{
name: 'operation',
header: 'Operation',
width: 200,
textAlign: 'left',
view: it => (
<div className={classes.operationTypeWrapper}>
{getOperationRender[it.operationType]}
</div>
)
},
{
name: 'machine',
header: 'Machine',
width: 200,
textAlign: 'left',
view: it => {
return R.find(R.propEq('id', it.deviceId))(machines).name
}
},
{
name: 'billCount',
header: 'Bill Count',
width: 115,
textAlign: 'left',
input: NumberInput,
inputProps: {
decimalPlaces: 0
},
view: it =>
R.isNil(it.customBillCount) ? it.bills.length : it.customBillCount
},
{
name: 'total',
header: 'Total',
width: 100,
textAlign: 'right',
view: it => (
<span>
{R.sum(R.map(b => R.prop('fiat', b), it.bills))} {currency}
</span>
)
},
{
name: 'date',
header: 'Date',
width: 135,
textAlign: 'right',
view: it => moment.utc(it.created).format('YYYY-MM-DD')
},
{
name: 'time',
header: 'Time (h:m)',
width: 125,
textAlign: 'right',
view: it => moment.utc(it.created).format('HH:mm')
},
{
name: 'performedBy',
header: 'Performed by',
width: 180,
textAlign: 'left',
view: it => {
if (notEditing(it.id))
return R.isNil(it.performedBy) ? 'Unknown entity' : it.performedBy
return (
<TextInput
onChange={e =>
setFields(
R.map(
f =>
f.id === it.id ? { ...f, performedBy: e.target.value } : f,
fields
)
)
}
error={error}
width={190 * 0.85}
value={R.prop(
'performedBy',
R.find(f => f.id === it.id, fields)
)}
/>
)
}
},
{
name: '',
header: 'Edit',
width: 150,
textAlign: 'right',
view: it => {
if (notEditing(it.id))
return (
<IconButton
onClick={() => {
setFields([
...fields,
{ id: it.id, performedBy: it.performedBy }
])
}}>
<EditIcon />
</IconButton>
)
return (
<div className={classes.saveAndCancel}>
<Link type="submit" color="primary" onClick={() => save(it)}>
Save
</Link>
<Link color="secondary" onClick={() => close(it.id)}>
Cancel
</Link>
</div>
)
}
}
]
return (
<>
{!loading && (
<DataTable
name="cashboxHistory"
elements={elements}
data={batches}
emptyText="No cashbox batches so far"
/>
)}
</>
)
}
export default CashboxHistory

View file

@ -0,0 +1,98 @@
import * as R from 'ramda'
import React, { useState } from 'react'
import * as Yup from 'yup'
import Modal from 'src/components/Modal'
import WizardSplash from './WizardSplash'
import WizardStep from './WizardStep'
const MODAL_WIDTH = 554
const MODAL_HEIGHT = 520
const CASHBOX_DEFAULT_CAPACITY = 500
const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
const [{ step, config }, setState] = useState({
step: 0,
config: { active: true }
})
const numberOfCassettes = R.isEmpty(cashoutSettings)
? 0
: machine.numberOfCassettes
const LAST_STEP = numberOfCassettes + 1
const title = `Update counts`
const isLastStep = step === LAST_STEP
const onContinue = it => {
const cashbox = config?.wasCashboxEmptied === 'YES' ? 0 : machine?.cashbox
if (isLastStep) {
const { cassette1, cassette2, cassette3, cassette4 } = R.map(parseInt, it)
save(machine.id, cashbox, cassette1, cassette2, cassette3, cassette4)
return onClose()
}
const newConfig = R.merge(config, it)
setState({
step: step + 1,
config: newConfig
})
}
const makeCassetteSteps = R.pipe(
R.add(1),
R.range(1),
R.map(i => ({
type: `cassette ${i}`,
schema: Yup.object().shape({
[`cassette${i}`]: Yup.number()
.positive()
.integer()
.required()
.min(0)
.max(CASHBOX_DEFAULT_CAPACITY)
})
}))
)
const steps = R.prepend(
{
type: 'cashbox',
schema: Yup.object().shape({
wasCashboxEmptied: Yup.string().required()
})
},
makeCassetteSteps(numberOfCassettes)
)
return (
<Modal
title={step === 0 ? null : title}
handleClose={onClose}
width={MODAL_WIDTH}
height={MODAL_HEIGHT}
open={true}>
{step === 0 && (
<WizardSplash name={machine?.name} onContinue={() => onContinue()} />
)}
{step !== 0 && (
<WizardStep
step={step}
name={machine?.name}
machine={machine}
cashoutSettings={cashoutSettings}
cassetteCapacity={CASHBOX_DEFAULT_CAPACITY}
error={error}
lastStep={isLastStep}
steps={steps}
fiatCurrency={locale.fiatCurrency}
onContinue={onContinue}
/>
)}
</Modal>
)
}
export default Wizard

View file

@ -0,0 +1,81 @@
import { makeStyles } from '@material-ui/core'
import React from 'react'
import { Button } from 'src/components/buttons'
import { H1, P, Info2 } from 'src/components/typography'
import filledCassettes from 'src/styling/icons/cassettes/both-filled.svg'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
import { comet } from 'src/styling/variables'
const styles = {
button: {
margin: [[35, 'auto', 0, 'auto']]
},
modalContent: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
flex: 1,
padding: [[0, 34]]
},
splashTitle: {
marginTop: 15
},
warningInfo: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: 15
},
warningIcon: {
width: 25,
height: 25,
marginRight: 8,
display: 'block'
},
warningText: {
flexBasis: '100%',
flexGrow: 1
},
machineName: {
margin: [[5, 0]],
color: comet
}
}
const useStyles = makeStyles(styles)
const WizardSplash = ({ name, onContinue }) => {
const classes = useStyles()
return (
<div className={classes.modalContent}>
<img width="148" height="196" alt="cassette" src={filledCassettes}></img>
<H1 className={classes.splashTitle} noMargin>
Update counts
</H1>
<Info2 className={classes.machineName} noMargin>
{name}
</Info2>
<div className={classes.warningInfo}>
<WarningIcon className={classes.warningIcon} />
<P noMargin className={classes.warningText}>
Before updating counts on Lamassu Admin, make sure you've done it
before on the machines.
</P>
</div>
<div className={classes.warningInfo}>
<WarningIcon className={classes.warningIcon} />
<P noMargin className={classes.warningText}>
For cash-out cassettes, please make sure you've removed the remaining
bills before adding the new ones.
</P>
</div>
<Button className={classes.button} onClick={onContinue}>
Get started
</Button>
</div>
)
}
export default WizardSplash

View file

@ -0,0 +1,294 @@
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { Formik, Form, Field } from 'formik'
import * as R from 'ramda'
import React from 'react'
import Stepper from 'src/components/Stepper'
import { Tooltip } from 'src/components/Tooltip'
import { Button } from 'src/components/buttons'
import { Cashbox } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
import { Info2, H4, P, Info1 } from 'src/components/typography'
import cashbox from 'src/styling/icons/cassettes/acceptor-left.svg'
import cassetteOne from 'src/styling/icons/cassettes/dispenser-1.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 { comet } from 'src/styling/variables'
const styles = {
content: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
flex: 1,
paddingBottom: 32
},
titleDiv: {
marginBottom: 32
},
title: {
margin: [[0, 0, 12, 0]],
color: comet
},
stepImage: {
width: 148,
height: 196
},
form: {
paddingBottom: 95
},
verticalAlign: {
display: 'flex',
flexDirection: 'column'
},
horizontalAlign: {
display: 'flex',
flexDirection: 'row'
},
centerAlignment: {
alignItems: 'center'
},
lineAlignment: {
alignItems: 'baseline'
},
fullWidth: {
margin: [[0, 'auto']],
flexBasis: 'auto'
},
formWrapper: {
flexBasis: '100%',
display: 'flex',
justifyContent: 'center'
},
submit: {
float: 'right'
},
cashboxBills: {
marginRight: 5
},
cassetteCashbox: {
width: 40,
height: 35
},
cassetteFormTitle: {
marginTop: 18
},
cassetteFormTitleContent: {
marginLeft: 10,
marginRight: 25
},
smBottomMargin: {
marginBottom: 25
},
fiatTotal: {
color: comet
}
}
const useStyles = makeStyles(styles)
const cassetesArtworks = (numberOfCassettes, step) =>
[
[cassetteOne, cassetteTwo],
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
[tejo4CassetteOne, tejo4CassetteTwo, tejo4CassetteThree, tejo4CassetteFour]
][numberOfCassettes - 2][step - 2]
const WizardStep = ({
step,
name,
machine,
cashoutSettings,
cassetteCapacity,
error,
lastStep,
steps,
fiatCurrency,
onContinue
}) => {
const classes = useStyles()
const label = lastStep ? 'Finish' : 'Confirm'
const stepOneRadioOptions = [
{ display: 'Yes', code: 'YES' },
{ display: 'No', code: 'NO' }
]
const cassetteField = `cassette${step - 1}`
const numberOfCassettes = machine.numberOfCassettes
const originalCassetteCount = machine?.[cassetteField]
const cassetteDenomination = cashoutSettings?.[cassetteField]
const cassetteCount = values => values[cassetteField] || originalCassetteCount
const cassetteTotal = values => cassetteCount(values) * cassetteDenomination
const getPercentage = R.pipe(
cassetteCount,
count => 100 * (count / cassetteCapacity),
R.clamp(0, 100)
)
return (
<div className={classes.content}>
<div className={classes.titleDiv}>
<Info2 className={classes.title}>{name}</Info2>
<Stepper steps={steps.length} currentStep={step} />
</div>
{step === 1 && (
<Formik
validateOnBlur={false}
validateOnChange={false}
onSubmit={onContinue}
initialValues={{ wasCashboxEmptied: '' }}
enableReinitialize
validationSchema={steps[0].schema}>
{({ values }) => (
<Form>
<div
className={classnames(classes.horizontalAlign, classes.form)}>
<img
className={classes.stepImage}
alt="cassette"
src={cashbox}></img>
<div className={classes.formWrapper}>
<div
className={classnames(
classes.verticalAlign,
classes.fullWidth
)}>
<H4 noMargin>Did you empty the cash-in box?</H4>
<Field
component={RadioGroup}
name="wasCashboxEmptied"
options={stepOneRadioOptions}
className={classes.horizontalAlign}
/>
<div
className={classnames(
classes.horizontalAlign,
classes.centerAlignment
)}>
<P>Since previous update</P>
<Tooltip width={215}>
<P>
Number of bills inside the cashbox, since the last
cashbox changes.
</P>
</Tooltip>
</div>
<div
className={classnames(
classes.horizontalAlign,
classes.lineAlignment
)}>
<Info1 noMargin className={classes.cashboxBills}>
{machine?.cashbox}
</Info1>
<P noMargin>accepted bills</P>
</div>
</div>
</div>
</div>
<Button className={classes.submit} type="submit">
{label}
</Button>
</Form>
)}
</Formik>
)}
{step > 1 && (
<Formik
validateOnBlur={false}
validateOnChange={false}
onSubmit={onContinue}
initialValues={{
cassette1: '',
cassette2: '',
cassette3: '',
cassette4: ''
}}
enableReinitialize
validationSchema={steps[step - 1].schema}>
{({ values }) => (
<Form>
<div
className={classnames(classes.horizontalAlign, classes.form)}>
<img
className={classes.stepImage}
alt="cassette"
src={cassetesArtworks(numberOfCassettes, step)}></img>
<div className={classes.formWrapper}>
<div
className={classnames(
classes.verticalAlign,
classes.fullWidth
)}>
<div
className={classnames(
classes.horizontalAlign,
classes.smBottomMargin
)}>
<div
className={classnames(
classes.horizontalAlign,
classes.cassetteFormTitle
)}>
<TxOutIcon />
<H4
className={classes.cassetteFormTitleContent}
noMargin>
Cash-out {step - 1} (dispenser)
</H4>
</div>
<Cashbox
className={classes.cassetteCashbox}
percent={getPercentage(values)}
cashOut
/>
</div>
<H4 noMargin>Refill bill count</H4>
<div
className={classnames(
classes.horizontalAlign,
classes.lineAlignment
)}>
<Field
component={NumberInput}
decimalPlaces={0}
width={50}
placeholder={originalCassetteCount.toString()}
name={cassetteField}
className={classes.cashboxBills}
/>
<P>
{cassetteDenomination} {fiatCurrency} bills loaded
</P>
</div>
<P noMargin className={classes.fiatTotal}>
= {cassetteTotal(values)} {fiatCurrency}
</P>
</div>
</div>
</div>
<Button className={classes.submit} type="submit">
{label}
</Button>
</Form>
)}
</Formik>
)}
</div>
)
}
export default WizardStep

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.58 258.82"><defs><style>.cls-1{fill:#ebefff;}.cls-2{fill:#4b5fef;}.cls-3{fill:#1b2559;}.cls-4{fill:#48f694;}.cls-5{fill:#41de85;}.cls-6{fill:#00cd5a;}.cls-7{fill:#5a67ff;}.cls-8{fill:#7687ff;}.cls-9{fill:#ccd8ff;opacity:0.74;}.cls-10{fill:#dee5fc;opacity:0.8;}</style></defs><title>acceptor-left-filled</title><path class="cls-1" d="M122.26,105.88V258.82l61.13-35.29V70.59Z"/><g id="Drawer_2" data-name="Drawer 2"><g id="rest_of_drawer" data-name="rest of drawer"><polygon id="Inner_right_side" data-name="Inner right side" class="cls-2" points="71.32 97.69 152.83 144.75 189.58 123.53 71.32 55.25 71.32 97.69"/><polygon class="cls-3" points="171.21 134.14 171.21 112.92 162.13 107.68 162.13 139.43 171.21 134.14"/><polygon id="Back" class="cls-3" points="71.32 55.25 71.32 97.69 52.94 87.08 34.56 76.47 71.32 55.25"/><polygon class="cls-4" points="143.05 149.7 71.32 108.29 71.32 75.78 171.21 133.45 143.05 149.7"/><polygon class="cls-4" points="71.32 75.78 71.32 108.29 43.16 92.03 71.32 75.78"/><polygon class="cls-5" points="171.21 134.05 171.21 133.45 143.05 149.7 143.57 150 171.21 134.05"/><polygon class="cls-4" points="143.05 148.1 71.32 106.69 71.32 74.18 171.21 131.85 143.05 148.1"/><polygon class="cls-4" points="71.32 74.18 71.32 106.69 43.16 90.43 71.32 74.18"/><polygon class="cls-5" points="171.21 132.45 171.21 131.85 143.05 148.1 143.57 148.4 171.21 132.45"/><polygon class="cls-4" points="143.05 146.5 71.32 105.09 71.32 72.58 171.21 130.25 143.05 146.5"/><polygon class="cls-4" points="71.32 72.58 71.32 105.09 43.16 88.83 71.32 72.58"/><polygon class="cls-5" points="171.21 130.85 171.21 130.25 143.05 146.5 143.57 146.8 171.21 130.85"/><polygon class="cls-4" points="143.05 144.9 71.32 103.49 71.32 70.98 171.21 128.65 143.05 144.9"/><polygon class="cls-4" points="71.32 70.98 71.32 103.49 43.16 87.23 71.32 70.98"/><polygon class="cls-5" points="171.21 129.25 171.21 128.65 143.05 144.9 143.57 145.2 171.21 129.25"/><polygon class="cls-4" points="143.05 143.3 71.32 101.89 71.32 69.38 171.21 127.05 143.05 143.3"/><polygon class="cls-4" points="71.32 69.38 71.32 101.89 43.16 85.63 71.32 69.38"/><polygon class="cls-5" points="171.21 127.65 171.21 127.05 143.05 143.3 143.57 143.6 171.21 127.65"/><polygon class="cls-4" points="143.05 141.5 71.32 100.09 71.32 67.58 171.21 125.25 143.05 141.5"/><polygon class="cls-4" points="71.32 67.58 71.32 100.09 43.16 83.83 71.32 67.58"/><polygon class="cls-5" points="171.21 125.85 171.21 125.25 143.05 141.5 143.57 141.8 171.21 125.85"/><polygon class="cls-4" points="143.05 139.9 71.32 98.49 71.32 65.98 171.21 123.65 143.05 139.9"/><polygon class="cls-4" points="71.32 65.98 71.32 98.49 43.16 82.23 71.32 65.98"/><polygon class="cls-5" points="171.21 124.25 171.21 123.65 143.05 139.9 143.57 140.2 171.21 124.25"/><polygon class="cls-4" points="143.05 138.3 71.32 96.89 71.32 64.38 171.21 122.05 143.05 138.3"/><polygon class="cls-4" points="71.32 64.38 71.32 96.89 43.16 80.63 71.32 64.38"/><polygon class="cls-5" points="171.21 122.65 171.21 122.05 143.05 138.3 143.05 138.9 171.21 122.65"/><polygon class="cls-5" points="43.16 81.23 43.16 80.63 143.05 138.3 143.05 138.9 43.16 81.23"/><polygon class="cls-4" points="143.05 136.7 71.32 95.29 71.32 62.78 171.21 120.45 143.05 136.7"/><polygon class="cls-4" points="71.32 62.78 71.32 95.29 43.16 79.03 71.32 62.78"/><polygon class="cls-5" points="171.21 121.05 171.21 120.45 143.05 136.7 143.05 137.3 171.21 121.05"/><polygon class="cls-5" points="43.16 79.63 43.16 79.03 143.05 136.7 143.05 137.3 43.16 79.63"/><path class="cls-6" d="M97,94.41c5-2.9,13.26-2.9,18.29,0s5,7.66,0,10.56S102,107.88,97,105,91.92,97.32,97,94.41Z"/><polygon id="Outer_right_side" data-name="Outer right side" class="cls-2" points="152.83 252.94 30.57 182.35 30.57 76.47 152.83 147.06 152.83 252.94"/></g><g id="Front_drawer" data-name="Front drawer"><polygon id="Front" class="cls-7" points="193.58 123.53 152.83 147.06 152.83 252.94 193.58 229.41 193.58 123.53"/><path id="Top_edge" data-name="Top edge" class="cls-8" d="M71.32,52.94,30.57,76.47l122.26,70.59,40.75-23.53ZM34.56,76.47,71.32,55.25l118.26,68.28-36.75,21.22Z"/></g><line class="cls-4" x1="171.21" y1="122.85" x2="143.05" y2="139.1"/></g><polygon class="cls-1" points="132.45 182.36 132.45 135.29 183.4 105.88 183.4 70.59 122.26 105.88 122.26 126.47 122.26 126.47 122.26 138.4 122.26 141.18 122.26 141.18 122.26 258.82 132.45 252.94 132.45 194.12 132.45 182.36"/><polygon id="left_part_box" data-name="left part box" class="cls-9" points="122.26 105.88 0 35.29 0 188.23 122.26 258.82 122.26 105.88"/><polygon id="top_part_box" data-name="top part box" class="cls-10" points="183.4 70.59 61.13 0 0 35.29 122.26 105.88 183.4 70.59"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.58 258.82"><defs><style>.cls-1{fill:#4b5fef;}.cls-2{fill:#7687ff;}.cls-3{fill:#ebefff;}.cls-4{fill:#5a67ff;}.cls-5{fill:#1b2559;}.cls-6{fill:#ccd8ff;opacity:0.74;}.cls-7{fill:#dee5fc;opacity:0.8;}</style></defs><title>acceptor-left</title><polyline class="cls-1" points="71.32 97.69 124.25 128.26 161 107.04 71.32 55.25"/><polygon class="cls-2" points="163.01 105.88 71.32 52.94 30.57 76.47 122.26 129.41 163.01 105.88"/><polygon class="cls-3" points="183.4 70.59 183.39 223.53 122.26 258.82 122.26 105.88 183.4 70.59"/><polygon class="cls-4" points="193.58 123.53 152.83 147.06 152.83 252.94 193.58 229.41 193.58 123.53"/><polygon class="cls-1" points="152.83 252.94 132.45 241.18 132.45 135.29 152.83 147.06 152.83 252.94"/><polygon class="cls-1" points="122.26 235.3 30.56 182.36 30.57 76.47 122.26 129.41 122.26 235.3"/><polyline class="cls-2" points="132.45 135.29 152.83 147.06 193.58 123.53 173.21 111.77"/><polyline class="cls-1" points="134.45 134.14 152.83 144.75 189.58 123.53 171.21 112.92"/><polygon class="cls-5" points="171.21 112.92 171.21 134.14 152.83 144.75 134.45 134.14 171.21 112.92"/><polygon class="cls-5" points="71.32 55.25 71.32 97.69 52.94 87.08 34.56 76.47 71.32 55.25"/><polygon class="cls-6" points="122.26 105.88 0 35.29 0 188.23 122.26 258.82 122.26 105.88"/><polygon class="cls-7" points="183.4 70.59 61.13 0 0 35.29 122.26 105.88 183.4 70.59"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.58259 258.82261"><defs><style>.a,.l{fill:#fff;}.a,.d,.j{opacity:0.74;}.b,.j{fill:#ccd8ff;}.c{fill:#d2d8ff;}.d,.h{fill:#5a67ff;}.e{fill:#4b5fef;}.f{fill:#7687ff;}.g{fill:#ebefff;}.i{fill:#1b2559;}.k{fill:#dee5fc;opacity:0.8;}.m{fill:#aebaff;}</style></defs><title>v2-1</title><polygon class="a" points="163.012 164.703 50.942 100 10.189 123.53 122.263 188.236 163.012 164.703"/><polyline class="b" points="50.938 144.748 124.248 187.087 161.003 165.867 50.938 102.309"/><polygon class="c" points="122.261 235.293 10.186 170.589 10.189 123.53 122.261 188.234 122.261 235.293"/><polygon class="d" points="50.938 102.309 50.938 144.748 32.56 134.14 14.184 123.529 50.938 102.309"/><polyline class="e" points="71.317 97.688 124.249 128.264 161.004 107.044 71.317 55.25"/><polygon class="f" points="163.013 105.88 71.32 52.94 30.568 76.47 122.264 129.413 163.013 105.88"/><polygon class="g" points="183.396 70.586 183.393 223.529 122.26 258.823 122.264 105.88 183.396 70.586"/><polygon class="c" points="173.205 170.589 132.45 194.119 132.45 241.178 173.206 217.648 173.205 170.589"/><polygon class="h" points="193.582 123.529 152.827 147.059 152.827 194.118 193.583 170.587 193.582 123.529"/><polygon class="e" points="152.827 194.118 132.448 182.353 132.451 135.294 152.827 147.059 152.827 194.118"/><polygon class="e" points="122.261 176.47 30.565 123.529 30.568 76.47 122.261 129.411 122.261 176.47"/><polyline class="f" points="132.451 135.294 152.827 147.059 193.582 123.529 173.206 111.765"/><polyline class="e" points="134.451 134.138 152.827 144.749 189.582 123.53 171.205 112.918"/><polygon class="i" points="171.205 112.918 171.205 134.138 152.827 144.749 134.451 134.138 171.205 112.918"/><polygon class="i" points="71.317 55.25 71.317 97.688 52.939 87.081 34.563 76.47 71.317 55.25"/><polygon class="j" points="122.264 105.88 0.003 35.292 0 188.234 122.26 258.823 122.264 105.88"/><polygon class="k" points="183.396 70.586 61.131 0 0.003 35.292 122.264 105.88 183.396 70.586"/><path class="l" d="M168.29324,169.92639l4.38476-2.48092-.047-11.27756c-.00286-.68706.02246-1.38981.02246-1.38981l-.05636.03136a9.58844,9.58844,0,0,1-.86851,1.6315l-1.60382,2.44627-2.14443-1.10638,4.99115-7.6831,3.16791-1.75169.12588,17.06967,4.38476-2.48092.03567,3.146-12.39164,7.04027Z"/><path class="m" d="M148.46477,220.14711c0-6.74172,8.50209-12.68419,8.49771-16.33186-.00192-1.60372-1.23141-1.8737-2.76652-1.0152a9.13732,9.13732,0,0,0-3.27439,4.03233l-2.4568-.30054a14.47236,14.47236,0,0,1,6.01029-7.06167c3.19687-1.76715,5.91119-1.31443,5.917,2.12668.00994,5.85452-8.13556,11.56521-8.21069,15.11138l8.50983-4.9792.005,2.84873-12.078,7.13776A11.38945,11.38945,0,0,1,148.46477,220.14711Z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 193.58151 258.82261"><defs><style>.a{fill:#4b5fef;}.b{fill:#7687ff;}.c{fill:#1b2559;}.d,.l{fill:#fff;}.d,.g,.i{opacity:0.74;}.e,.i{fill:#ccd8ff;}.f{fill:#d2d8ff;}.g,.k{fill:#5a67ff;}.h{fill:#ebefff;}.j{fill:#dee5fc;opacity:0.8;}</style></defs><title>v2-2</title><polyline class="a" points="71.318 156.512 124.25 187.087 161.005 165.867 71.318 114.073"/><polygon class="b" points="163.014 164.703 71.321 111.764 30.569 135.294 122.265 188.237 163.014 164.703"/><polygon class="a" points="122.262 235.294 30.565 182.353 30.569 135.294 122.262 188.235 122.262 235.294"/><polygon class="c" points="71.318 114.073 71.318 156.512 52.94 145.904 34.564 135.293 71.318 114.073"/><polygon class="d" points="163.014 96.672 50.943 31.969 10.191 55.499 122.265 120.205 163.014 96.672"/><polyline class="e" points="50.94 76.717 124.25 119.056 161.005 97.836 50.94 34.278"/><polygon class="f" points="122.262 167.262 10.188 102.558 10.191 55.499 122.262 120.203 122.262 167.262"/><polygon class="g" points="50.94 34.278 50.94 76.717 32.562 66.109 14.186 55.498 50.94 34.278"/><polygon class="h" points="183.396 70.586 183.393 223.529 122.26 258.823 122.264 105.88 183.396 70.586"/><polygon class="f" points="173.205 100 132.45 123.53 132.45 170.589 173.206 147.058 173.205 100"/><polygon class="i" points="122.264 105.88 0.003 35.292 0 188.234 122.26 258.823 122.264 105.88"/><polygon class="j" points="183.396 70.586 61.131 0 0.003 35.292 122.264 105.88 183.396 70.586"/><polygon class="k" points="193.581 182.354 152.826 205.884 152.826 252.943 193.582 229.412 193.581 182.354"/><polygon class="a" points="152.826 252.943 132.447 241.178 132.45 194.119 152.826 205.884 152.826 252.943"/><polyline class="b" points="132.45 194.119 152.826 205.884 193.581 182.354 173.205 170.59"/><polyline class="a" points="134.45 192.963 152.826 203.574 189.581 182.355 171.204 171.743"/><polygon class="c" points="171.204 171.743 171.204 192.963 152.826 203.574 134.45 192.963 171.204 171.743"/><path class="l" d="M166.09883,231.81817c0-7.5674,9.54391-14.23774,9.539-18.33135-.00216-1.80015-1.38174-2.10383-3.10553-1.13988-2.38357,1.33288-3.67542,4.52582-3.67542,4.52582l-2.758-.33729a16.24393,16.24393,0,0,1,6.74659-7.92622c3.58844-1.98344,6.6354-1.47533,6.642,2.38714.01116,6.57184-9.132,12.98179-9.21626,16.96152l9.5526-5.58888.00556,3.19778-13.55763,8.01151A12.67431,12.67431,0,0,1,166.09883,231.81817Z"/><path class="h" d="M148.45675,147.16772l4.92216-2.785-.0527-12.65868c-.00321-.77094.02451-1.5598.02451-1.5598l-.06255.03481a10.7746,10.7746,0,0,1-.976,1.83219l-1.79958,2.74486-2.407-1.241,5.60186-8.624,3.55679-1.9667.14131,19.16,4.92146-2.78457.04,3.53148-13.90939,7.90252Z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB