feat: add cashboxes screen

feat: add cashboxes route
feat: add non-editable cashbox component
feat: cashboxes action required icon
feat: add cashOut denomination to cashboxes
feat: edit cashboxes values
feat: new server empty cashIn and reset cashOut actions
feat: reset cashboxes from UI
fix: cashbox border, cashbox font
fix: move cashbox styles to its own file
fix: use default table for cashboxes-table
fix: better import
fix: TODO: find a better way to display cashbox reset errors
fix: TODO for cashout
fix: move cashboxestable closer to parent
fix: WIP use EditableTable instead of fakatable
wip: move to editabletable
fix: WIP split cashbox into view + input components that can be used with formik
feat: rewrite cashbox component into view + fromik
feat: WIP use editableTable instead of hand made table
feat: WIP cashboxes editable table
feat: split cashbox
feat: Yup validation schema for cashboxes editable table
feat: split cashbox into view+formik
feat: WIP use editableTable instead of faketable
feat: use editableTable instead of fakeTable
fix: custom CashboxesTable not needed anymore
This commit is contained in:
Mauricio Navarro Miranda 2020-02-06 01:47:55 -06:00
parent 840788e044
commit 8f4ee4da0a
9 changed files with 498 additions and 6 deletions

View file

@ -75,6 +75,11 @@ function resetCashOutBills (rec) {
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
}
function emptyCashInBills (rec) {
const sql = 'update devices set cashbox=0 where device_id=$1'
return db.none(sql, [rec.deviceId])
}
function unpair (rec) {
return pairing.unpair(rec.deviceId)
}
@ -89,6 +94,7 @@ function restartServices (rec) {
function setMachine (rec) {
switch (rec.action) {
case 'emptyCashInBills': return emptyCashInBills(rec)
case 'resetCashOutBills': return resetCashOutBills(rec)
case 'unpair': return unpair(rec)
case 'reboot': return reboot(rec)

View file

@ -177,6 +177,7 @@ const typeDefs = gql`
}
enum MachineAction {
emptyCashInBills
resetCashOutBills
unpair
reboot
@ -184,7 +185,7 @@ const typeDefs = gql`
}
type Mutation {
machineAction(deviceId:ID!, action: MachineAction!): Machine
machineAction(deviceId:ID!, action: MachineAction!, cassettes: [Int]): Machine
machineSupportLogs(deviceId: ID!): SupportLogsResponse
serverSupportLogs: SupportLogsResponse
saveConfig(config: JSONObject): JSONObject
@ -219,7 +220,7 @@ const resolvers = {
accounts: () => settingsLoader.getAccounts()
},
Mutation: {
machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }),
machineAction: (...[, { deviceId, action, cassettes }]) => machineAction({ deviceId, action, cassettes }),
machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId),
createPairingTotem: (...[, { name }]) => pairing.totem(name),
serverSupportLogs: () => serverLogs.insert(),

View file

@ -6,13 +6,13 @@ function getMachine (machineId) {
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
}
function machineAction ({ deviceId, action }) {
function machineAction ({ deviceId, action, cassettes }) {
return getMachine(deviceId)
.then(machine => {
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
return machine
})
.then(machineLoader.setMachine({ deviceId, action }))
.then(machineLoader.setMachine({ deviceId, action, cassettes }))
.then(getMachine(deviceId))
}

View file

@ -0,0 +1,151 @@
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import Chip from 'src/components/Chip'
import { Link } from 'src/components/buttons'
import { Info2, Label1, Label2 } from 'src/components/typography'
import TextInputFormik from '../base/TextInput'
import { cashboxStyles, gridStyles } from './Cashbox.styles'
const cashboxClasses = makeStyles(cashboxStyles)
const gridClasses = makeStyles(gridStyles)
const Cashbox = ({ percent = 0, cashOut = false }) => {
const classes = cashboxClasses({ percent, cashOut })
return (
<div className={classes.cashbox}>
<div className={classes.emptyPart}>
{percent <= 50 && <Label2>{percent.toFixed(0)}%</Label2>}
</div>
<div className={classes.fullPart}>
{percent > 50 && <Label2>{percent.toFixed(0)}%</Label2>}
</div>
</div>
)
}
// https://support.lamassu.is/hc/en-us/articles/360025595552-Installing-the-Sintra-Forte
// Sintra and Sintra Forte can have up to 500 notes per cashOut box and up to 1000 per cashIn box
const CashIn = ({ capacity = 1000, notes = 0, total = 0 }) => {
const percent = (100 * notes) / capacity
const classes = gridClasses()
return (
<>
<div className={classes.row}>
<div>
<Cashbox percent={percent} />
</div>
<div className={classes.col2}>
<div>
<Info2 className={classes.noMarginText}>{notes} notes</Info2>
<Label1 className={classes.noMarginText}>{total}</Label1>
</div>
</div>
</div>
</>
)
}
const CashInFormik = ({
capacity = 1000,
onEmpty,
field: {
value: { notes, deviceId }
},
form: { setFieldValue }
}) => {
const classes = gridClasses()
return (
<>
<div className={classes.row}>
<div>
<Cashbox percent={(100 * notes) / capacity} />
</div>
<div className={classes.col2}>
<div>
<Link
onClick={() => {
onEmpty({
variables: {
deviceId,
action: 'emptyCashInBills'
}
}).then(() => setFieldValue('cashin.notes', 0))
}}
className={classes.link}
color={'primary'}>
Empty
</Link>
</div>
</div>
</div>
</>
)
}
const CashOut = ({ capacity = 500, denomination = 0, currency, notes }) => {
const percent = (100 * notes) / capacity
const classes = gridClasses()
return (
<>
<div className={classes.row}>
<div className={classes.col}>
<Cashbox percent={percent} cashOut />
</div>
<div className={(classes.col, classes.col2)}>
<div>
<Info2 className={classes.noMarginText}>
{notes} <Chip label={`${denomination} ${currency.code}`} />
</Info2>
<Label1 className={classes.noMarginText}>
{notes * denomination} {currency.code}
</Label1>
</div>
</div>
</div>
</>
)
}
const CashOutFormik = ({ capacity = 500, ...props }) => {
const {
name,
onChange,
onBlur,
value: { notes }
} = props.field
const { touched, errors } = props.form
const error = !!(touched[name] && errors[name])
const percent = (100 * notes) / capacity
const classes = gridClasses()
return (
<>
<div className={classes.row}>
<div className={classes.col}>
<Cashbox percent={percent} cashOut />
</div>
<div className={(classes.col, classes.col2)}>
<div>
<TextInputFormik
fullWidth
name={name + '.notes'}
onChange={onChange}
onBlur={onBlur}
value={notes}
error={error}
{...props}
/>
</div>
</div>
</div>
</>
)
}
export { Cashbox, CashIn, CashInFormik, CashOut, CashOutFormik }

View file

@ -0,0 +1,67 @@
import { spacer, tomato, primaryColor as zodiac } from 'src/styling/variables'
const colors = {
cashOut: {
empty: tomato,
full: zodiac
},
cashIn: {
empty: zodiac,
full: tomato
}
}
const colorPicker = ({ percent, cashOut }) =>
colors[cashOut ? 'cashOut' : 'cashIn'][percent >= 50 ? 'full' : 'empty']
const cashboxStyles = {
cashbox: {
borderColor: colorPicker,
backgroundColor: colorPicker,
height: 34,
width: 80,
border: '2px solid',
textAlign: 'end',
display: 'inline-block'
},
emptyPart: {
backgroundColor: 'white',
height: ({ percent }) => `${100 - percent}%`,
'& > p': {
color: colorPicker,
display: 'inline-block'
}
},
fullPart: {
backgroundColor: colorPicker,
height: ({ percent }) => `${percent}%`,
'& > p': {
color: 'white',
display: 'inline'
}
}
}
const gridStyles = {
row: {
height: 36,
width: 183,
display: 'grid',
gridTemplateColumns: 'repeat(2,1fr)',
gridTemplateRows: '1fr',
gridColumnGap: 18,
gridRowGap: 0
},
col2: {
width: 117
},
noMarginText: {
marginTop: 0,
marginBottom: 0
},
link: {
marginTop: spacer
}
}
export { cashboxStyles, gridStyles }

View file

@ -4,5 +4,15 @@ import RadioGroup from './base/RadioGroup'
import Select from './base/Select'
import Switch from './base/Switch'
import TextInput from './base/TextInput'
import { CashIn, CashOut } from './cashbox/Cashbox'
export { Autocomplete, TextInput, Checkbox, Switch, Select, RadioGroup }
export {
Autocomplete,
TextInput,
Checkbox,
Switch,
Select,
RadioGroup,
CashIn,
CashOut
}

View file

@ -0,0 +1,214 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import { gql } from 'apollo-boost'
import React, { useState } from 'react'
import * as Yup from 'yup'
import Title from 'src/components/Title'
import { Table as EditableTable } from 'src/components/editableTable'
import {
CashIn,
CashOut,
CashOutFormik,
CashInFormik
} from 'src/components/inputs/cashbox/Cashbox'
import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as ErrorIcon } from 'src/styling/icons/status/tomato.svg'
const ValidationSchema = Yup.object().shape({
name: Yup.string().required('Required'),
cashin: Yup.object()
.required('Required')
.shape({
notes: Yup.number()
.required('Required')
.integer()
.min(0)
}),
cashout1: Yup.object()
.required('Required')
.shape({
notes: Yup.number()
.required('Required')
.integer()
.min(0),
denomination: Yup.number()
.required('Required')
.integer()
}),
cashout2: Yup.object()
.required('Required')
.shape({
notes: Yup.number()
.required('Required')
.integer()
.min(0),
denomination: Yup.number()
.required('Required')
.integer()
})
})
const GET_MACHINES_AND_CONFIG = gql`
{
machines {
name
deviceId
cashbox
cassette1
cassette2
}
config
}
`
const EMPTY_CASHIN_BILLS = gql`
mutation MachineAction($deviceId: ID!, $action: MachineAction!) {
machineAction(deviceId: $deviceId, action: $action) {
deviceId
cashbox
cassette1
cassette2
}
}
`
const RESET_CASHOUT_BILLS = gql`
mutation MachineAction(
$deviceId: ID!
$action: MachineAction!
$cassettes: [Int]!
) {
machineAction(deviceId: $deviceId, action: $action, cassettes: $cassettes) {
deviceId
cashbox
cassette1
cassette2
}
}
`
const useStyles = makeStyles(mainStyles)
const Cashboxes = () => {
const [machines, setMachines] = useState([])
const classes = useStyles()
useQuery(GET_MACHINES_AND_CONFIG, {
onCompleted: data =>
setMachines(
data.machines.map(m => ({
...m,
currency: data.config.fiatCurrency ?? { code: 'N/D' },
denominations: (data.config.cashOutDenominations ?? {})[m.deviceId]
}))
)
})
const [resetCashOut] = useMutation(RESET_CASHOUT_BILLS, {
onError: ({ graphQLErrors, message }) => {
const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message
// TODO: this should not be final
alert(JSON.stringify(errorMessage))
}
})
const [onEmpty] = useMutation(EMPTY_CASHIN_BILLS, {
onError: ({ graphQLErrors, message }) => {
const errorMessage = graphQLErrors[0] ? graphQLErrors[0].message : message
// TODO: this should not be final
alert(JSON.stringify(errorMessage))
}
})
const onSave = ({ cashin, cashout1, cashout2 }, { setSubmitting }) =>
resetCashOut({
variables: {
deviceId: cashin.deviceId,
action: 'resetCashOutBills',
cassettes: [Number(cashout1.notes), Number(cashout2.notes)]
}
}).then(() => setSubmitting(false))
const elements = [
{
name: 'name',
header: 'Machine',
size: 254,
textAlign: 'left',
view: name => <>{name}</>,
input: ({ field: { value: name } }) => <>{name}</>
},
{
name: 'cashin',
header: 'Cash-in',
size: 265,
textAlign: 'left',
view: props => <CashIn {...props} />,
input: props => <CashInFormik onEmpty={onEmpty} {...props} />
},
{
name: 'cashout1',
header: 'Cash-out 1',
size: 265,
textAlign: 'left',
view: props => <CashOut {...props} />,
input: CashOutFormik
},
{
name: 'cashout2',
header: 'Cash-out 2',
size: 265,
textAlign: 'left',
view: props => <CashOut {...props} />,
input: CashOutFormik
},
{
name: 'edit',
header: 'Update',
size: 151,
textAlign: 'right',
view: onclick => <EditIcon onClick={onclick} />
}
]
const data = machines.map(
({
name,
cassette1,
cassette2,
currency,
denominations: { top, bottom },
cashbox,
deviceId
}) => ({
name,
cashin: { notes: cashbox, deviceId },
cashout1: { notes: cassette1, denomination: top, currency },
cashout2: { notes: cassette2, denomination: bottom, currency }
})
)
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Cashboxes</Title>
</div>
<div className={classes.headerLabels}>
<ErrorIcon />
<span>Action required</span>
</div>
</div>
<EditableTable
elements={elements}
data={data}
save={onSave}
validationSchema={ValidationSchema}
/>
</>
)
}
export default Cashboxes

