diff --git a/new-lamassu-admin/src/pages/Cashout/Cashout.js b/new-lamassu-admin/src/pages/Cashout/Cashout.js index 331e6642..5a82d083 100644 --- a/new-lamassu-admin/src/pages/Cashout/Cashout.js +++ b/new-lamassu-admin/src/pages/Cashout/Cashout.js @@ -1,23 +1,24 @@ import { useQuery, useMutation } from '@apollo/react-hooks' -import { makeStyles } from '@material-ui/core' import { gql } from 'apollo-boost' +import * as R from 'ramda' 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 { NamespacedTable as EditableTable } from 'src/components/editableTable' +import TitleSection from 'src/components/layout/TitleSection' +import { fromNamespace, toNamespace } from 'src/utils/config' -import Table from './CashoutTable' import Wizard from './Wizard' -import WizardSplash from './WizardSplash' +import { DenominationsSchema, getElements } from './helper' -const GET_MACHINES_AND_CONFIG = gql` - { +const SAVE_CONFIG = gql` + mutation Save($config: JSONObject, $accounts: [JSONObject]) { + saveConfig(config: $config) + saveAccounts(accounts: $accounts) + } +` + +const GET_INFO = gql` + query getData { machines { name deviceId @@ -29,218 +30,62 @@ const GET_MACHINES_AND_CONFIG = gql` } ` -const SAVE_CONFIG = gql` - mutation Save($config: JSONObject) { - saveConfig(config: $config) - } -` +const CashOut = ({ name: SCREEN_KEY }) => { + const [wizard, setWizard] = useState(false) + const [error, setError] = useState(false) + const { data } = useQuery(GET_INFO) -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, { + onCompleted: () => setWizard(false), + onError: () => setError(true), + refetchQueries: () => ['getData'] }) - const [saveConfig] = useMutation(SAVE_CONFIG) + const save = (rawConfig, accounts) => { + const config = toNamespace(SCREEN_KEY)(rawConfig) + setError(false) - const saveCashoutConfig = machine => - saveConfig({ - variables: { - config: { - ...config, - cashOutDenominations: { - ...config.cashOutDenominations, - [machine.deviceId]: machine.cashOutDenominations - } - } - } - }) - - const handleEnable = machine => event => { - setModalContent( - - ) - setModalOpen(true) + return saveConfig({ variables: { config, accounts } }) } - const handleEditClick = row => { - setModalOpen(true) - handleModalNavigation(row)(1) + const config = data?.config && fromNamespace(SCREEN_KEY)(data.config) + const locale = data?.config && fromNamespace('locale')(data.config) + const machines = data?.machines ?? [] + + const onToggle = id => { + const namespaced = fromNamespace(id)(config) + if (!DenominationsSchema.isValidSync(namespaced)) return setWizard(id) + save(toNamespace(id, { active: !namespaced?.active })) } - const handleModalClose = () => { - setModalOpen(false) - setModalContent(null) - } - - const handleModalNavigation = machine => currentPage => { - switch (currentPage) { - case 1: - setModalContent( - - ) - break - case 2: - setModalContent( - - ) - break - case 3: - setModalContent( - - ) - 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 && ( - - {cashOutDenominations.top} {currency.code} - - )} - - ) - }, - { - header: 'Cassette 2', - size: 265, - textAlign: 'left', - view: ({ cashOutDenominations, currency }) => ( - <> - {cashOutDenominations && cashOutDenominations.bottom && ( - - {cashOutDenominations.bottom} {currency.code} - - )} - - ) - }, - { - header: 'Edit', - size: 265, - textAlign: 'left' - }, - { - header: 'Enable', - size: 151, - textAlign: 'right' - } - ] - return ( <> -
-
- Cash-out -
-
-

- Transaction fudge factor On{' '} - -

-
-
- + !DenominationsSchema.isValidSync(it)} + enableEdit + editWidth={134} + enableToggle + toggleWidth={109} + onToggle={onToggle} + save={save} + validationSchema={DenominationsSchema} + disableRowEdit={R.compose(R.not, R.path(['active']))} + elements={getElements(machines, locale)} /> - - {modalContent} - + {wizard && ( + setWizard(false)} + save={save} + error={error} + /> + )} ) } -export default Cashboxes +export default CashOut diff --git a/new-lamassu-admin/src/pages/Cashout/CashoutTable.js b/new-lamassu-admin/src/pages/Cashout/CashoutTable.js deleted file mode 100644 index b5b607c3..00000000 --- a/new-lamassu-admin/src/pages/Cashout/CashoutTable.js +++ /dev/null @@ -1,132 +0,0 @@ -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 ( - - {elements - .slice(0, -2) - .map( - ( - { size, className, textAlign, view = it => it?.toString() }, - idx - ) => ( - - ) - )} - - - - ) -} - -/* 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 ( -
- {view({ ...data, editing: active })} - - {data.cashOutDenominations && ( - - )} - - -
- - {elements.map(({ size, className, textAlign, header }, idx) => ( - - ))} - - - - {data.map((it, idx) => ( - - ))} - -
- {header} -
- ) -} - -export default CashOutTable diff --git a/new-lamassu-admin/src/pages/Cashout/Wizard.js b/new-lamassu-admin/src/pages/Cashout/Wizard.js index 909eac9c..a49576b3 100644 --- a/new-lamassu-admin/src/pages/Cashout/Wizard.js +++ b/new-lamassu-admin/src/pages/Cashout/Wizard.js @@ -1,191 +1,70 @@ +import * as R from 'ramda' 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' +import Modal from 'src/components/Modal' +import { toNamespace } from 'src/utils/config' -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 +import WizardSplash from './WizardSplash' +import WizardStep from './WizardStep' + +const LAST_STEP = 3 +const MODAL_WIDTH = 554 + +const Wizard = ({ machine, onClose, save, error }) => { + const [{ step, config }, setState] = useState({ + step: 0, + config: { active: true } + }) + + const title = `Enable cash-out` + const isLastStep = step === LAST_STEP + + const onContinue = async it => { + const newConfig = R.merge(config, it) + + if (isLastStep) { + return save(toNamespace(machine.deviceId, newConfig)) } - }, - 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 ( -
- {error && Failed to save} - -
- ) -} - -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)) + setState({ + step: step + 1, + config: newConfig + }) } - 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) { + const getStepData = () => { + switch (step) { case 1: - return topOverride > 0 + return { type: 'top', display: 'Cassete 1 (Top)' } case 2: - return bottomOverride > 0 + return { type: 'bottom', display: 'Cassete 2' } + case 3: + return { type: 'agreed' } default: - return isSubmittable(1) && isSubmittable(2) + return null } } return ( -
-

Enable cash-out

- {machine.name} - - {currentStage < 3 && ( - <> -

{pageName}

-

Choose bill denomination

- + + {step === 0 && ( + onContinue()} /> )} -
- {currentStage < 3 && ( - <> - {currentStage === 1 && ( - - )} - - {currentStage === 2 && ( - - )} - - - - )} - {currentStage === 3 && ( - <> -

{pageName}

-

- 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. -

-

{pageName}

-

- 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. -

- - )} -
- -
+ {step !== 0 && ( + + )} + ) } diff --git a/new-lamassu-admin/src/pages/Cashout/WizardSplash.js b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js index d1c2d42e..1a410bab 100644 --- a/new-lamassu-admin/src/pages/Cashout/WizardSplash.js +++ b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js @@ -1,60 +1,49 @@ -import React from 'react' import { makeStyles } from '@material-ui/core' +import React from 'react' -import { H1, P } from 'src/components/typography' import { Button } from 'src/components/buttons' -import { neon, spacer } from 'src/styling/variables' +import { H1, P } from 'src/components/typography' const styles = { - logoWrapper: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: 80, - margin: [[40, 0, 24]], - '& > svg': { - maxHeight: '100%', - width: '100%' - } + logo: { + maxHeight: 80, + maxWidth: 200 + }, + title: { + margin: [[24, 0, 32, 0]] + }, + text: { + margin: 0 + }, + button: { + marginTop: 'auto', + marginBottom: 58 }, 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]] - } - } + padding: [[0, 42]], + flex: 1 } } const useStyles = makeStyles(styles) -const WizardSplash = ({ handleModalNavigation, machine }) => { +const WizardSplash = ({ name, onContinue }) => { const classes = useStyles() return (
-

Enable cash-out

-

- You are about to activate cash-out functionality on your {machine.name}{' '} - machine which will allow your customers to sell crypto to you. -
+

Enable cash-out

+

+ You are about to activate cash-out functionality on your {name} machine + which will allow your customers to sell crypto to you.
In order to activate cash-out for this machine, please enter the denominations for the machine.

-
diff --git a/new-lamassu-admin/src/pages/Cashout/WizardStep.js b/new-lamassu-admin/src/pages/Cashout/WizardStep.js new file mode 100644 index 00000000..e2fe99fa --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/WizardStep.js @@ -0,0 +1,130 @@ +import { makeStyles } from '@material-ui/core' +import classnames from 'classnames' +import * as R from 'ramda' +import React, { useReducer, useEffect } from 'react' + +import ErrorMessage from 'src/components/ErrorMessage' +import Stepper from 'src/components/Stepper' +import { Button } from 'src/components/buttons' +import { TextInput } from 'src/components/inputs' +import { Info2, H4, P } from 'src/components/typography' + +import styles from './WizardStep.styles' +const useStyles = makeStyles(styles) + +const initialState = { + selected: null, + iError: false +} + +const reducer = (state, action) => { + switch (action.type) { + case 'select': + return { + form: null, + selected: action.selected, + isNew: null, + iError: false + } + case 'form': + return { + form: action.form, + selected: action.form.code, + isNew: true, + iError: false + } + case 'error': + return R.merge(state, { iError: true }) + case 'reset': + return initialState + default: + throw new Error() + } +} + +const WizardStep = ({ + type, + name, + step, + error, + lastStep, + onContinue, + display +}) => { + const classes = useStyles() + const [{ iError, selected }, dispatch] = useReducer(reducer, initialState) + + useEffect(() => { + dispatch({ type: 'reset' }) + }, [step]) + + const iContinue = config => { + if (lastStep) config[type] = true + + if (!config || !config[type]) { + return dispatch({ type: 'error' }) + } + + onContinue(config) + } + + const label = lastStep ? 'Finish' : 'Next' + const subtitleClass = { + [classes.subtitle]: true, + [classes.error]: iError + } + + return ( + <> + {name} + + {display &&

Edit {display}

} + + {!lastStep && ( + + dispatch({ type: 'select', selected: evt.target.value }) + } + autoFocus + id="confirm-input" + type="text" + size="lg" + touched={{}} + error={false} + InputLabelProps={{ shrink: true }} + /> + // TODO: there was a disabled link here showing the currency code; restore it + )} + + {lastStep && ( + <> +

+ 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. +

+

+ 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. +

+ + )} + +
+ {error && Failed to save} + +
+ + ) +} + +export default WizardStep diff --git a/new-lamassu-admin/src/pages/Cashout/WizardStep.styles.js b/new-lamassu-admin/src/pages/Cashout/WizardStep.styles.js new file mode 100644 index 00000000..d143ab4f --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/WizardStep.styles.js @@ -0,0 +1,26 @@ +import { errorColor } from 'src/styling/variables' + +const LABEL_WIDTH = 150 + +export default { + title: { + margin: [[0, 0, 12, 0]] + }, + subtitle: { + margin: [[32, 0, 21, 0]] + }, + error: { + color: errorColor + }, + button: { + marginLeft: 'auto' + }, + submit: { + display: 'flex', + flexDirection: 'row', + margin: [['auto', 0, 24]] + }, + picker: { + width: LABEL_WIDTH + } +} diff --git a/new-lamassu-admin/src/pages/Cashout/helper.js b/new-lamassu-admin/src/pages/Cashout/helper.js new file mode 100644 index 00000000..466da650 --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/helper.js @@ -0,0 +1,41 @@ +import * as Yup from 'yup' + +import TextInput from 'src/components/inputs/formik/TextInput' + +const DenominationsSchema = Yup.object().shape({ + top: Yup.number().required('Required'), + bottom: Yup.number().required('Required') +}) + +const getElements = (machines, { fiatCurrency } = {}) => { + return [ + { + name: 'id', + header: 'Machine', + width: 254, + view: it => machines.find(({ deviceId }) => deviceId === it).name, + size: 'sm', + editable: false + }, + { + name: 'top', + header: 'Cassette 1 (Top)', + view: it => `${it} ${fiatCurrency}`, + size: 'sm', + stripe: true, + width: 265, + input: TextInput + }, + { + name: 'bottom', + header: 'Cassette 2', + view: it => `${it} ${fiatCurrency}`, + size: 'sm', + stripe: true, + width: 265, + input: TextInput + } + ] +} + +export { DenominationsSchema, getElements } diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 4bd1ca65..90535954 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -68,12 +68,6 @@ const tree = [ return () => }, children: [ - { - key: namespaces.CASH_OUT, - label: 'Cash-out', - route: '/settings/cash-out', - component: Cashout - }, { key: namespaces.COMMISSIONS, label: 'Commissions', @@ -86,6 +80,12 @@ const tree = [ route: '/settings/locale', component: Locales }, + { + key: namespaces.CASH_OUT, + label: 'Cash-out', + route: '/settings/cash-out', + component: Cashout + }, { key: namespaces.SERVICES, label: '3rd party services', diff --git a/new-lamassu-admin/src/utils/config.js b/new-lamassu-admin/src/utils/config.js index 4b98c793..f4f3c810 100644 --- a/new-lamassu-admin/src/utils/config.js +++ b/new-lamassu-admin/src/utils/config.js @@ -1,7 +1,7 @@ import * as R from 'ramda' const namespaces = { - CASH_OUT: 'cash-out', + CASH_OUT: 'denominations', WALLETS: 'wallets', OPERATOR_INFO: 'operatorInfo', NOTIFICATIONS: 'notifications',