From 8f4ee4da0a9f4bfb77874ecec746b47f6e44b054 Mon Sep 17 00:00:00 2001 From: Mauricio Navarro Miranda Date: Thu, 6 Feb 2020 01:47:55 -0600 Subject: [PATCH] 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 --- lib/machine-loader.js | 6 + lib/new-admin/graphql/schema.js | 5 +- lib/new-admin/machines.js | 4 +- .../src/components/inputs/cashbox/Cashbox.js | 151 ++++++++++++ .../inputs/cashbox/Cashbox.styles.js | 67 ++++++ .../src/components/inputs/index.js | 12 +- .../src/pages/maintenance/Cashboxes.js | 214 ++++++++++++++++++ new-lamassu-admin/src/routing/routes.js | 8 + new-lamassu-admin/src/stories/index.js | 37 ++- 9 files changed, 498 insertions(+), 6 deletions(-) create mode 100644 new-lamassu-admin/src/components/inputs/cashbox/Cashbox.js create mode 100644 new-lamassu-admin/src/components/inputs/cashbox/Cashbox.styles.js create mode 100644 new-lamassu-admin/src/pages/maintenance/Cashboxes.js 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 24feee74..41f84d5c 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -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(), 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/pages/maintenance/Cashboxes.js b/new-lamassu-admin/src/pages/maintenance/Cashboxes.js new file mode 100644 index 00000000..96714461 --- /dev/null +++ b/new-lamassu-admin/src/pages/maintenance/Cashboxes.js @@ -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 => , + input: props => + }, + { + name: 'cashout1', + header: 'Cash-out 1', + size: 265, + textAlign: 'left', + view: props => , + input: CashOutFormik + }, + { + name: 'cashout2', + header: 'Cash-out 2', + size: 265, + textAlign: 'left', + view: props => , + input: CashOutFormik + }, + { + name: 'edit', + header: 'Update', + size: 151, + textAlign: 'right', + view: 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 ( + <> +
+
+ Cashboxes +
+
+ + Action required +
+
+ + + ) +} + +export default Cashboxes diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 6ee06e6d..29ba5857 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

)