View file

@ -18,6 +18,8 @@ import WalletSettings from 'src/pages/Wallet/Wallet'
import MachineStatus from 'src/pages/maintenance/MachineStatus'
import { namespaces } from 'src/utils/config'
import Cashboxes from '../pages/maintenance/Cashboxes'
const tree = [
{
key: 'transactions',
@ -56,6 +58,12 @@ const tree = [
label: 'Machine Status',
route: '/maintenance/machine-status',
component: MachineStatus
},
{
key: 'cashboxes',
label: 'Cashboxes',
route: '/maintenance/cashboxes',
component: Cashboxes
}
]
},

View file

@ -13,7 +13,13 @@ import extendJss from 'jss-plugin-extend'
import React from 'react'
import { ActionButton, Button, Link } from 'src/components/buttons'
import { TextInput, Switch } from 'src/components/inputs'
import {
Radio,
TextInput,
Switch,
CashIn,
CashOut
} from 'src/components/inputs'
import { ReactComponent as AuthorizeIconReversed } from 'src/styling/icons/button/authorize/white.svg'
import { ReactComponent as AuthorizeIcon } from 'src/styling/icons/button/authorize/zodiac.svg'
@ -178,6 +184,35 @@ story.add('ConfirmDialog', () => (
</Wrapper>
))
story.add('Cashbox', () => (
<Wrapper>
<div>
<CashIn percent={0} notes={0} />
<hr />
<CashIn percent={49} notes={19} />
<hr />
<CashIn percent={50} notes={20} />
<hr />
<CashIn percent={51} notes={22} />
<hr />
<CashIn percent={99} notes={39} />
<hr />
</div>
<div>
<CashOut percent={0} notes={0} denomination={20} />
<hr />
<CashOut percent={49} notes={19} denomination={20} />
<hr />
<CashOut percent={50} notes={20} denomination={20} />
<hr />
<CashOut percent={51.00001} notes={22} denomination={20} />
</div>
</Wrapper>
))
story.add('Radio', () => <Radio label="Hehe" />)
const typographyStory = storiesOf('Typography', module)
typographyStory.add('H1', () => <H1>Hehehe</H1>)