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:
Mauricio Navarro Miranda 2020-02-22 00:41:41 -06:00
parent 01e330ae98
commit af95a366c6
6 changed files with 642 additions and 0 deletions

View 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

View 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

View 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

View 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

View file

@ -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',

View file

@ -1,6 +1,7 @@
import * as R from 'ramda'
const namespaces = {
CASH_OUT: 'cash-out',
WALLETS: 'wallets',
OPERATOR_INFO: 'operatorInfo',
NOTIFICATIONS: 'notifications',