diff --git a/lib/cashbox-batches.js b/lib/cashbox-batches.js
new file mode 100644
index 00000000..18e7e3c1
--- /dev/null
+++ b/lib/cashbox-batches.js
@@ -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 }
diff --git a/lib/machine-loader.js b/lib/machine-loader.js
index 6d3b6a10..0576dbc1 100644
--- a/lib/machine-loader.js
+++ b/lib/machine-loader.js
@@ -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)
})
}
diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js
index a81fbc84..5987f298 100644
--- a/lib/new-admin/graphql/schema.js
+++ b/lib/new-admin/graphql/schema.js
@@ -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)
}
}
diff --git a/lib/routes.js b/lib/routes.js
index 119038cf..d8e7774e 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -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)
diff --git a/migrations/1616528363530-add_cashbox_batches.js b/migrations/1616528363530-add_cashbox_batches.js
new file mode 100644
index 00000000..bfb88db3
--- /dev/null
+++ b/migrations/1616528363530-add_cashbox_batches.js
@@ -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()
+}
diff --git a/migrations/1617967601902-add-batches-type.js b/migrations/1617967601902-add-batches-type.js
new file mode 100644
index 00000000..20d64368
--- /dev/null
+++ b/migrations/1617967601902-add-batches-type.js
@@ -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()
+}
diff --git a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js
index dfcea4dc..04a556fd 100644
--- a/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js
+++ b/new-lamassu-admin/src/pages/Maintenance/CashCassettes.js
@@ -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,25 +229,55 @@ const CashCassettes = () => {
1
)
+ elements.push({
+ name: 'edit',
+ header: 'Edit',
+ width: 87,
+ view: (value, { id }) => {
+ return (
+ {
+ setMachineId(id)
+ setWizard(true)
+ }}>
+
+
+ )
+ }
+ })
+
return (
<>
-
+
-
+ {!showHistory && (
+ <>
+
- {data && R.isEmpty(machines) && (
-
+ {data && R.isEmpty(machines) && (
+
+ )}
+ >
+ )}
+ {showHistory && (
+
)}
{
bills={bills}
deviceIds={deviceIds}
/>
+ {wizard && (
+ {
+ setWizard(false)
+ }}
+ error={error?.message}
+ save={onSave}
+ locale={locale}
+ />
+ )}
>
)
}
diff --git a/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js b/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js
new file mode 100644
index 00000000..b3e71917
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Maintenance/CashboxHistory.js
@@ -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`,
+ <>
+
+ Cash-out {i} refill
+ >
+ ),
+ R.assoc(
+ `cash-out-${i}-empty`,
+ <>
+
+ Cash-out {i} emptied
+ >
+ )
+ )(ret),
+ {
+ 'cash-in-empty': (
+ <>
+
+ Cash-in emptied
+ >
+ )
+ },
+ 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 => (
+
+ {getOperationRender[it.operationType]}
+
+ )
+ },
+ {
+ 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 => (
+
+ {R.sum(R.map(b => R.prop('fiat', b), it.bills))} {currency}
+
+ )
+ },
+ {
+ 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 (
+
+ 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 (
+ {
+ setFields([
+ ...fields,
+ { id: it.id, performedBy: it.performedBy }
+ ])
+ }}>
+
+
+ )
+ return (
+
+ save(it)}>
+ Save
+
+ close(it.id)}>
+ Cancel
+
+
+ )
+ }
+ }
+ ]
+
+ return (
+ <>
+ {!loading && (
+
+ )}
+ >
+ )
+}
+
+export default CashboxHistory
diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/Wizard.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/Wizard.js
new file mode 100644
index 00000000..a7c99998
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/Wizard.js
@@ -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 (
+
+ {step === 0 && (
+ onContinue()} />
+ )}
+ {step !== 0 && (
+
+ )}
+
+ )
+}
+
+export default Wizard
diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardSplash.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardSplash.js
new file mode 100644
index 00000000..a5cb66ff
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardSplash.js
@@ -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 (
+
+
+
+ Update counts
+
+
+ {name}
+
+
+
+
+ Before updating counts on Lamassu Admin, make sure you've done it
+ before on the machines.
+
+
+
+
+
+ For cash-out cassettes, please make sure you've removed the remaining
+ bills before adding the new ones.
+
+
+
+ Get started
+
+
+ )
+}
+
+export default WizardSplash
diff --git a/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js
new file mode 100644
index 00000000..8de43f87
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Maintenance/Wizard/WizardStep.js
@@ -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 (
+
+
+ {name}
+
+
+
+ {step === 1 && (
+
+ {({ values }) => (
+
+ )}
+
+ )}
+
+ {step > 1 && (
+
+ {({ values }) => (
+
+ )}
+
+ )}
+
+ )
+}
+
+export default WizardStep
diff --git a/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left-filled.svg b/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left-filled.svg
new file mode 100644
index 00000000..c2a9b20c
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left-filled.svg
@@ -0,0 +1 @@
+acceptor-left-filled
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left.svg b/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left.svg
new file mode 100644
index 00000000..06519d18
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/cassettes/acceptor-left.svg
@@ -0,0 +1 @@
+acceptor-left
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/cassettes/both-filled.svg b/new-lamassu-admin/src/styling/icons/cassettes/both-filled.svg
new file mode 100644
index 00000000..6104c6dd
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/cassettes/both-filled.svg
@@ -0,0 +1 @@
+both-filled
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/cassettes/dispenser-1.svg b/new-lamassu-admin/src/styling/icons/cassettes/dispenser-1.svg
new file mode 100644
index 00000000..a255b0b3
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/cassettes/dispenser-1.svg
@@ -0,0 +1 @@
+v2-1
\ No newline at end of file
diff --git a/new-lamassu-admin/src/styling/icons/cassettes/dispenser-2.svg b/new-lamassu-admin/src/styling/icons/cassettes/dispenser-2.svg
new file mode 100644
index 00000000..68c9ea1b
--- /dev/null
+++ b/new-lamassu-admin/src/styling/icons/cassettes/dispenser-2.svg
@@ -0,0 +1 @@
+v2-2
\ No newline at end of file