feat: add cash-out screen
feat: add cash-out route feat: add cash-out table component feat: add cash-out page feat: add wizard splash for enable cashout feat: wizard component for enable cash-out feat: use wizard to enable cash-out fix: denominations are numbers feat: update cashout denominations config on gql feat: refetch cashout infos after config save fix: use default table for cashout table fix: move cashout table closer to parent
This commit is contained in:
parent
01e330ae98
commit
af95a366c6
6 changed files with 642 additions and 0 deletions
246
new-lamassu-admin/src/pages/Cashout/Cashout.js
Normal file
246
new-lamassu-admin/src/pages/Cashout/Cashout.js
Normal file
|
|
@ -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(
|
||||
<WizardSplash
|
||||
handleModalNavigation={handleModalNavigation(machine)}
|
||||
machine={machine}
|
||||
/>
|
||||
)
|
||||
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(
|
||||
<Wizard
|
||||
handleModalNavigation={handleModalNavigation}
|
||||
pageName="Edit Cassette 1 (Top)"
|
||||
machine={machine}
|
||||
currentStage={1}
|
||||
/>
|
||||
)
|
||||
break
|
||||
case 2:
|
||||
setModalContent(
|
||||
<Wizard
|
||||
machine={machine}
|
||||
handleModalNavigation={handleModalNavigation}
|
||||
pageName="Edit Cassette 2"
|
||||
currentStage={2}
|
||||
/>
|
||||
)
|
||||
break
|
||||
case 3:
|
||||
setModalContent(
|
||||
<Wizard
|
||||
machine={machine}
|
||||
handleModalNavigation={handleModalNavigation}
|
||||
pageName="Cashout Bill Count"
|
||||
currentStage={3}
|
||||
/>
|
||||
)
|
||||
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 && (
|
||||
<Info2>
|
||||
{cashOutDenominations.top} {currency.code}
|
||||
</Info2>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Cassette 2',
|
||||
size: 265,
|
||||
textAlign: 'left',
|
||||
view: ({ cashOutDenominations, currency }) => (
|
||||
<>
|
||||
{cashOutDenominations && cashOutDenominations.bottom && (
|
||||
<Info2>
|
||||
{cashOutDenominations.bottom} {currency.code}
|
||||
</Info2>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Edit',
|
||||
size: 265,
|
||||
textAlign: 'left'
|
||||
},
|
||||
{
|
||||
header: 'Enable',
|
||||
size: 151,
|
||||
textAlign: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>Cash-out</Title>
|
||||
</div>
|
||||
<div>
|
||||
<P>
|
||||
Transaction fudge factor <Switch checked={true} /> On{' '}
|
||||
<HelpIcon className={classes.help} />
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
elements={elements}
|
||||
data={machines}
|
||||
handleEnable={handleEnable}
|
||||
handleEditClick={handleEditClick}
|
||||
/>
|
||||
<Modal
|
||||
aria-labelledby="simple-modal-title"
|
||||
aria-describedby="simple-modal-description"
|
||||
open={modalOpen}
|
||||
handleClose={handleModalClose}
|
||||
className={classes.modal}>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cashboxes
|
||||
132
new-lamassu-admin/src/pages/Cashout/CashoutTable.js
Normal file
132
new-lamassu-admin/src/pages/Cashout/CashoutTable.js
Normal file
|
|
@ -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 (
|
||||
<Tr
|
||||
className={classnames(classes.row)}
|
||||
error={data.error}
|
||||
errorMessage={data.errorMessage}>
|
||||
{elements
|
||||
.slice(0, -2)
|
||||
.map(
|
||||
(
|
||||
{ size, className, textAlign, view = it => it?.toString() },
|
||||
idx
|
||||
) => (
|
||||
<Td
|
||||
key={idx}
|
||||
size={size}
|
||||
className={className}
|
||||
textAlign={textAlign}>
|
||||
{view({ ...data, editing: active })}
|
||||
</Td>
|
||||
)
|
||||
)}
|
||||
<Td
|
||||
size={elements[elements.length - 2].size}
|
||||
textAlign={elements[elements.length - 2].textAlign}>
|
||||
{data.cashOutDenominations && (
|
||||
<button
|
||||
onClick={() => rowAction(data)}
|
||||
className={classes.expandButton}>
|
||||
<EditIcon />
|
||||
</button>
|
||||
)}
|
||||
</Td>
|
||||
<Td
|
||||
size={elements[elements.length - 1].size}
|
||||
textAlign={elements[elements.length - 1].textAlign}>
|
||||
<Switch
|
||||
checked={data.cashOutDenominations}
|
||||
onChange={handleEnable(data)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
}
|
||||
|
||||
/* 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 (
|
||||
<Table>
|
||||
<THead>
|
||||
{elements.map(({ size, className, textAlign, header }, idx) => (
|
||||
<Th key={idx} size={size} className={className} textAlign={textAlign}>
|
||||
{header}
|
||||
</Th>
|
||||
))}
|
||||
</THead>
|
||||
|
||||
<TBody>
|
||||
{data.map((it, idx) => (
|
||||
<Row
|
||||
id={idx}
|
||||
elements={elements}
|
||||
data={it}
|
||||
active={idx === active}
|
||||
rowAction={handleEditClick}
|
||||
onSave={onSave}
|
||||
handleEnable={handleEnable}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashOutTable
|
||||
192
new-lamassu-admin/src/pages/Cashout/Wizard.js
Normal file
192
new-lamassu-admin/src/pages/Cashout/Wizard.js
Normal file
|
|
@ -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 (
|
||||
<div className={classes.submitButtonWrapper}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button {...props}>{label}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classes.modalContent}>
|
||||
<H1>Enable cash-out</H1>
|
||||
<Info2>{machine.name}</Info2>
|
||||
<Stage
|
||||
stages={3}
|
||||
currentStage={currentStage}
|
||||
color="spring"
|
||||
className={classes.stages}
|
||||
/>
|
||||
{currentStage < 3 && (
|
||||
<>
|
||||
<H4>{pageName}</H4>
|
||||
<P>Choose bill denomination</P>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
{currentStage < 3 && (
|
||||
<>
|
||||
{currentStage === 1 && (
|
||||
<TextInput
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
large
|
||||
value={topOverride}
|
||||
touched={{}}
|
||||
error={false}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={overrideTop}
|
||||
className={classes.texInput}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStage === 2 && (
|
||||
<TextInput
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
large
|
||||
value={bottomOverride}
|
||||
touched={{}}
|
||||
error={false}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={overrideBottom}
|
||||
className={classes.texInput}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
disabled
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
large
|
||||
value={machine.currency.code}
|
||||
touched={{}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
className={classes.texInput}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{currentStage === 3 && (
|
||||
<>
|
||||
<H4>{pageName}</H4>
|
||||
<P>
|
||||
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.
|
||||
</P>
|
||||
<H4>{pageName}</H4>
|
||||
<P>
|
||||
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.
|
||||
</P>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SubmitButton
|
||||
className={classes.submitButton}
|
||||
label={currentStage === 3 ? 'Finish' : 'Next'}
|
||||
disabled={!isSubmittable(currentStage)}
|
||||
onClick={handleNext(machine)}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wizard
|
||||
64
new-lamassu-admin/src/pages/Cashout/WizardSplash.js
Normal file
64
new-lamassu-admin/src/pages/Cashout/WizardSplash.js
Normal file
|
|
@ -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 (
|
||||
<div className={classes.modalContent}>
|
||||
<H1>Enable cash-out</H1>
|
||||
<P>
|
||||
You are about to activate cash-out functionality on your {machine.name}{' '}
|
||||
machine which will allow your customers to sell crypto to you.
|
||||
<br />
|
||||
<br />
|
||||
In order to activate cash-out for this machine, please enter the
|
||||
denominations for the machine.
|
||||
</P>
|
||||
<Button onClick={() => handleModalNavigation(1)}>
|
||||
Start configuration
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardSplash
|
||||
|
|
@ -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 () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: namespaces.CASH_OUT,
|
||||
label: 'Cash-out',
|
||||
route: '/settings/cash-out',
|
||||
component: Cashout
|
||||
},
|
||||
{
|
||||
key: namespaces.COMMISSIONS,
|
||||
label: 'Commissions',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as R from 'ramda'
|
||||
|
||||
const namespaces = {
|
||||
CASH_OUT: 'cash-out',
|
||||
WALLETS: 'wallets',
|
||||
OPERATOR_INFO: 'operatorInfo',
|
||||
NOTIFICATIONS: 'notifications',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue