diff --git a/new-lamassu-admin/src/components/Stage.js b/new-lamassu-admin/src/components/Stage.js new file mode 100644 index 00000000..916daf4e --- /dev/null +++ b/new-lamassu-admin/src/components/Stage.js @@ -0,0 +1,112 @@ +import { makeStyles } from '@material-ui/core' +import classnames from 'classnames' +import * as R from 'ramda' +import React, { memo } from 'react' + +import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg' +import { ReactComponent as CurrentStageIconSpring } from 'src/styling/icons/stage/spring/current.svg' +import { ReactComponent as EmptyStageIconSpring } from 'src/styling/icons/stage/spring/empty.svg' +import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg' +import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg' +import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg' +import { + primaryColor, + secondaryColor, + offColor, + disabledColor +} from 'src/styling/variables' + +const styles = { + stages: { + display: 'flex', + alignItems: 'center' + }, + wrapper: { + display: 'flex', + alignItems: 'center', + margin: 0 + }, + stage: { + display: 'flex', + height: 28, + width: 28, + zIndex: 2, + '& > svg': { + height: '100%', + width: '100%', + overflow: 'visible' + } + }, + separator: { + width: 28, + height: 2, + border: [[2, 'solid']], + zIndex: 1 + }, + separatorSpring: { + borderColor: secondaryColor + }, + separatorZodiac: { + borderColor: primaryColor + }, + separatorSpringEmpty: { + borderColor: disabledColor + }, + separatorZodiacEmpty: { + borderColor: offColor + } +} + +const useStyles = makeStyles(styles) + +const Stage = memo(({ stages, currentStage, color = 'spring', className }) => { + if (currentStage < 1 || currentStage > stages) + throw Error('Value of currentStage is invalid') + if (stages < 1) throw Error('Value of stages is invalid') + + const classes = useStyles() + + const separatorClasses = { + [classes.separator]: true, + [classes.separatorSpring]: color === 'spring', + [classes.separatorZodiac]: color === 'zodiac' + } + + const separatorEmptyClasses = { + [classes.separator]: true, + [classes.separatorSpringEmpty]: color === 'spring', + [classes.separatorZodiacEmpty]: color === 'zodiac' + } + + return ( +
+ {R.range(1, currentStage).map(idx => ( +
+ {idx > 1 &&
} +
+ {color === 'spring' && } + {color === 'zodiac' && } +
+
+ ))} +
+ {currentStage > 1 &&
} +
+ {color === 'spring' && } + {color === 'zodiac' && } +
+
+ {R.range(currentStage + 1, stages + 1).map(idx => ( +
+
+
+ {color === 'spring' && } + {color === 'zodiac' && } +
+
+ ))} +
+ ) +}) + +export default Stage diff --git a/new-lamassu-admin/src/pages/Cashout/Cashout.js b/new-lamassu-admin/src/pages/Cashout/Cashout.js new file mode 100644 index 00000000..5a82d083 --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/Cashout.js @@ -0,0 +1,91 @@ +import { useQuery, useMutation } from '@apollo/react-hooks' +import { gql } from 'apollo-boost' +import * as R from 'ramda' +import React, { useState } from 'react' + +import { NamespacedTable as EditableTable } from 'src/components/editableTable' +import TitleSection from 'src/components/layout/TitleSection' +import { fromNamespace, toNamespace } from 'src/utils/config' + +import Wizard from './Wizard' +import { DenominationsSchema, getElements } from './helper' + +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 + cashbox + cassette1 + cassette2 + } + config + } +` + +const CashOut = ({ name: SCREEN_KEY }) => { + const [wizard, setWizard] = useState(false) + const [error, setError] = useState(false) + const { data } = useQuery(GET_INFO) + + const [saveConfig] = useMutation(SAVE_CONFIG, { + onCompleted: () => setWizard(false), + onError: () => setError(true), + refetchQueries: () => ['getData'] + }) + + const save = (rawConfig, accounts) => { + const config = toNamespace(SCREEN_KEY)(rawConfig) + setError(false) + + return saveConfig({ variables: { config, accounts } }) + } + + 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 })) + } + + return ( + <> + + !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)} + /> + {wizard && ( + setWizard(false)} + save={save} + error={error} + /> + )} + + ) +} + +export default CashOut diff --git a/new-lamassu-admin/src/pages/Cashout/Wizard.js b/new-lamassu-admin/src/pages/Cashout/Wizard.js new file mode 100644 index 00000000..a49576b3 --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/Wizard.js @@ -0,0 +1,71 @@ +import * as R from 'ramda' +import React, { useState } from 'react' + +import Modal from 'src/components/Modal' +import { toNamespace } from 'src/utils/config' + +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)) + } + + setState({ + step: step + 1, + config: newConfig + }) + } + + const getStepData = () => { + switch (step) { + case 1: + return { type: 'top', display: 'Cassete 1 (Top)' } + case 2: + return { type: 'bottom', display: 'Cassete 2' } + case 3: + return { type: 'agreed' } + default: + return null + } + } + + return ( + + {step === 0 && ( + onContinue()} /> + )} + {step !== 0 && ( + + )} + + ) +} + +export default Wizard diff --git a/new-lamassu-admin/src/pages/Cashout/WizardSplash.js b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js new file mode 100644 index 00000000..1a410bab --- /dev/null +++ b/new-lamassu-admin/src/pages/Cashout/WizardSplash.js @@ -0,0 +1,53 @@ +import { makeStyles } from '@material-ui/core' +import React from 'react' + +import { Button } from 'src/components/buttons' +import { H1, P } from 'src/components/typography' + +const styles = { + 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, 42]], + flex: 1 + } +} + +const useStyles = makeStyles(styles) + +const WizardSplash = ({ name, onContinue }) => { + const classes = useStyles() + + return ( +
+

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

+ +
+ ) +} + +export default WizardSplash 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/pages/common.styles.js b/new-lamassu-admin/src/pages/common.styles.js new file mode 100644 index 00000000..db9958b5 --- /dev/null +++ b/new-lamassu-admin/src/pages/common.styles.js @@ -0,0 +1,17 @@ +export default { + titleWrapper: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row' + }, + titleAndButtonsContainer: { + display: 'flex' + }, + iconButton: { + border: 'none', + outline: 0, + backgroundColor: 'transparent', + cursor: 'pointer' + } +} diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 28ce1b60..7266898e 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -3,6 +3,7 @@ import React from 'react' import { Route, Redirect, Switch } from 'react-router-dom' import AuthRegister from 'src/pages/AuthRegister' +import Cashout from 'src/pages/Cashout/Cashout' import Commissions from 'src/pages/Commissions' import { Customers, CustomerProfile } from 'src/pages/Customers' import Funding from 'src/pages/Funding' @@ -87,6 +88,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 c6abad75..f4f3c810 100644 --- a/new-lamassu-admin/src/utils/config.js +++ b/new-lamassu-admin/src/utils/config.js @@ -1,6 +1,7 @@ import * as R from 'ramda' const namespaces = { + CASH_OUT: 'denominations', WALLETS: 'wallets', OPERATOR_INFO: 'operatorInfo', NOTIFICATIONS: 'notifications',