diff --git a/new-lamassu-admin/src/pages/Cashout/Cashout.js b/new-lamassu-admin/src/pages/Cashout/Cashout.js
new file mode 100644
index 00000000..331e6642
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Cashout/Cashout.js
@@ -0,0 +1,246 @@
+import { useQuery, useMutation } from '@apollo/react-hooks'
+import { makeStyles } from '@material-ui/core'
+import { gql } from 'apollo-boost'
+import React, { useState } from 'react'
+
+import Modal from 'src/components/Modal'
+import Title from 'src/components/Title'
+import { Switch } from 'src/components/inputs'
+import { P, Info2 } from 'src/components/typography'
+import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
+import commonStyles from 'src/pages/common.styles'
+import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg'
+import { spacer, zircon } from 'src/styling/variables'
+
+import Table from './CashoutTable'
+import Wizard from './Wizard'
+import WizardSplash from './WizardSplash'
+
+const GET_MACHINES_AND_CONFIG = gql`
+ {
+ machines {
+ name
+ deviceId
+ cashbox
+ cassette1
+ cassette2
+ }
+ config
+ }
+`
+
+const SAVE_CONFIG = gql`
+ mutation Save($config: JSONObject) {
+ saveConfig(config: $config)
+ }
+`
+
+const useStyles = makeStyles({
+ ...mainStyles,
+ commonStyles,
+ help: {
+ width: 20,
+ height: 20,
+ marginLeft: spacer * 2
+ },
+ disabledDrawing: {
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ '& > div': {
+ position: 'absolute',
+ backgroundColor: zircon,
+ height: 36,
+ width: 678
+ }
+ },
+ modal: {
+ width: 544
+ },
+ switchErrorMessage: {
+ margin: [['auto', 0, 'auto', 20]]
+ }
+})
+
+const Cashboxes = () => {
+ const [machines, setMachines] = useState([])
+ const [config, setConfig] = useState({})
+
+ const [modalContent, setModalContent] = useState(null)
+ const [modalOpen, setModalOpen] = useState(false)
+ const classes = useStyles()
+
+ const { refetch } = useQuery(GET_MACHINES_AND_CONFIG, {
+ onCompleted: ({ machines, config }) => {
+ setMachines(
+ machines.map(m => ({
+ ...m,
+ currency: config.fiatCurrency ?? { code: 'N/D' },
+ cashOutDenominations: (config.cashOutDenominations ?? {})[m.deviceId]
+ }))
+ )
+ setConfig(config)
+ }
+ })
+
+ const [saveConfig] = useMutation(SAVE_CONFIG)
+
+ const saveCashoutConfig = machine =>
+ saveConfig({
+ variables: {
+ config: {
+ ...config,
+ cashOutDenominations: {
+ ...config.cashOutDenominations,
+ [machine.deviceId]: machine.cashOutDenominations
+ }
+ }
+ }
+ })
+
+ const handleEnable = machine => event => {
+ setModalContent(
+
+ )
+ setModalOpen(true)
+ }
+
+ const handleEditClick = row => {
+ setModalOpen(true)
+ handleModalNavigation(row)(1)
+ }
+
+ const handleModalClose = () => {
+ setModalOpen(false)
+ setModalContent(null)
+ }
+
+ const handleModalNavigation = machine => currentPage => {
+ switch (currentPage) {
+ case 1:
+ setModalContent(
+
+ )
+ break
+ case 2:
+ setModalContent(
+
+ )
+ break
+ case 3:
+ setModalContent(
+
+ )
+ break
+ case 4:
+ // save
+ return saveCashoutConfig(machine)
+ .then(refetch)
+ .then(() => {
+ setModalOpen(false)
+ setModalContent(null)
+ })
+ default:
+ break
+ }
+
+ return new Promise(() => {})
+ }
+
+ const elements = [
+ {
+ header: 'Machine',
+ size: 254,
+ textAlign: 'left',
+ view: m => m.name
+ },
+ {
+ header: 'Cassette 1 (Top)',
+ size: 265,
+ textAlign: 'left',
+ view: ({ cashOutDenominations, currency }) => (
+ <>
+ {cashOutDenominations && cashOutDenominations.top && (
+
+ {cashOutDenominations.top} {currency.code}
+
+ )}
+ >
+ )
+ },
+ {
+ header: 'Cassette 2',
+ size: 265,
+ textAlign: 'left',
+ view: ({ cashOutDenominations, currency }) => (
+ <>
+ {cashOutDenominations && cashOutDenominations.bottom && (
+
+ {cashOutDenominations.bottom} {currency.code}
+
+ )}
+ >
+ )
+ },
+ {
+ header: 'Edit',
+ size: 265,
+ textAlign: 'left'
+ },
+ {
+ header: 'Enable',
+ size: 151,
+ textAlign: 'right'
+ }
+ ]
+
+ return (
+ <>
+
+
+
Cash-out
+
+
+
+ Transaction fudge factor On{' '}
+
+
+
+
+
+
+ {modalContent}
+
+ >
+ )
+}
+
+export default Cashboxes
diff --git a/new-lamassu-admin/src/pages/Cashout/CashoutTable.js b/new-lamassu-admin/src/pages/Cashout/CashoutTable.js
new file mode 100644
index 00000000..b5b607c3
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Cashout/CashoutTable.js
@@ -0,0 +1,132 @@
+import { makeStyles } from '@material-ui/core/styles'
+import classnames from 'classnames'
+import React, { useState } from 'react'
+
+import {
+ Th,
+ Tr,
+ Td,
+ THead,
+ TBody,
+ Table
+} from 'src/components/fake-table/Table'
+import { Switch } from 'src/components/inputs'
+import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
+import { spacer } from 'src/styling/variables'
+
+const styles = {
+ expandButton: {
+ border: 'none',
+ backgroundColor: 'transparent',
+ cursor: 'pointer',
+ padding: 4
+ },
+ row: {
+ borderRadius: 0
+ },
+ link: {
+ marginLeft: spacer
+ }
+}
+
+const useStyles = makeStyles(styles)
+
+const Row = ({
+ id,
+ elements,
+ data,
+ active,
+ rowAction,
+ onSave,
+ handleEnable,
+ ...props
+}) => {
+ const classes = useStyles()
+
+ return (
+
+ {elements
+ .slice(0, -2)
+ .map(
+ (
+ { size, className, textAlign, view = it => it?.toString() },
+ idx
+ ) => (
+
+ {view({ ...data, editing: active })}
+
+ )
+ )}
+
+ {data.cashOutDenominations && (
+ rowAction(data)}
+ className={classes.expandButton}>
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+/* rows = [{ columns = [{ name, value, className, textAlign, size }], details, className, error, errorMessage }]
+ * Don't forget to include the size of the last (expand button) column!
+ */
+const CashOutTable = ({
+ elements = [],
+ data = [],
+ Details,
+ className,
+ onSave,
+ handleEditClick,
+ handleEnable,
+ ...props
+}) => {
+ const [active] = useState(null)
+
+ return (
+
+
+ {elements.map(({ size, className, textAlign, header }, idx) => (
+
+ {header}
+
+ ))}
+
+
+
+ {data.map((it, idx) => (
+
+ ))}
+
+
+ )
+}
+
+export default CashOutTable
diff --git a/new-lamassu-admin/src/pages/Cashout/Wizard.js b/new-lamassu-admin/src/pages/Cashout/Wizard.js
new file mode 100644
index 00000000..909eac9c
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Cashout/Wizard.js
@@ -0,0 +1,192 @@
+import React, { useState } from 'react'
+import { makeStyles } from '@material-ui/core'
+
+import { H1, Info2, H4, P } from 'src/components/typography'
+import { Button } from 'src/components/buttons'
+import Stage from 'src/components/Stage'
+import { TextInput } from 'src/components/inputs'
+import ErrorMessage from 'src/components/ErrorMessage'
+import { spacer } from 'src/styling/variables'
+
+const styles = {
+ modalContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: [[24, 32, 0]],
+ '& > h1': {
+ margin: [[0, 0, 10]]
+ },
+ '& > h4': {
+ margin: [[32, 0, 32 - 9, 0]]
+ },
+ '& > p': {
+ margin: 0
+ }
+ },
+ submitButtonWrapper: {
+ display: 'flex',
+ alignSelf: 'flex-end',
+ margin: [['auto', 0, 0]]
+ },
+ submitButton: {
+ width: 67,
+ padding: [[0, 0]],
+ margin: [['auto', 0, 24, 20]],
+ '&:active': {
+ margin: [['auto', 0, 24, 20]]
+ }
+ },
+ stages: {
+ marginTop: 10
+ },
+ texInput: {
+ width: spacer * 6,
+ marginRight: spacer * 2
+ }
+}
+
+const useStyles = makeStyles(styles)
+
+const SubmitButton = ({ error, label, ...props }) => {
+ const classes = useStyles()
+
+ return (
+
+ {error && Failed to save }
+ {label}
+
+ )
+}
+
+const Wizard = ({ pageName, currentStage, handleModalNavigation, machine }) => {
+ const [topOverride, setTopOverride] = useState(
+ machine?.cashOutDenominations?.top
+ )
+ const [bottomOverride, setBottomOverride] = useState(
+ machine?.cashOutDenominations?.bottom
+ )
+
+ const overrideTop = event => {
+ setTopOverride(Number(event.target.value))
+ }
+
+ const overrideBottom = event => {
+ setBottomOverride(Number(event.target.value))
+ }
+
+ const [error, setError] = useState(null)
+ const classes = useStyles()
+
+ const handleNext = machine => event => {
+ const cashOutDenominations = { top: topOverride, bottom: bottomOverride }
+ const nav = handleModalNavigation({ ...machine, cashOutDenominations })(
+ currentStage + 1
+ )
+ nav.catch(error => setError(error))
+ }
+
+ const isSubmittable = currentStage => {
+ switch (currentStage) {
+ case 1:
+ return topOverride > 0
+ case 2:
+ return bottomOverride > 0
+ default:
+ return isSubmittable(1) && isSubmittable(2)
+ }
+ }
+
+ return (
+
+
Enable cash-out
+
{machine.name}
+
+ {currentStage < 3 && (
+ <>
+
{pageName}
+
Choose bill denomination
+ >
+ )}
+
+ {currentStage < 3 && (
+ <>
+ {currentStage === 1 && (
+
+ )}
+
+ {currentStage === 2 && (
+
+ )}
+
+
+ >
+ )}
+ {currentStage === 3 && (
+ <>
+
{pageName}
+
+ When enabling cash out, your bill count will be authomatically set
+ to zero. Make sure you physically put cash inside the cashboxes to
+ allow the machine to dispense it to your users. If you already
+ did, make sure you set the correct cash out bill count for this
+ machine on your Cashboxes tab under Maintenance.
+
+
{pageName}
+
+ When enabling cash out, default commissions will be set. To change
+ commissions for this machine, please go to the Commissions tab
+ under Settings. where you can set exceptions for each of the
+ available cryptocurrencies.
+
+ >
+ )}
+
+
+
+ )
+}
+
+export default Wizard
diff --git a/new-lamassu-admin/src/pages/Cashout/WizardSplash.js b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js
new file mode 100644
index 00000000..d1c2d42e
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import { makeStyles } from '@material-ui/core'
+
+import { H1, P } from 'src/components/typography'
+import { Button } from 'src/components/buttons'
+import { neon, spacer } from 'src/styling/variables'
+
+const styles = {
+ logoWrapper: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 80,
+ margin: [[40, 0, 24]],
+ '& > svg': {
+ maxHeight: '100%',
+ width: '100%'
+ }
+ },
+ modalContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: [[0, 66]],
+ '& > h1': {
+ color: neon,
+ margin: [[spacer * 8, 0, 32]]
+ },
+ '& > p': {
+ margin: 0
+ },
+ '& > button': {
+ margin: [['auto', 0, 56]],
+ '&:active': {
+ margin: [['auto', 0, 56]]
+ }
+ }
+ }
+}
+
+const useStyles = makeStyles(styles)
+
+const WizardSplash = ({ handleModalNavigation, machine }) => {
+ const classes = useStyles()
+
+ return (
+
+
Enable cash-out
+
+ You are about to activate cash-out functionality on your {machine.name}{' '}
+ machine which will allow your customers to sell crypto to you.
+
+
+ In order to activate cash-out for this machine, please enter the
+ denominations for the machine.
+
+
handleModalNavigation(1)}>
+ Start configuration
+
+
+ )
+}
+
+export default WizardSplash
diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js
index 6ee06e6d..4bd1ca65 100644
--- a/new-lamassu-admin/src/routing/routes.js
+++ b/new-lamassu-admin/src/routing/routes.js
@@ -3,6 +3,7 @@ import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom'
import AuthRegister from 'src/pages/AuthRegister'
+import Cashout from 'src/pages/Cashout/Cashout'
import Commissions from 'src/pages/Commissions'
import Customers from 'src/pages/Customers'
import Funding from 'src/pages/Funding'
@@ -67,6 +68,12 @@ const tree = [
return () =>
},
children: [
+ {
+ key: namespaces.CASH_OUT,
+ label: 'Cash-out',
+ route: '/settings/cash-out',
+ component: Cashout
+ },
{
key: namespaces.COMMISSIONS,
label: 'Commissions',
diff --git a/new-lamassu-admin/src/utils/config.js b/new-lamassu-admin/src/utils/config.js
index c6abad75..4b98c793 100644
--- a/new-lamassu-admin/src/utils/config.js
+++ b/new-lamassu-admin/src/utils/config.js
@@ -1,6 +1,7 @@
import * as R from 'ramda'
const namespaces = {
+ CASH_OUT: 'cash-out',
WALLETS: 'wallets',
OPERATOR_INFO: 'operatorInfo',
NOTIFICATIONS: 'notifications',