diff --git a/lib/machine-loader.js b/lib/machine-loader.js
index 8f4c9d85..4bce608b 100644
--- a/lib/machine-loader.js
+++ b/lib/machine-loader.js
@@ -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)
diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js
index 355d820d..a59fa041 100644
--- a/lib/new-admin/graphql/schema.js
+++ b/lib/new-admin/graphql/schema.js
@@ -207,6 +207,7 @@ const typeDefs = gql`
}
enum MachineAction {
+ emptyCashInBills
resetCashOutBills
unpair
reboot
@@ -214,7 +215,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
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer
@@ -254,7 +255,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(),
diff --git a/lib/new-admin/machines.js b/lib/new-admin/machines.js
index 7f49956f..cb0924ff 100644
--- a/lib/new-admin/machines.js
+++ b/lib/new-admin/machines.js
@@ -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))
}
diff --git a/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js b/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js
new file mode 100644
index 00000000..91454e78
--- /dev/null
+++ b/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js
@@ -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 (
+
+
+ {percent <= 50 && {percent.toFixed(0)}%}
+
+
+ {percent > 50 && {percent.toFixed(0)}%}
+
+
+ )
+}
+
+// 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 (
+ <>
+
+
+
+
+
+
+ {notes} notes
+ {total}
+
+
+
+ >
+ )
+}
+
+const CashInFormik = ({
+ capacity = 1000,
+ onEmpty,
+ field: {
+ value: { notes, deviceId }
+ },
+ form: { setFieldValue }
+}) => {
+ const classes = gridClasses()
+
+ return (
+ <>
+
+
+
+
+
+
+ {
+ onEmpty({
+ variables: {
+ deviceId,
+ action: 'emptyCashInBills'
+ }
+ }).then(() => setFieldValue('cashin.notes', 0))
+ }}
+ className={classes.link}
+ color={'primary'}>
+ Empty
+
+
+
+
+ >
+ )
+}
+
+const CashOut = ({ capacity = 500, denomination = 0, currency, notes }) => {
+ const percent = (100 * notes) / capacity
+ const classes = gridClasses()
+ return (
+ <>
+
+
+
+
+
+
+
+ {notes}
+
+
+ {notes * denomination} {currency.code}
+
+
+
+
+ >
+ )
+}
+
+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 (
+ <>
+
+ >
+ )
+}
+
+export { Cashbox, CashIn, CashInFormik, CashOut, CashOutFormik }
diff --git a/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.styles.js b/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.styles.js
new file mode 100644
index 00000000..d87ee123
--- /dev/null
+++ b/new-lamassu-admin/src/components/inputs/cashbox/Cashbox.styles.js
@@ -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 }
diff --git a/new-lamassu-admin/src/components/inputs/index.js b/new-lamassu-admin/src/components/inputs/index.js
index 966f52e7..59c95f8f 100644
--- a/new-lamassu-admin/src/components/inputs/index.js
+++ b/new-lamassu-admin/src/components/inputs/index.js
@@ -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
+}
diff --git a/new-lamassu-admin/src/components/layout/TitleSection.js b/new-lamassu-admin/src/components/layout/TitleSection.js
index 75db7d89..019a6ba5 100644
--- a/new-lamassu-admin/src/components/layout/TitleSection.js
+++ b/new-lamassu-admin/src/components/layout/TitleSection.js
@@ -8,7 +8,7 @@ import styles from './TitleSection.styles'
const useStyles = makeStyles(styles)
-const TitleSection = ({ title, error }) => {
+const TitleSection = ({ title, error, labels }) => {
const classes = useStyles()
return (
@@ -18,6 +18,7 @@ const TitleSection = ({ title, error }) => {
Failed to save
)}
+ {labels}
)
}
diff --git a/new-lamassu-admin/src/components/layout/TitleSection.styles.js b/new-lamassu-admin/src/components/layout/TitleSection.styles.js
index 7a52e077..397ece50 100644
--- a/new-lamassu-admin/src/components/layout/TitleSection.styles.js
+++ b/new-lamassu-admin/src/components/layout/TitleSection.styles.js
@@ -1,3 +1,5 @@
+import { mainStyles } from 'src/pages/Transactions/Transactions.styles'
+
export default {
titleWrapper: {
display: 'flex',
@@ -10,5 +12,6 @@ export default {
},
error: {
marginLeft: 12
- }
+ },
+ headerLabels: mainStyles.headerLabels
}
diff --git a/new-lamassu-admin/src/pages/maintenance/Cashboxes.js b/new-lamassu-admin/src/pages/maintenance/Cashboxes.js
new file mode 100644
index 00000000..214f2ab2
--- /dev/null
+++ b/new-lamassu-admin/src/pages/maintenance/Cashboxes.js
@@ -0,0 +1,210 @@
+import { useQuery, useMutation } from '@apollo/react-hooks'
+import { gql } from 'apollo-boost'
+import React, { useState } from 'react'
+import * as Yup from 'yup'
+
+import { Table as EditableTable } from 'src/components/editableTable'
+import {
+ CashIn,
+ CashOut,
+ CashOutFormik,
+ CashInFormik
+} from 'src/components/inputs/cashbox/Cashbox'
+import TitleSection from 'src/components/layout/TitleSection'
+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()
+ .default(0)
+ }),
+ cashout2: Yup.object()
+ .required('Required')
+ .shape({
+ notes: Yup.number()
+ .required('Required')
+ .integer()
+ .min(0),
+ denomination: Yup.number()
+ .required('Required')
+ .integer()
+ .default(0)
+ })
+})
+
+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 Cashboxes = () => {
+ const [machines, setMachines] = useState([])
+
+ useQuery(GET_MACHINES_AND_CONFIG, {
+ onCompleted: ({ machines, config }) =>
+ setMachines(
+ machines.map(m => ({
+ ...m,
+ currency: { code: config.locale_fiatCurrency ?? '' },
+ denominations: {
+ top: config[`denominations_${m.deviceId}_top`],
+ bottom: config[`denominations_${m.deviceId}_bottom`]
+ }
+ }))
+ )
+ })
+
+ 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 }) =>
+ resetCashOut({
+ variables: {
+ deviceId: cashin.deviceId,
+ action: 'resetCashOutBills',
+ cassettes: [Number(cashout1.notes), Number(cashout2.notes)]
+ }
+ })
+
+ const elements = [
+ {
+ name: 'name',
+ header: 'Machine',
+ width: 254,
+ textAlign: 'left',
+ view: name => <>{name}>,
+ input: ({ field: { value: name } }) => <>{name}>
+ },
+ {
+ name: 'cashin',
+ header: 'Cash-in',
+ width: 265,
+ textAlign: 'left',
+ view: props => ,
+ input: props =>
+ },
+ {
+ name: 'cashout1',
+ header: 'Cash-out 1',
+ width: 265,
+ textAlign: 'left',
+ view: props => ,
+ input: CashOutFormik
+ },
+ {
+ name: 'cashout2',
+ header: 'Cash-out 2',
+ width: 265,
+ textAlign: 'left',
+ view: props => ,
+ input: CashOutFormik
+ }
+ ]
+
+ const data = machines.map(
+ ({
+ name,
+ cassette1,
+ cassette2,
+ currency,
+ denominations: { top, bottom },
+ cashbox,
+ deviceId
+ }) => ({
+ id: deviceId,
+ name,
+ cashin: { notes: cashbox, deviceId },
+ cashout1: { notes: cassette1, denomination: top, currency },
+ cashout2: { notes: cassette2, denomination: bottom, currency }
+ })
+ )
+
+ return (
+ <>
+
+
+ Action required
+ >
+ }
+ />
+
+
+ >
+ )
+}
+
+export default Cashboxes
diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js
index 1acd9be0..28ce1b60 100644
--- a/new-lamassu-admin/src/routing/routes.js
+++ b/new-lamassu-admin/src/routing/routes.js
@@ -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
}
]
},
diff --git a/new-lamassu-admin/src/stories/index.js b/new-lamassu-admin/src/stories/index.js
index f7aba47c..e30ee344 100644
--- a/new-lamassu-admin/src/stories/index.js
+++ b/new-lamassu-admin/src/stories/index.js
@@ -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', () => (
))
+story.add('Cashbox', () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+))
+
+story.add('Radio', () => )
+
const typographyStory = storiesOf('Typography', module)
typographyStory.add('H1', () => Hehehe
)