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:
parent
840788e044
commit
8f4ee4da0a
9 changed files with 498 additions and 6 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
151
new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js
Normal file
151
new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js
Normal 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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
214
new-lamassu-admin/src/pages/maintenance/Cashboxes.js
Normal file
214
new-lamassu-admin/src/pages/maintenance/Cashboxes.js
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue