Merge pull request #389 from mautematico/feat-add-cash-out-screen
feat: add cash out screen
This commit is contained in:
commit
d23067f679
10 changed files with 549 additions and 0 deletions
112
new-lamassu-admin/src/components/Stage.js
Normal file
112
new-lamassu-admin/src/components/Stage.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconSpring } from 'src/styling/icons/stage/spring/current.svg'
|
||||
import { ReactComponent as EmptyStageIconSpring } from 'src/styling/icons/stage/spring/empty.svg'
|
||||
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg'
|
||||
import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg'
|
||||
import {
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
offColor,
|
||||
disabledColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
stages: {
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
margin: 0
|
||||
},
|
||||
stage: {
|
||||
display: 'flex',
|
||||
height: 28,
|
||||
width: 28,
|
||||
zIndex: 2,
|
||||
'& > svg': {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflow: 'visible'
|
||||
}
|
||||
},
|
||||
separator: {
|
||||
width: 28,
|
||||
height: 2,
|
||||
border: [[2, 'solid']],
|
||||
zIndex: 1
|
||||
},
|
||||
separatorSpring: {
|
||||
borderColor: secondaryColor
|
||||
},
|
||||
separatorZodiac: {
|
||||
borderColor: primaryColor
|
||||
},
|
||||
separatorSpringEmpty: {
|
||||
borderColor: disabledColor
|
||||
},
|
||||
separatorZodiacEmpty: {
|
||||
borderColor: offColor
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Stage = memo(({ stages, currentStage, color = 'spring', className }) => {
|
||||
if (currentStage < 1 || currentStage > stages)
|
||||
throw Error('Value of currentStage is invalid')
|
||||
if (stages < 1) throw Error('Value of stages is invalid')
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const separatorClasses = {
|
||||
[classes.separator]: true,
|
||||
[classes.separatorSpring]: color === 'spring',
|
||||
[classes.separatorZodiac]: color === 'zodiac'
|
||||
}
|
||||
|
||||
const separatorEmptyClasses = {
|
||||
[classes.separator]: true,
|
||||
[classes.separatorSpringEmpty]: color === 'spring',
|
||||
[classes.separatorZodiacEmpty]: color === 'zodiac'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(className, classes.stages)}>
|
||||
{R.range(1, currentStage).map(idx => (
|
||||
<div key={idx} className={classes.wrapper}>
|
||||
{idx > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <CompleteStageIconSpring />}
|
||||
{color === 'zodiac' && <CompleteStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={classes.wrapper}>
|
||||
{currentStage > 1 && <div className={classnames(separatorClasses)} />}
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <CurrentStageIconSpring />}
|
||||
{color === 'zodiac' && <CurrentStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
{R.range(currentStage + 1, stages + 1).map(idx => (
|
||||
<div key={idx} className={classes.wrapper}>
|
||||
<div className={classnames(separatorEmptyClasses)} />
|
||||
<div className={classes.stage}>
|
||||
{color === 'spring' && <EmptyStageIconSpring />}
|
||||
{color === 'zodiac' && <EmptyStageIconZodiac />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Stage
|
||||
91
new-lamassu-admin/src/pages/Cashout/Cashout.js
Normal file
91
new-lamassu-admin/src/pages/Cashout/Cashout.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { gql } from 'apollo-boost'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
|
||||
import Wizard from './Wizard'
|
||||
import { DenominationsSchema, getElements } from './helper'
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject, $accounts: [JSONObject]) {
|
||||
saveConfig(config: $config)
|
||||
saveAccounts(accounts: $accounts)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getData {
|
||||
machines {
|
||||
name
|
||||
deviceId
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
}
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const CashOut = ({ name: SCREEN_KEY }) => {
|
||||
const [wizard, setWizard] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const { data } = useQuery(GET_INFO)
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: () => setWizard(false),
|
||||
onError: () => setError(true),
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const save = (rawConfig, accounts) => {
|
||||
const config = toNamespace(SCREEN_KEY)(rawConfig)
|
||||
setError(false)
|
||||
|
||||
return saveConfig({ variables: { config, accounts } })
|
||||
}
|
||||
|
||||
const config = data?.config && fromNamespace(SCREEN_KEY)(data.config)
|
||||
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||
const machines = data?.machines ?? []
|
||||
|
||||
const onToggle = id => {
|
||||
const namespaced = fromNamespace(id)(config)
|
||||
if (!DenominationsSchema.isValidSync(namespaced)) return setWizard(id)
|
||||
save(toNamespace(id, { active: !namespaced?.active }))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Cash-out" error={error} />
|
||||
<EditableTable
|
||||
name="test"
|
||||
namespaces={R.map(R.path(['deviceId']))(machines)}
|
||||
data={config}
|
||||
stripeWhen={it => !DenominationsSchema.isValidSync(it)}
|
||||
enableEdit
|
||||
editWidth={134}
|
||||
enableToggle
|
||||
toggleWidth={109}
|
||||
onToggle={onToggle}
|
||||
save={save}
|
||||
validationSchema={DenominationsSchema}
|
||||
disableRowEdit={R.compose(R.not, R.path(['active']))}
|
||||
elements={getElements(machines, locale)}
|
||||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
machine={R.find(R.propEq('deviceId', wizard))(machines)}
|
||||
onClose={() => setWizard(false)}
|
||||
save={save}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashOut
|
||||
71
new-lamassu-admin/src/pages/Cashout/Wizard.js
Normal file
71
new-lamassu-admin/src/pages/Cashout/Wizard.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { toNamespace } from 'src/utils/config'
|
||||
|
||||
import WizardSplash from './WizardSplash'
|
||||
import WizardStep from './WizardStep'
|
||||
|
||||
const LAST_STEP = 3
|
||||
const MODAL_WIDTH = 554
|
||||
|
||||
const Wizard = ({ machine, onClose, save, error }) => {
|
||||
const [{ step, config }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true }
|
||||
})
|
||||
|
||||
const title = `Enable cash-out`
|
||||
const isLastStep = step === LAST_STEP
|
||||
|
||||
const onContinue = async it => {
|
||||
const newConfig = R.merge(config, it)
|
||||
|
||||
if (isLastStep) {
|
||||
return save(toNamespace(machine.deviceId, newConfig))
|
||||
}
|
||||
|
||||
setState({
|
||||
step: step + 1,
|
||||
config: newConfig
|
||||
})
|
||||
}
|
||||
|
||||
const getStepData = () => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return { type: 'top', display: 'Cassete 1 (Top)' }
|
||||
case 2:
|
||||
return { type: 'bottom', display: 'Cassete 2' }
|
||||
case 3:
|
||||
return { type: 'agreed' }
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={step === 0 ? null : title}
|
||||
handleClose={onClose}
|
||||
width={MODAL_WIDTH}
|
||||
open={true}>
|
||||
{step === 0 && (
|
||||
<WizardSplash name={machine.name} onContinue={() => onContinue()} />
|
||||
)}
|
||||
{step !== 0 && (
|
||||
<WizardStep
|
||||
step={step}
|
||||
name={machine.name}
|
||||
error={error}
|
||||
lastStep={isLastStep}
|
||||
{...getStepData()}
|
||||
onContinue={onContinue}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wizard
|
||||
53
new-lamassu-admin/src/pages/Cashout/WizardSplash.js
Normal file
53
new-lamassu-admin/src/pages/Cashout/WizardSplash.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H1, P } from 'src/components/typography'
|
||||
|
||||
const styles = {
|
||||
logo: {
|
||||
maxHeight: 80,
|
||||
maxWidth: 200
|
||||
},
|
||||
title: {
|
||||
margin: [[24, 0, 32, 0]]
|
||||
},
|
||||
text: {
|
||||
margin: 0
|
||||
},
|
||||
button: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 58
|
||||
},
|
||||
modalContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: [[0, 42]],
|
||||
flex: 1
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const WizardSplash = ({ name, onContinue }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<div className={classes.modalContent}>
|
||||
<H1 className={classes.title}>Enable cash-out</H1>
|
||||
<P className={classes.text}>
|
||||
You are about to activate cash-out functionality on your {name} machine
|
||||
which will allow your customers to sell crypto to you.
|
||||
<br />
|
||||
In order to activate cash-out for this machine, please enter the
|
||||
denominations for the machine.
|
||||
</P>
|
||||
<Button className={classes.button} onClick={onContinue}>
|
||||
Start configuration
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardSplash
|
||||
130
new-lamassu-admin/src/pages/Cashout/WizardStep.js
Normal file
130
new-lamassu-admin/src/pages/Cashout/WizardStep.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { useReducer, useEffect } from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Stepper from 'src/components/Stepper'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs'
|
||||
import { Info2, H4, P } from 'src/components/typography'
|
||||
|
||||
import styles from './WizardStep.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const initialState = {
|
||||
selected: null,
|
||||
iError: false
|
||||
}
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'select':
|
||||
return {
|
||||
form: null,
|
||||
selected: action.selected,
|
||||
isNew: null,
|
||||
iError: false
|
||||
}
|
||||
case 'form':
|
||||
return {
|
||||
form: action.form,
|
||||
selected: action.form.code,
|
||||
isNew: true,
|
||||
iError: false
|
||||
}
|
||||
case 'error':
|
||||
return R.merge(state, { iError: true })
|
||||
case 'reset':
|
||||
return initialState
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
const WizardStep = ({
|
||||
type,
|
||||
name,
|
||||
step,
|
||||
error,
|
||||
lastStep,
|
||||
onContinue,
|
||||
display
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [{ iError, selected }, dispatch] = useReducer(reducer, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'reset' })
|
||||
}, [step])
|
||||
|
||||
const iContinue = config => {
|
||||
if (lastStep) config[type] = true
|
||||
|
||||
if (!config || !config[type]) {
|
||||
return dispatch({ type: 'error' })
|
||||
}
|
||||
|
||||
onContinue(config)
|
||||
}
|
||||
|
||||
const label = lastStep ? 'Finish' : 'Next'
|
||||
const subtitleClass = {
|
||||
[classes.subtitle]: true,
|
||||
[classes.error]: iError
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Info2 className={classes.title}>{name}</Info2>
|
||||
<Stepper steps={3} currentStep={step} />
|
||||
{display && <H4 className={classnames(subtitleClass)}>Edit {display}</H4>}
|
||||
|
||||
{!lastStep && (
|
||||
<TextInput
|
||||
label={'Choose bill denomination'}
|
||||
onChange={evt =>
|
||||
dispatch({ type: 'select', selected: evt.target.value })
|
||||
}
|
||||
autoFocus
|
||||
id="confirm-input"
|
||||
type="text"
|
||||
size="lg"
|
||||
touched={{}}
|
||||
error={false}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
// TODO: there was a disabled link here showing the currency code; restore it
|
||||
)}
|
||||
|
||||
{lastStep && (
|
||||
<>
|
||||
<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>
|
||||
<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 className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button
|
||||
className={classes.button}
|
||||
onClick={() => iContinue({ [type]: selected })}>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardStep
|
||||
26
new-lamassu-admin/src/pages/Cashout/WizardStep.styles.js
Normal file
26
new-lamassu-admin/src/pages/Cashout/WizardStep.styles.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { errorColor } from 'src/styling/variables'
|
||||
|
||||
const LABEL_WIDTH = 150
|
||||
|
||||
export default {
|
||||
title: {
|
||||
margin: [[0, 0, 12, 0]]
|
||||
},
|
||||
subtitle: {
|
||||
margin: [[32, 0, 21, 0]]
|
||||
},
|
||||
error: {
|
||||
color: errorColor
|
||||
},
|
||||
button: {
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
submit: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, 24]]
|
||||
},
|
||||
picker: {
|
||||
width: LABEL_WIDTH
|
||||
}
|
||||
}
|
||||
41
new-lamassu-admin/src/pages/Cashout/helper.js
Normal file
41
new-lamassu-admin/src/pages/Cashout/helper.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import TextInput from 'src/components/inputs/formik/TextInput'
|
||||
|
||||
const DenominationsSchema = Yup.object().shape({
|
||||
top: Yup.number().required('Required'),
|
||||
bottom: Yup.number().required('Required')
|
||||
})
|
||||
|
||||
const getElements = (machines, { fiatCurrency } = {}) => {
|
||||
return [
|
||||
{
|
||||
name: 'id',
|
||||
header: 'Machine',
|
||||
width: 254,
|
||||
view: it => machines.find(({ deviceId }) => deviceId === it).name,
|
||||
size: 'sm',
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
name: 'top',
|
||||
header: 'Cassette 1 (Top)',
|
||||
view: it => `${it} ${fiatCurrency}`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 265,
|
||||
input: TextInput
|
||||
},
|
||||
{
|
||||
name: 'bottom',
|
||||
header: 'Cassette 2',
|
||||
view: it => `${it} ${fiatCurrency}`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 265,
|
||||
input: TextInput
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export { DenominationsSchema, getElements }
|
||||
17
new-lamassu-admin/src/pages/common.styles.js
Normal file
17
new-lamassu-admin/src/pages/common.styles.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
titleWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
titleAndButtonsContainer: {
|
||||
display: 'flex'
|
||||
},
|
||||
iconButton: {
|
||||
border: 'none',
|
||||
outline: 0,
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
}
|
||||
|
|
@ -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, CustomerProfile } from 'src/pages/Customers'
|
||||
import Funding from 'src/pages/Funding'
|
||||
|
|
@ -87,6 +88,12 @@ const tree = [
|
|||
route: '/settings/locale',
|
||||
component: Locales
|
||||
},
|
||||
{
|
||||
key: namespaces.CASH_OUT,
|
||||
label: 'Cash-out',
|
||||
route: '/settings/cash-out',
|
||||
component: Cashout
|
||||
},
|
||||
{
|
||||
key: namespaces.SERVICES,
|
||||
label: '3rd party services',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as R from 'ramda'
|
||||
|
||||
const namespaces = {
|
||||
CASH_OUT: 'denominations',
|
||||
WALLETS: 'wallets',
|
||||
OPERATOR_INFO: 'operatorInfo',
|
||||
NOTIFICATIONS: 'notifications',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue