diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js index da171b08..bdf585d8 100644 --- a/lib/new-settings-loader.js +++ b/lib/new-settings-loader.js @@ -11,7 +11,11 @@ low(adapter).then(it => { function saveConfig (config) { const currentState = db.getState() - const newState = _.merge(currentState, config) + const newState = _.mergeWith((objValue, srcValue) => { + if (_.isArray(objValue)) { + return srcValue + } + }, currentState, config) db.setState(newState) return db.write() diff --git a/new-lamassu-admin/src/components/buttons/AddButton.js b/new-lamassu-admin/src/components/buttons/AddButton.js new file mode 100644 index 00000000..d549aa80 --- /dev/null +++ b/new-lamassu-admin/src/components/buttons/AddButton.js @@ -0,0 +1,53 @@ +import { makeStyles } from '@material-ui/core/styles' +import classnames from 'classnames' +import React, { memo } from 'react' + +import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg' +import { zircon, zircon2, comet, fontColor, white } from 'src/styling/variables' +import typographyStyles from 'src/components/typography/styles' + +const { p } = typographyStyles + +const styles = { + button: { + extend: p, + border: 'none', + backgroundColor: zircon, + cursor: 'pointer', + outline: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: 167, + height: 48, + color: fontColor, + '&:hover': { + backgroundColor: zircon2 + }, + '&:active': { + backgroundColor: comet, + color: white, + '& svg g *': { + stroke: white + } + }, + '& svg': { + marginRight: 8 + } + } +} + +const useStyles = makeStyles(styles) + +const SimpleButton = memo(({ className, children, ...props }) => { + const classes = useStyles() + + return ( + + ) +}) + +export default SimpleButton diff --git a/new-lamassu-admin/src/components/buttons/index.js b/new-lamassu-admin/src/components/buttons/index.js index 6effb00e..413e17d1 100644 --- a/new-lamassu-admin/src/components/buttons/index.js +++ b/new-lamassu-admin/src/components/buttons/index.js @@ -1,8 +1,17 @@ import ActionButton from './ActionButton' +import AddButton from './AddButton' import Button from './Button' import FeatureButton from './FeatureButton' import IDButton from './IDButton' import Link from './Link' import SimpleButton from './SimpleButton' -export { Button, Link, SimpleButton, ActionButton, FeatureButton, IDButton } +export { + Button, + Link, + SimpleButton, + ActionButton, + FeatureButton, + IDButton, + AddButton +} diff --git a/new-lamassu-admin/src/components/editableTable/Row.js b/new-lamassu-admin/src/components/editableTable/Row.js index d91f8fbf..1d7e4e16 100644 --- a/new-lamassu-admin/src/components/editableTable/Row.js +++ b/new-lamassu-admin/src/components/editableTable/Row.js @@ -1,83 +1,250 @@ -import { Form, Formik, FastField, useFormikContext } from 'formik' -import React, { useState, memo } from 'react' +import React, { memo } from 'react' +import * as R from 'ramda' +import classnames from 'classnames' +import { Form, Formik, Field, useFormikContext } from 'formik' +import { makeStyles } from '@material-ui/core' import { Link } from 'src/components/buttons' -import { Td, Tr } from 'src/components/fake-table/Table' +import { Td, Tr, CellDoubleLevel } from 'src/components/fake-table/Table' +import { TextInputDisplay } from 'src/components/inputs/base/TextInput' +import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' +import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg' +// import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' +// import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg' -const getField = (name, component, props = {}) => ( - -) - -const ERow = memo(({ elements }) => { - const { values, submitForm, resetForm, errors } = useFormikContext() - const [editing, setEditing] = useState(false) - - const innerSave = () => { - submitForm() +const styles = { + button: { + border: 'none', + backgroundColor: 'transparent', + outline: 0, + cursor: 'pointer' + }, + actionCol: { + display: 'flex', + marginLeft: 'auto' + }, + actionColDisplayMode: { + justifyContent: 'center' + }, + actionColEditMode: { + justifyContent: 'flex-end', + '& > :first-child': { + marginRight: 16 + } + }, + textInput: { + '& > .MuiInputBase-input': { + width: 282 + } } + // doubleLevelRow: { + // '& > div': { + // marginRight: 72 + // } + // } +} - const innerCancel = () => { - setEditing(false) - resetForm() - } +const useStyles = makeStyles(styles) - return ( - - {elements.map( - ( - { +const ERow = memo( + ({ elements, editing, setEditing, disableAction, action }) => { + const classes = useStyles() + + const Cell = ({ + name, + input, + type, + display, + className, + size, + textAlign, + inputProps, + editing + }) => { + return ( + + {editing && ( + + )} + {!editing && type === 'text' && ( + + )} + + ) + } + + const actionCol = R.last(elements) + const { values, errors } = useFormikContext() + + const actionColClasses = { + [classes.actionCol]: true, + [classes.actionColDisplayMode]: !editing, + [classes.actionColEditMode]: editing + } + + const icon = (action, disabled) => { + if (action === 'delete' && !disabled) return + if (action === 'delete' && disabled) return + } + + return ( + + {R.init(elements).map((element, idx) => { + const colClasses = { + [classes.textInput]: true + } + + if (Array.isArray(element)) { + return ( + + {R.map( + ( + { + name, + input, + size, + textAlign, + type, + view = it => it?.toString(), + inputProps + }, + idx + ) => ( + + // + // (x === '' ? '-' : x)} + // decoration="%" + // className={classes.eRowField} + // setError={setError} + // /> + // + ) + )(R.tail(element))} + + ) + } + + const { name, input, size, textAlign, + type, view = it => it?.toString(), inputProps - }, - idx - ) => ( - - {editing && getField(name, input, inputProps)} - {!editing && view(values[name])} - - ) - )} - - {editing ? ( - <> - - Cancel - - - Save - - - ) : ( - setEditing(true)}> - Edit - - )} - - - ) -}) + } = element -const ERowWithFormik = memo(({ value, validationSchema, save, elements }) => { - return ( - -
- - -
- ) -}) + return ( + + // + // {editing && ( + // + // )} + // {!editing && type === 'text' && ( + // + // )} + // + ) + })} + + {!editing && !disableAction && ( + + )} + {!editing && disableAction && ( +
{icon(actionCol.name, disableAction)}
+ )} + {editing && ( + <> + + Cancel + + + Save + + + )} + + + ) + } +) + +const ERowWithFormik = memo( + ({ + initialValues, + validationSchema, + save, + reset, + action, + elements, + editing, + disableAction + }) => { + return ( + +
+ + +
+ ) + } +) export default ERowWithFormik diff --git a/new-lamassu-admin/src/components/editableTable/Table.js b/new-lamassu-admin/src/components/editableTable/Table.js index ea2baa7a..100a9b93 100644 --- a/new-lamassu-admin/src/components/editableTable/Table.js +++ b/new-lamassu-admin/src/components/editableTable/Table.js @@ -1,34 +1,124 @@ import React, { memo } from 'react' +import * as R from 'ramda' -import { Td, THead, TBody, Table } from 'src/components/fake-table/Table' +import { + Th, + ThDoubleLevel, + THead, + TBody, + Table, + TDoubleLevelHead +} from 'src/components/fake-table/Table' import { startCase } from 'src/utils/string' import ERow from './Row' -const ETable = memo(({ elements = [], data = [], save, validationSchema }) => { +const ETHead = memo(({ elements, className }) => { + const action = R.last(elements) + return ( - - - {elements.map(({ name, size, header, textAlign }, idx) => ( - - ))} - - - {data.map((it, idx) => ( - - ))} - -
- {header || startCase(name)} - -
+ + {R.init(elements).map(({ name, size, display, textAlign }, idx) => ( + + {display} + + ))} + + {startCase(action.name)} + + ) }) +const ETDoubleHead = memo(({ elements, className }) => { + const action = R.last(elements) + + return ( + + {R.init(elements).map((element, idx) => { + if (Array.isArray(element)) { + return ( + + {R.map(({ name, size, display, textAlign }) => ( + + {display} + + ))(R.tail(element))} + + ) + } + + const { name, size, display, textAlign } = element + return ( + + {display} + + ) + })} + + {startCase(action.name)} + + + ) +}) + +const ETable = memo( + ({ + elements = [], + data = [], + save, + reset, + action, + initialValues, + validationSchema, + editing, + addingRow, + disableAction, + className, + double + }) => { + return ( + + {!double && } + {double && ( + + )} + + {addingRow && ( + + )} + {data.map((it, idx) => ( + reset(it)} + action={action} + validationSchema={validationSchema} + disableAction={disableAction} + editing={editing[idx]} + /> + ))} + +
+ ) + } +) + export default ETable diff --git a/new-lamassu-admin/src/components/fake-table/Table.js b/new-lamassu-admin/src/components/fake-table/Table.js index 5189a939..37747023 100644 --- a/new-lamassu-admin/src/components/fake-table/Table.js +++ b/new-lamassu-admin/src/components/fake-table/Table.js @@ -10,15 +10,17 @@ import { tableHeaderHeight, tableErrorColor, spacer, - white + white, + tableDoubleHeaderHeight, + offColor } from 'src/styling/variables' import typographyStyles from 'src/components/typography/styles' -const { tl2, p } = typographyStyles +const { tl2, p, label1 } = typographyStyles const useStyles = makeStyles({ body: { - borderSpacing: '0 4px' + borderSpacing: [[0, 4]] }, header: { extend: tl2, @@ -26,16 +28,49 @@ const useStyles = makeStyles({ height: tableHeaderHeight, textAlign: 'left', color: white, - // display: 'flex' + display: 'flex', + alignItems: 'center' + }, + doubleHeader: { + extend: tl2, + backgroundColor: tableHeaderColor, + height: tableDoubleHeaderHeight, + color: white, display: 'table-row' }, + thDoubleLevel: { + padding: [[0, spacer * 2]], + display: 'table-cell', + '& > :first-child': { + extend: label1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: offColor, + color: white, + borderRadius: [[0, 0, 8, 8]], + height: 28 + }, + '& > :last-child': { + display: 'table-cell', + verticalAlign: 'middle', + height: tableDoubleHeaderHeight - 28, + '& > div': { + display: 'inline-block' + } + } + }, + cellDoubleLevel: { + display: 'flex', + padding: [[0, spacer * 2]] + }, td: { - padding: `0 ${spacer * 3}px` + padding: [[0, spacer * 3]] }, tdHeader: { verticalAlign: 'middle', display: 'table-cell', - padding: `0 ${spacer * 3}px` + padding: [[0, spacer * 3]] }, trError: { backgroundColor: tableErrorColor @@ -59,9 +94,12 @@ const useStyles = makeStyles({ '&:before': { height: 0 }, - margin: '4px 0', - width: 'min-content', - boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)' + margin: [[4, 0]], + width: '100%', + boxShadow: [[0, 0, 4, 0, 'rgba(0, 0, 0, 0.08)']] + }, + actionCol: { + marginLeft: 'auto' } }) @@ -76,16 +114,27 @@ const THead = ({ children, className }) => { return
{children}
} +const TDoubleLevelHead = ({ children, className }) => { + const classes = useStyles() + + return ( +
+ {children} +
+ ) +} + const TBody = ({ children, className }) => { const classes = useStyles() return
{children}
} -const Td = ({ children, header, className, size = 100, textAlign }) => { +const Td = ({ children, header, className, size = 100, textAlign, action }) => { const classes = useStyles() const classNames = { [classes.td]: true, - [classes.tdHeader]: header + [classes.tdHeader]: header, + [classes.actionCol]: action } return ( @@ -105,6 +154,27 @@ const Th = ({ children, ...props }) => { ) } +const ThDoubleLevel = ({ title, children, className }) => { + const classes = useStyles() + + return ( +
+
{title}
+
{children}
+
+ ) +} + +const CellDoubleLevel = ({ children, className }) => { + const classes = useStyles() + + return ( +
+ {children} +
+ ) +} + const Tr = ({ error, errorMessage, children, className }) => { const classes = useStyles() const cardClasses = { root: classes.cardContentRoot } @@ -135,4 +205,15 @@ const EditCell = ({ save, cancel }) => ( ) -export { Table, THead, TBody, Tr, Td, Th, EditCell } +export { + Table, + THead, + TDoubleLevelHead, + TBody, + Tr, + Td, + Th, + ThDoubleLevel, + CellDoubleLevel, + EditCell +} diff --git a/new-lamassu-admin/src/components/inputs/autocomplete/Autocomplete.js b/new-lamassu-admin/src/components/inputs/autocomplete/Autocomplete.js index f27e0a93..17e80002 100644 --- a/new-lamassu-admin/src/components/inputs/autocomplete/Autocomplete.js +++ b/new-lamassu-admin/src/components/inputs/autocomplete/Autocomplete.js @@ -13,7 +13,16 @@ import { } from './commons' const Autocomplete = memo( - ({ suggestions, classes, placeholder, label, itemToString, ...props }) => { + ({ + suggestions, + classes, + placeholder, + label, + itemToString, + code = 'code', + display = 'display', + ...props + }) => { const { name, value, onBlur } = props.field const { touched, errors, setFieldValue } = props.form @@ -22,7 +31,11 @@ const Autocomplete = memo( return ( (itemToString ? itemToString(it) : it?.display)} + itemToString={it => { + if (itemToString) return itemToString(it) + if (it) return it[display] + return undefined + }} onChange={it => setFieldValue(name, it)} defaultHighlightedIndex={0} selectedItem={value}> @@ -40,9 +53,8 @@ const Autocomplete = memo( }) => (
{renderInput({ - id: name, + name, fullWidth: true, - classes, error: (touched[`${name}-input`] || touched[name]) && errors[name], success: @@ -52,7 +64,10 @@ const Autocomplete = memo( value: inputValue2 || '', placeholder, onBlur, - onClick: () => toggleMenu(), + onClick: event => { + setPopperNode(event.currentTarget.parentElement) + toggleMenu() + }, onChange: it => { if (it.target.value === '') { clearSelection() @@ -60,12 +75,12 @@ const Autocomplete = memo( inputValue = it.target.value } }), - ref: node => { - setPopperNode(node) - }, label })} - +
{filterSuggestions( suggestions, inputValue2, - value ? R.of(value) : [] + value ? R.of(value) : [], + code, + display ).map((suggestion, index) => renderSuggestion({ suggestion, index, itemProps: getItemProps({ item: suggestion }), highlightedIndex, - selectedItem: selectedItem2 + selectedItem: selectedItem2, + code, + display }) )} diff --git a/new-lamassu-admin/src/components/inputs/autocomplete/commons.js b/new-lamassu-admin/src/components/inputs/autocomplete/commons.js index dae6045a..03e40d2c 100644 --- a/new-lamassu-admin/src/components/inputs/autocomplete/commons.js +++ b/new-lamassu-admin/src/components/inputs/autocomplete/commons.js @@ -1,32 +1,31 @@ import MenuItem from '@material-ui/core/MenuItem' -import TextField from '@material-ui/core/TextField' import Fuse from 'fuse.js' -import * as R from 'ramda' import React from 'react' import slugify from 'slugify' +import { withStyles } from '@material-ui/core/styles' import { fontColor, inputFontSize, - inputFontWeight + inputFontWeight, + zircon } from 'src/styling/variables' import S from 'src/utils/sanctuary' -function renderInput(inputProps) { - const { onBlur, success, InputProps, classes, ref, ...other } = inputProps +import { TextInput } from '../base' + +function renderInput({ InputProps, error, name, success, ...props }) { + const { onChange, onBlur, value } = InputProps return ( - ) } @@ -36,29 +35,44 @@ function renderSuggestion({ index, itemProps, highlightedIndex, - selectedItem + selectedItem, + code, + display }) { const isHighlighted = highlightedIndex === index - const item = R.o(R.defaultTo(''), R.path(['display']))(selectedItem) - const isSelected = R.indexOf(suggestion.display)(item) > -1 + const StyledMenuItem = withStyles(theme => ({ + root: { + fontSize: 14, + fontWeight: 400, + color: fontColor + }, + selected: { + '&.Mui-selected, &.Mui-selected:hover': { + fontWeight: 500, + backgroundColor: zircon + } + } + }))(MenuItem) return ( - - {suggestion.display} - + component="div"> + {suggestion[display]} + ) } -function filterSuggestions(suggestions = [], value = '', currentValues = []) { +function filterSuggestions( + suggestions = [], + value = '', + currentValues = [], + code, + display +) { const options = { shouldSort: true, threshold: 0.2, @@ -66,14 +80,15 @@ function filterSuggestions(suggestions = [], value = '', currentValues = []) { distance: 100, maxPatternLength: 32, minMatchCharLength: 1, - keys: ['code', 'display'] + code, + display } const fuse = new Fuse(suggestions, options) const result = value ? fuse.search(slugify(value, ' ')) : suggestions - const currentCodes = S.map(S.prop('code'))(currentValues) - const filtered = S.filter(it => !S.elem(it.code)(currentCodes))(result) + const currentCodes = S.map(S.prop(code))(currentValues) + const filtered = S.filter(it => !S.elem(it[code])(currentCodes))(result) const amountToTake = S.min(filtered.length)(5) diff --git a/new-lamassu-admin/src/components/inputs/base/Switch.js b/new-lamassu-admin/src/components/inputs/base/Switch.js index 7e601547..e7a19ae9 100644 --- a/new-lamassu-admin/src/components/inputs/base/Switch.js +++ b/new-lamassu-admin/src/components/inputs/base/Switch.js @@ -26,7 +26,11 @@ const useStyles = makeStyles(theme => ({ } }, '&$checked': { + transform: 'translateX(58%)', color: theme.palette.common.white, + '&$disabled': { + color: disabledColor2 + }, '& + $track': { backgroundColor: secondaryColor, opacity: 1, diff --git a/new-lamassu-admin/src/components/inputs/base/TextInput.js b/new-lamassu-admin/src/components/inputs/base/TextInput.js index 835b5730..6f835064 100644 --- a/new-lamassu-admin/src/components/inputs/base/TextInput.js +++ b/new-lamassu-admin/src/components/inputs/base/TextInput.js @@ -1,6 +1,5 @@ import React, { memo } from 'react' import classnames from 'classnames' -import InputAdornment from '@material-ui/core/InputAdornment' import TextField from '@material-ui/core/TextField' import { makeStyles } from '@material-ui/core/styles' @@ -10,27 +9,49 @@ import { secondaryColor, inputFontSize, inputFontSizeLg, - inputFontWeight + inputFontWeight, + inputFontWeightLg } from 'src/styling/variables' +import { TL2, Label2, Info1, Info2 } from 'src/components/typography' const useStyles = makeStyles({ + wrapper: { + display: 'inline-block', + maxWidth: '100%', + '& > span': { + display: 'flex', + alignItems: 'baseline', + '& > p:first-child': { + margin: [[0, 4, 5, 0]] + }, + '&> p:last-child': { + margin: [[0, 0, 0, 3]] + } + } + }, inputRoot: { fontSize: inputFontSize, color: fontColor, fontWeight: inputFontWeight, - paddingLeft: 4 + paddingLeft: 4, + '& > .MuiInputBase-input': { + width: 282 + } }, inputRootLg: { fontSize: inputFontSizeLg, color: fontColor, - fontWeight: inputFontWeight + fontWeight: inputFontWeightLg, + '& > .MuiInputBase-input': { + width: 96 + } }, labelRoot: { color: fontColor, paddingLeft: 4 }, root: { - '& .MuiInput-underline:before': { + '& > .MuiInput-underline:before': { borderBottom: [[2, 'solid', fontColor]] }, '& .Mui-focused': { @@ -40,9 +61,6 @@ const useStyles = makeStyles({ paddingTop: 4, paddingBottom: 3 }, - '& .MuiInputBase-input': { - width: 282 - }, '& .MuiInputBase-inputMultiline': { width: 500, paddingRight: 20 @@ -66,6 +84,23 @@ const useStyles = makeStyles({ } }) +const TextInputDisplay = memo(({ display, suffix, large }) => { + const classes = useStyles() + + return ( +
+ + {large && !suffix && {display}} + {!large && !suffix && {display}} + {large && suffix && {display}} + {!large && suffix && {display}} + {suffix && large && {suffix}} + {suffix && !large && {suffix}} + +
+ ) +}) + const TextInput = memo( ({ name, @@ -76,6 +111,7 @@ const TextInput = memo( suffix, large, className, + InputProps, ...props }) => { const classes = useStyles() @@ -87,30 +123,33 @@ const TextInput = memo( } return ( - - {suffix} - - ) : null - }} - InputLabelProps={{ className: classes.labelRoot }} - {...props} - /> +
+ + + {suffix && large && ( + <> + {suffix} + + )} + {suffix && !large && {suffix}} + +
) } ) -export default TextInput +export { TextInput, TextInputDisplay } diff --git a/new-lamassu-admin/src/components/inputs/base/index.js b/new-lamassu-admin/src/components/inputs/base/index.js index f52fc5e8..b56e6329 100644 --- a/new-lamassu-admin/src/components/inputs/base/index.js +++ b/new-lamassu-admin/src/components/inputs/base/index.js @@ -1,5 +1,5 @@ import Checkbox from './Checkbox' -import TextInput from './TextInput' +import { TextInput } from './TextInput' import Switch from './Switch' import RadioGroup from './RadioGroup' diff --git a/new-lamassu-admin/src/components/inputs/index.js b/new-lamassu-admin/src/components/inputs/index.js index 7ffe2a38..1650643e 100644 --- a/new-lamassu-admin/src/components/inputs/index.js +++ b/new-lamassu-admin/src/components/inputs/index.js @@ -5,7 +5,7 @@ import Radio from './base/Radio' import RadioGroup from './base/RadioGroup' import Select from './base/Select' import Switch from './base/Switch' -import TextInput from './base/TextInput' +import { TextInput } from './base/TextInput' export { Autocomplete, diff --git a/new-lamassu-admin/src/components/typography/index.js b/new-lamassu-admin/src/components/typography/index.js index 004d3c3a..8736e878 100644 --- a/new-lamassu-admin/src/components/typography/index.js +++ b/new-lamassu-admin/src/components/typography/index.js @@ -36,7 +36,7 @@ function H3({ children, className, ...props }) { function H4({ children, className, ...props }) { const classes = useStyles() return ( -

+

{children}

) diff --git a/new-lamassu-admin/src/pages/Notifications/CryptoBalanceAlerts.js b/new-lamassu-admin/src/pages/Notifications/CryptoBalanceAlerts.js new file mode 100644 index 00000000..f6864759 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/CryptoBalanceAlerts.js @@ -0,0 +1,254 @@ +import React, { useState } from 'react' +import * as R from 'ramda' +import { gql } from 'apollo-boost' +import classnames from 'classnames' +import * as Yup from 'yup' +import { makeStyles } from '@material-ui/core' +import { useQuery } from '@apollo/react-hooks' + +import { Info2 } from 'src/components/typography' +import commonStyles from 'src/pages/common.styles' +import { Table as EditableTable } from 'src/components/editableTable' +import Link from 'src/components/buttons/Link.js' +import { Autocomplete } from 'src/components/inputs/index.js' +import { AddButton } from 'src/components/buttons/index.js' +import TextInputFormik from 'src/components/inputs/formik/TextInput.js' + +import { + isDisabled, + LOW_BALANCE_KEY, + HIGH_BALANCE_KEY, + OVERRIDES_KEY, + ADD_OVERRIDE_CBA_KEY +} from './aux.js' +import { BigNumericInput } from './Inputs' +import { localStyles, cryptoBalanceAlertsStyles } from './Notifications.styles' + +const CRYPTOCURRENCY_KEY = 'cryptocurrency' + +const styles = R.mergeAll([ + commonStyles, + localStyles, + cryptoBalanceAlertsStyles +]) + +const GET_CRYPTOCURRENCIES = gql` + { + cryptoCurrencies { + code + display + } + } +` + +const useStyles = makeStyles(styles) + +const CryptoBalanceAlerts = ({ + values: setupValues, + save, + editingState, + handleEditingClick, + setError +}) => { + const [cryptoCurrencies, setCryptoCurrencies] = useState(null) + + useQuery(GET_CRYPTOCURRENCIES, { + onCompleted: data => { + setCryptoCurrencies(data.cryptoCurrencies) + }, + onError: error => console.error(error) + }) + const classes = useStyles() + + const editingLowBalance = editingState[LOW_BALANCE_KEY] + const editingHighBalance = editingState[HIGH_BALANCE_KEY] + const addingOverride = editingState[ADD_OVERRIDE_CBA_KEY] + + const overrideOpsDisabled = isDisabled(editingState, ADD_OVERRIDE_CBA_KEY) + + const handleEdit = R.curry(handleEditingClick) + + const handleSubmit = it => save(it) + + const handleSubmitOverrides = it => { + const newOverrides = { + [OVERRIDES_KEY]: R.prepend(it, setupValues[OVERRIDES_KEY]) + } + save(newOverrides) + } + + const handleResetForm = () => { + handleEdit(ADD_OVERRIDE_CBA_KEY)(false) + setError(null) + } + + const deleteOverride = it => { + const cryptocurrency = it[CRYPTOCURRENCY_KEY] + + const idx = R.findIndex( + R.propEq([CRYPTOCURRENCY_KEY], cryptocurrency), + setupValues[OVERRIDES_KEY] + ) + const newOverrides = R.remove(idx, 1, setupValues[OVERRIDES_KEY]) + + save({ [OVERRIDES_KEY]: newOverrides }) + } + + const defaultsFields = { + [LOW_BALANCE_KEY]: { + name: LOW_BALANCE_KEY, + label: 'Alert me under', + value: setupValues[LOW_BALANCE_KEY] + }, + [HIGH_BALANCE_KEY]: { + name: HIGH_BALANCE_KEY, + label: 'Alert me over', + value: setupValues[HIGH_BALANCE_KEY] + } + } + + const getSuggestions = () => { + const overridenCryptos = R.map( + override => override[CRYPTOCURRENCY_KEY], + setupValues[OVERRIDES_KEY] + ) + return R.without(overridenCryptos, cryptoCurrencies ?? []) + } + + const { [OVERRIDES_KEY]: overrides } = setupValues + + const initialValues = { + [CRYPTOCURRENCY_KEY]: '', + [LOW_BALANCE_KEY]: '', + [HIGH_BALANCE_KEY]: '' + } + + const validationSchema = Yup.object().shape({ + [CRYPTOCURRENCY_KEY]: Yup.string().required(), + [LOW_BALANCE_KEY]: Yup.number() + .integer() + .min(0) + .max(99999999) + .required(), + [HIGH_BALANCE_KEY]: Yup.number() + .integer() + .min(0) + .max(99999999) + .required() + }) + + const elements = [ + { + name: CRYPTOCURRENCY_KEY, + display: 'Cryptocurrency', + size: 166, + textAlign: 'left', + view: R.path(['display']), + type: 'text', + input: Autocomplete, + inputProps: { + suggestions: getSuggestions(), + onFocus: () => setError(null) + } + }, + { + name: LOW_BALANCE_KEY, + display: 'Low Balance', + size: 140, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: 'EUR', // TODO: Current currency? + className: classes.textInput, + onFocus: () => setError(null) + } + }, + { + name: HIGH_BALANCE_KEY, + display: 'High Balance', + size: 140, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: 'EUR', // TODO: Current currency? + className: classes.textInput, + onFocus: () => setError(null) + } + }, + { + name: 'delete', + size: 91 + } + ] + + if (!cryptoCurrencies) return null + + return ( + <> +
+
+ + +
+
+
+
+ Overrides + {!addingOverride && !overrideOpsDisabled && overrides.length > 0 && ( + handleEdit(ADD_OVERRIDE_CBA_KEY)(true)}> + Add override + + )} +
+ {!addingOverride && !overrideOpsDisabled && overrides.length === 0 && ( + handleEdit(ADD_OVERRIDE_CBA_KEY)(true)}> + Add overrides + + )} + {(addingOverride || overrides.length > 0) && ( + false, + R.range(0, setupValues[OVERRIDES_KEY].length) + )} + save={handleSubmitOverrides} + reset={handleResetForm} + action={deleteOverride} + initialValues={initialValues} + validationSchema={validationSchema} + data={setupValues[OVERRIDES_KEY]} + elements={elements} + /> + )} +
+ + ) +} + +export default CryptoBalanceAlerts diff --git a/new-lamassu-admin/src/pages/Notifications/FiatBalanceAlerts.js b/new-lamassu-admin/src/pages/Notifications/FiatBalanceAlerts.js new file mode 100644 index 00000000..a25aee90 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/FiatBalanceAlerts.js @@ -0,0 +1,597 @@ +import React, { useState } from 'react' +import * as R from 'ramda' +import classnames from 'classnames' +import { gql } from 'apollo-boost' +import * as Yup from 'yup' +import { makeStyles } from '@material-ui/core' +import { useQuery } from '@apollo/react-hooks' + +import { Info2 } from 'src/components/typography' +import commonStyles from 'src/pages/common.styles' +import { Table as EditableTable } from 'src/components/editableTable' +import { Link, AddButton } from 'src/components/buttons' +import { Autocomplete } from 'src/components/inputs' +import TextInputFormik from 'src/components/inputs/formik/TextInput.js' + +import { BigPercentageAndNumericInput, MultiplePercentageInput } from './Inputs' +import { localStyles, fiatBalanceAlertsStyles } from './Notifications.styles' +import { + CASH_IN_FULL_KEY, + isDisabled, + CASH_OUT_EMPTY_KEY, + CASSETTE_1_KEY, + CASSETTE_2_KEY, + PERCENTAGE_KEY, + NUMERARY_KEY, + OVERRIDES_KEY, + ADD_OVERRIDE_FBA_KEY, + MACHINE_KEY +} from './aux' + +const styles = R.mergeAll([commonStyles, localStyles, fiatBalanceAlertsStyles]) + +const useStyles = makeStyles(styles) + +const GET_MACHINES = gql` + { + machines { + name + deviceId + } + } +` + +// const OverridesRow = ({ +// machine, +// handleSubmitOverrides, +// handleEdit, +// setError, +// sizes, +// editing, +// fields, +// disabled, +// getSuggestions, +// ...props +// }) => { +// const classes = useStyles() + +// const baseInitialValues = { +// [fields[PERCENTAGE_KEY].name]: fields[PERCENTAGE_KEY].value ?? '', +// [fields[NUMERARY_KEY].name]: fields[NUMERARY_KEY].value ?? '', +// [fields[CASSETTE_1_KEY].name]: fields[CASSETTE_1_KEY].value ?? '', +// [fields[CASSETTE_2_KEY].name]: fields[CASSETTE_2_KEY].value ?? '' +// } + +// const initialValues = machine +// ? baseInitialValues +// : R.assoc(fields[MACHINE_KEY].name, '', baseInitialValues) + +// const baseValidationSchemaShape = { +// [fields[PERCENTAGE_KEY].name]: Yup.number() +// .integer() +// .min(0) +// .max(100) +// .required(), +// [fields[NUMERARY_KEY].name]: Yup.number() +// .integer() +// .min(0) +// .max(99999999) +// .required(), +// [fields[CASSETTE_1_KEY].name]: Yup.number() +// .integer() +// .min(0) +// .max(100) +// .required(), +// [fields[CASSETTE_2_KEY].name]: Yup.number() +// .integer() +// .min(0) +// .max(100) +// .required() +// } + +// const validationSchemaShape = machine +// ? baseValidationSchemaShape +// : R.assoc( +// fields[MACHINE_KEY].name, +// Yup.string().required(), +// baseValidationSchemaShape +// ) + +// return ( +// { +// const machineName = machine +// ? machine.name +// : values[fields[MACHINE_KEY].name].name +// handleSubmitOverrides(machineName)(values) +// }} +// onReset={(values, bag) => { +// handleEdit(machine?.name ?? ADD_OVERRIDE_FBA_KEY)(false) +// setError(null) +// }}> +//
+// +// +// {machine && machine.name} +// {!machine && ( +// +// )} +// +// +// +// (x === '' ? '-' : x)} +// decoration="%" +// className={classes.eRowField} +// setError={setError} +// /> +// +// +// (x === '' ? '-' : x)} +// decoration="EUR" +// className={classes.eRowField} +// setError={setError} +// /> +// +// +// +// +// (x === '' ? '-' : x)} +// decoration="%" +// className={classes.eRowField} +// setError={setError} +// /> +// +// +// (x === '' ? '-' : x)} +// decoration="%" +// className={classes.eRowField} +// setError={setError} +// /> +// +// +// +// {!editing && !disabled && ( +// +// )} +// {disabled && ( +//
+// +//
+// )} +// {editing && ( +// <> +// +// Save +// +// +// Cancel +// +// +// )} +// +// +// +//
+// ) +// } + +const FiatBalanceAlerts = ({ + values: setupValues, + save, + editingState, + handleEditingClick, + setError +}) => { + const [machines, setMachines] = useState(null) + useQuery(GET_MACHINES, { + onCompleted: data => { + setMachines(data.machines) + }, + onError: error => console.error(error) + }) + + const classes = useStyles() + + const getValue = R.path(R.__, setupValues) + + const handleEdit = R.curry(handleEditingClick) + + const handleSubmit = R.curry((key, it) => { + const setup = setupValues[key] + const pairs = R.mapObjIndexed((num, k, obj) => { + return [R.split('-', k)[1], num] + }, it) + const rightKeys = R.fromPairs(R.values(pairs)) + const newItem = { [key]: R.merge(setup, rightKeys) } + save(newItem) + }) + + const handleSubmitOverrides = R.curry((key, it) => { + const setup = setupValues[OVERRIDES_KEY] + const pathMatches = R.pathEq(['machine', 'name'], key) + + const pairs = R.values( + R.mapObjIndexed((num, k, obj) => { + const split = R.split('-', k) + if (split.length < 3) return { [split[1]]: num } + return { [split[1]]: { [split[2]]: num } } + }, it) + ) + + const old = R.find(pathMatches, setup) + if (!old) { + const newOverride = R.reduce(R.mergeDeepRight, {}, pairs) + const newOverrides = { + [OVERRIDES_KEY]: R.prepend(newOverride, setup) + } + save(newOverrides) + return + } + + const machineIdx = R.findIndex(pathMatches, setup) + const newOverride = R.mergeDeepRight( + old, + R.reduce(R.mergeDeepRight, {}, pairs) + ) + const newOverrides = { + [OVERRIDES_KEY]: R.update(machineIdx, newOverride, setup) + } + save(newOverrides) + }) + + const handleResetForm = it => { + const machine = it?.machine + handleEdit(machine?.name ?? ADD_OVERRIDE_FBA_KEY)(false) + setError(null) + } + + const getSuggestions = () => { + const overridenMachines = R.map( + override => override.machine, + setupValues[OVERRIDES_KEY] + ) + return R.without(overridenMachines, machines ?? []) + } + + const cashInFields = { + percentage: { + name: CASH_IN_FULL_KEY + '-' + PERCENTAGE_KEY, + label: 'Alert me over', + value: getValue([CASH_IN_FULL_KEY, PERCENTAGE_KEY]) + }, + numeric: { + name: CASH_IN_FULL_KEY + '-' + NUMERARY_KEY, + label: 'Or', + value: getValue([CASH_IN_FULL_KEY, NUMERARY_KEY]) + } + } + + const cashOutFields = [ + { + title: 'Cassette 1 (Top)', + name: CASH_OUT_EMPTY_KEY + '-' + CASSETTE_1_KEY, + label: 'Alert me at', + value: getValue([CASH_OUT_EMPTY_KEY, CASSETTE_1_KEY]) + }, + { + title: 'Cassette 2', + name: CASH_OUT_EMPTY_KEY + '-' + CASSETTE_2_KEY, + label: 'Alert me at', + value: getValue([CASH_OUT_EMPTY_KEY, CASSETTE_2_KEY]) + } + ] + + const { overrides } = setupValues + + const initialValues = { + [MACHINE_KEY]: '', + [PERCENTAGE_KEY]: '', + [NUMERARY_KEY]: '', + [CASSETTE_1_KEY]: '', + [CASSETTE_2_KEY]: '' + } + + const validationSchema = Yup.object().shape({ + [MACHINE_KEY]: Yup.string().required(), + [PERCENTAGE_KEY]: Yup.number() + .integer() + .min(0) + .max(100) + .required(), + [NUMERARY_KEY]: Yup.number() + .integer() + .min(0) + .max(99999999) + .required(), + [CASSETTE_1_KEY]: Yup.number() + .integer() + .min(0) + .max(100) + .required(), + [CASSETTE_2_KEY]: Yup.number() + .integer() + .min(0) + .max(100) + .required() + }) + + const elements = [ + { + name: MACHINE_KEY, + display: 'Machine', + size: 238, + textAlign: 'left', + view: R.path(['display']), + type: 'text', + input: Autocomplete, + inputProps: { + suggestions: getSuggestions(), + onFocus: () => setError(null) + } + }, + [ + { display: 'Cash-in (Cassette Full)' }, + { + name: PERCENTAGE_KEY, + display: 'Percentage', + size: 128, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: '%', + className: classes.textInput, + onFocus: () => setError(null) + } + }, + { + name: NUMERARY_KEY, + display: 'Amount', + size: 128, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: 'EUR', // TODO: Current currency? + className: classes.textInput, + onFocus: () => setError(null) + } + } + ], + [ + { display: 'Cash-out (Cassette Empty)' }, + { + name: CASSETTE_1_KEY, + display: 'Cash-out 1', + size: 128, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: '%', + className: classes.textInput, + onFocus: () => setError(null) + } + }, + { + name: CASSETTE_2_KEY, + display: 'Cash-out 2', + size: 128, + textAlign: 'right', + view: it => it, + type: 'text', + input: TextInputFormik, + inputProps: { + suffix: '%', + className: classes.textInput, + onFocus: () => setError(null) + } + } + ], + { + name: 'edit', + size: 98 + } + ] + + const cashInEditing = editingState[CASH_IN_FULL_KEY] + const cashOutEditing = editingState[CASH_OUT_EMPTY_KEY] + const overrideOpsDisabled = isDisabled(editingState, ADD_OVERRIDE_FBA_KEY) + const addingOverride = editingState[ADD_OVERRIDE_FBA_KEY] + + if (!machines) return null + + return ( +
+
+ +
+ +
+
+
+
+ Overrides + {!addingOverride && !overrideOpsDisabled && overrides.length > 0 && ( + handleEdit(ADD_OVERRIDE_FBA_KEY)(true)}> + Add override + + )} +
+ {!addingOverride && !overrideOpsDisabled && overrides.length === 0 && ( + handleEdit(ADD_OVERRIDE_FBA_KEY)(true)}> + Add overrides + + )} + {(addingOverride || overrides.length > 0) && ( + false, + R.range(0, setupValues[OVERRIDES_KEY].length) + )} + save={handleSubmitOverrides} + reset={() => handleResetForm} + action={it => handleEdit(it.machine.name)(true)} + initialValues={initialValues} + validationSchema={validationSchema} + // data={setupValues[OVERRIDES_KEY]} + data={[]} + elements={elements} + double + /> + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // {addingOverride && ( + // + // )} + // {overrides.map((override, idx) => { + // const machine = override[MACHINE_KEY] + + // const fields = { + // [PERCENTAGE_KEY]: { + // name: `${machine.name}-${CASH_IN_FULL_KEY}-${PERCENTAGE_KEY}`, + // value: override[CASH_IN_FULL_KEY][PERCENTAGE_KEY] + // }, + // [NUMERARY_KEY]: { + // name: `${machine.name}-${CASH_IN_FULL_KEY}-${NUMERARY_KEY}`, + // value: override[CASH_IN_FULL_KEY][NUMERARY_KEY] + // }, + // [CASSETTE_1_KEY]: { + // name: `${machine.name}-${CASH_OUT_EMPTY_KEY}-${CASSETTE_1_KEY}`, + // value: override[CASH_OUT_EMPTY_KEY][CASSETTE_1_KEY] + // }, + // [CASSETTE_2_KEY]: { + // name: `${machine.name}-${CASH_OUT_EMPTY_KEY}-${CASSETTE_2_KEY}`, + // value: override[CASH_OUT_EMPTY_KEY][CASSETTE_2_KEY] + // } + // } + + // const editing = editingState[machine.name] + // const disabled = isDisabled(editingState, machine.name) + + // return ( + // + // ) + // })} + // + //
Machine + // Percentage + // + // Amount + // + // Cash-out 1 + // + // Cash-out 2 + // + // Edit + //
+ )} +
+
+ ) +} + +export default FiatBalanceAlerts diff --git a/new-lamassu-admin/src/pages/Notifications/Inputs.js b/new-lamassu-admin/src/pages/Notifications/Inputs.js new file mode 100644 index 00000000..8bcc0b70 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/Inputs.js @@ -0,0 +1,340 @@ +import React from 'react' +import * as R from 'ramda' +import classnames from 'classnames' +import * as Yup from 'yup' +import { Form, Formik, Field as FormikField } from 'formik' +import { makeStyles } from '@material-ui/core' + +import { + H4, + Label1, + Info1, + TL2, + Info2, + Label2 +} from 'src/components/typography' +import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg' +import { ReactComponent as DisabledEditIcon } from 'src/styling/icons/action/edit/disabled.svg' +import { Link } from 'src/components/buttons' +import TextInputFormik from 'src/components/inputs/formik/TextInput' + +import { + localStyles, + inputSectionStyles, + percentageAndNumericInputStyles, + multiplePercentageInputStyles, + fieldStyles +} from './Notifications.styles' + +const fieldUseStyles = makeStyles(R.mergeAll([fieldStyles, localStyles])) + +const Field = ({ + editing, + field, + displayValue, + decoration, + className, + setError, + ...props +}) => { + const classes = fieldUseStyles() + + const classNames = { + [className]: true, + [classes.field]: true, + [classes.notEditing]: !editing, + [classes.percentageInput]: decoration === '%' + } + + return ( +
+ {field.label && {field.label}} +
+ {!editing && props.large && ( + <> + {displayValue(field.value)} + + )} + {!editing && !props.large && ( + <> + {displayValue(field.value)} + + )} + {editing && ( + setError(null)} + {...props} + /> + )} + {props.large && ( + <> + {decoration} + + )} + {!props.large && ( + <> + {decoration} + + )} +
+
+ ) +} + +const useStyles = makeStyles(inputSectionStyles) + +const Header = ({ title, editing, disabled, setEditing }) => { + const classes = useStyles() + + return ( +
+

{title}

+ {!editing && !disabled && ( + + )} + {disabled && ( +
+ +
+ )} + {editing && ( +
+ + Save + + + Cancel + +
+ )} +
+ ) +} + +const BigNumericInput = ({ + title, + field, + editing, + disabled, + setEditing, + handleSubmit, + setError, + className +}) => { + const classes = useStyles() + + const { name, value } = field + + return ( +
+ { + handleSubmit(values) + }} + onReset={(values, bag) => { + setEditing(false) + setError(null) + }}> +
+
setEditing(true)} + /> +
+ (x === '' ? '-' : x)} + decoration="EUR" + large + setError={setError} + /> +
+ + +
+ ) +} + +const percentageAndNumericInputUseStyles = makeStyles( + R.merge(inputSectionStyles, percentageAndNumericInputStyles) +) + +const BigPercentageAndNumericInput = ({ + title, + fields, + editing, + disabled, + setEditing, + handleSubmit, + setError, + className +}) => { + const classes = percentageAndNumericInputUseStyles() + + const { percentage, numeric } = fields + const { name: percentageName, value: percentageValue } = percentage + const { name: numericName, value: numericValue } = numeric + + return ( +
+ { + handleSubmit(values) + }} + onReset={(values, bag) => { + setEditing(false) + setError(null) + }}> +
+
setEditing(true)} + /> +
+
+
+
+
+ (x === '' ? '-' : x)} + decoration="%" + large + setError={setError} + /> + (x === '' ? '-' : x)} + decoration="EUR" + large + setError={setError} + /> +
+
+ + +
+ ) +} + +const multiplePercentageInputUseStyles = makeStyles( + R.merge(inputSectionStyles, multiplePercentageInputStyles) +) + +const MultiplePercentageInput = ({ + title, + fields, + editing, + disabled, + setEditing, + handleSubmit, + setError, + className +}) => { + const classes = multiplePercentageInputUseStyles() + + const initialValues = R.fromPairs(R.map(f => [f.name, f.value], fields)) + const validationSchemaShape = R.fromPairs( + R.map( + f => [ + f.name, + Yup.number() + .integer() + .min(0) + .max(100) + .required() + ], + fields + ) + ) + + return ( +
+ { + handleSubmit(values) + }} + onReset={(values, bag) => { + setEditing(false) + setError(null) + }}> +
+
setEditing(true)} + /> +
+ {fields.map((field, idx) => ( +
+
+
+
+
+ {field.title} + (x === '' ? '-' : x)} + decoration="%" + className={classes.percentageInput} + large + setError={setError} + /> +
+
+ ))} +
+ + +
+ ) +} + +export { + Field, + BigNumericInput, + BigPercentageAndNumericInput, + MultiplePercentageInput +} diff --git a/new-lamassu-admin/src/pages/Notifications/Notifications.js b/new-lamassu-admin/src/pages/Notifications/Notifications.js new file mode 100644 index 00000000..0c919469 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/Notifications.js @@ -0,0 +1,232 @@ +import React, { useState } from 'react' +import * as R from 'ramda' +import { gql } from 'apollo-boost' +import { makeStyles } from '@material-ui/core' +import { useQuery, useMutation } from '@apollo/react-hooks' + +import { TL1 } from 'src/components/typography' +import Title from 'src/components/Title' +import ErrorMessage from 'src/components/ErrorMessage' +import commonStyles from 'src/pages/common.styles' + +import { localStyles } from './Notifications.styles' +import Setup from './Setup' +import TransactionAlerts from './TransactionAlerts' +import { + SETUP_KEY, + TRANSACTION_ALERTS_KEY, + HIGH_VALUE_TRANSACTION_KEY, + CASH_IN_FULL_KEY, + FIAT_BALANCE_ALERTS_KEY, + CASH_OUT_EMPTY_KEY, + CASSETTE_1_KEY, + CASSETTE_2_KEY, + OVERRIDES_KEY, + PERCENTAGE_KEY, + NUMERARY_KEY, + CRYPTO_BALANCE_ALERTS_KEY, + LOW_BALANCE_KEY, + HIGH_BALANCE_KEY, + ADD_OVERRIDE_FBA_KEY, + ADD_OVERRIDE_CBA_KEY, + EMAIL_KEY, + BALANCE_KEY, + TRANSACTIONS_KEY, + COMPLIANCE_KEY, + SECURITY_KEY, + ERRORS_KEY, + ACTIVE_KEY, + SMS_KEY +} from './aux.js' +import FiatBalanceAlerts from './FiatBalanceAlerts' +import CryptoBalanceAlerts from './CryptoBalanceAlerts' + +const fiatBalanceAlertsInitialValues = { + [CASH_IN_FULL_KEY]: { + [PERCENTAGE_KEY]: '', + [NUMERARY_KEY]: '' + }, + [CASH_OUT_EMPTY_KEY]: { + [CASSETTE_1_KEY]: '', + [CASSETTE_2_KEY]: '' + } +} + +const initialValues = { + [SETUP_KEY]: { + [EMAIL_KEY]: { + [BALANCE_KEY]: false, + [TRANSACTIONS_KEY]: false, + [COMPLIANCE_KEY]: false, + [SECURITY_KEY]: false, + [ERRORS_KEY]: false, + [ACTIVE_KEY]: false + }, + [SMS_KEY]: { + [BALANCE_KEY]: false, + [TRANSACTIONS_KEY]: false, + [COMPLIANCE_KEY]: false, + [SECURITY_KEY]: false, + [ERRORS_KEY]: false, + [ACTIVE_KEY]: false + } + }, + [TRANSACTION_ALERTS_KEY]: { + [HIGH_VALUE_TRANSACTION_KEY]: '' + }, + [FIAT_BALANCE_ALERTS_KEY]: { + ...fiatBalanceAlertsInitialValues, + [OVERRIDES_KEY]: [] + }, + [CRYPTO_BALANCE_ALERTS_KEY]: { + [LOW_BALANCE_KEY]: '', + [HIGH_BALANCE_KEY]: '', + [OVERRIDES_KEY]: [] + } +} + +const initialEditingState = { + [HIGH_VALUE_TRANSACTION_KEY]: false, + [CASH_IN_FULL_KEY]: false, + [CASH_OUT_EMPTY_KEY]: false, + [LOW_BALANCE_KEY]: false, + [HIGH_BALANCE_KEY]: false, + [ADD_OVERRIDE_FBA_KEY]: false, + [ADD_OVERRIDE_CBA_KEY]: false +} + +const GET_INFO = gql` + { + config + } +` + +const SAVE_CONFIG = gql` + mutation Save($config: JSONObject) { + saveConfig(config: $config) + } +` + +const styles = R.merge(commonStyles, localStyles) + +const useStyles = makeStyles(styles) + +const SectionHeader = ({ error, children }) => { + const classes = useStyles() + + return ( +
+ {children} + {error && Failed to save changes} +
+ ) +} + +const Notifications = () => { + const [state, setState] = useState(null) + const [editingState, setEditingState] = useState(initialEditingState) + const [error, setError] = useState(null) + const [tryingToSave, setTryingToSave] = useState(null) + const [saveConfig] = useMutation(SAVE_CONFIG, { + onCompleted: data => { + const { notifications } = data.saveConfig + setState(notifications) + setEditingState(R.map(x => false, editingState)) + setTryingToSave(null) + setError(null) + }, + onError: e => { + setError({ section: tryingToSave, error: e }) + } + }) + const classes = useStyles() + + useQuery(GET_INFO, { + onCompleted: data => { + const { notifications } = data.config + if (notifications) { + const { [OVERRIDES_KEY]: machines } = notifications[ + FIAT_BALANCE_ALERTS_KEY + ] + const editingFiatBalanceAlertsOverrides = R.fromPairs( + machines.map(machine => [machine.name, false]) + ) + setEditingState({ + ...editingState, + ...editingFiatBalanceAlertsOverrides + }) + } + setState(notifications ?? initialValues) + }, + fetchPolicy: 'network-only' + }) + + const save = it => { + return saveConfig({ variables: { config: { notifications: it } } }) + } + + const handleEditingClick = (key, state) => { + setEditingState(R.merge(editingState, { [key]: state })) + } + + const curriedSave = R.curry((key, values) => { + setTryingToSave(key) + save(R.mergeDeepRight(state)({ [key]: values })) + }) + + if (!state) return null + + return ( + <> +
+
+ Notifications +
+
+
+ + Setup + + +
+
+ + Transaction alerts + + +
+
+ + Fiat balance alerts + + +
+
+ + Crypto balance alerts + + +
+ + ) +} + +export default Notifications diff --git a/new-lamassu-admin/src/pages/Notifications/Notifications.styles.js b/new-lamassu-admin/src/pages/Notifications/Notifications.styles.js new file mode 100644 index 00000000..3f021375 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/Notifications.styles.js @@ -0,0 +1,252 @@ +import { offColor, primaryColor } from 'src/styling/variables' +import theme from 'src/styling/theme' + +const localStyles = { + section: { + marginBottom: 72, + '&:last-child': { + marginBottom: 150 + } + }, + sectionHeader: { + display: 'flex', + alignItems: 'center', + '& > :first-child': { + marginRight: 20 + } + }, + sectionTitle: { + color: offColor, + margin: [[16, 0, 16, 0]] + }, + button: { + border: 'none', + backgroundColor: 'transparent', + cursor: 'pointer', + height: '100%' + }, + defaults: { + display: 'flex', + '& > div': { + display: 'flex', + alignItems: 'center' + }, + '& > div:first-child': { + borderRight: [['solid', 1, primaryColor]] + }, + '& > div:not(:first-child)': { + marginLeft: 56 + } + }, + overrides: { + display: 'inline-block' + }, + overridesTitle: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'baseline', + '& > :first-child': { + color: offColor, + margin: [[0, 0, 24, 0]] + }, + '& > button': { + height: '100%' + } + }, + displayValue: { + display: 'flex', + alignItems: 'baseline', + '& > p:first-child': { + margin: [[0, 4, 5, 0]] + }, + '&> p:last-child': { + margin: 0 + } + }, + edit: { + display: 'flex', + justifyContent: 'flex-end', + '& > :first-child': { + marginRight: 16 + } + }, + eRowField: { + display: 'inline-block', + height: '100%' + }, + textInput: { + '& .MuiInputBase-input': { + width: 80 + } + } +} + +const inputSectionStyles = { + header: { + display: 'flex', + alignItems: 'center', + marginBottom: 24, + height: 26, + '& > :first-child': { + flexShrink: 2, + margin: 0, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis' + }, + '& button': { + border: 'none', + backgroundColor: 'transparent', + cursor: 'pointer' + } + }, + editButton: { + marginLeft: 16 + }, + disabledButton: { + padding: [[0, theme.spacing(1)]], + lineHeight: 'normal', + marginLeft: 16 + }, + editingButtons: { + display: 'flex', + marginLeft: 16, + '& > :not(:last-child)': { + marginRight: 20 + } + }, + percentageDisplay: { + position: 'relative', + width: 76, + height: 118, + border: [['solid', 4, primaryColor]], + marginRight: 12, + '& > div': { + position: 'absolute', + bottom: 0, + left: 0, + width: '100%', + backgroundColor: primaryColor, + transition: [['height', '0.5s']], + transitionTimingFunction: 'ease-out' + } + }, + inputColumn: { + '& > div:not(:last-child)': { + marginBottom: 4 + } + } +} + +const fiatBalanceAlertsStyles = { + cashInWrapper: { + width: 254 + }, + doubleLevelHead: { + '& > div > div': { + marginRight: 72 + } + }, + doubleLevelRow: { + '& > div': { + marginRight: 72 + } + }, + fbaDefaults: { + '& > div': { + height: 185 + }, + marginBottom: 69 + } +} + +const cryptoBalanceAlertsStyles = { + lowBalance: { + width: 254, + '& form': { + width: 217 + } + }, + cbaDefaults: { + '& > div': { + height: 135 + }, + marginBottom: 36 + }, + overridesTable: { + width: 648 + } +} + +const percentageAndNumericInputStyles = { + body: { + display: 'flex', + alignItems: 'center' + } +} + +const multiplePercentageInputStyles = { + body: { + display: 'flex', + '& > div': { + display: 'flex' + }, + '& > div:not(:last-child)': { + marginRight: 43 + } + }, + title: { + margin: [[2, 0, 12, 0]] + } +} + +const fieldStyles = { + field: { + position: 'relative', + display: 'flex', + flexDirection: 'column', + height: 53, + padding: 0, + '& > div': { + display: 'flex', + alignItems: 'baseline', + '& > p:first-child': { + margin: [[0, 4, 5, 0]] + }, + '&> p:last-child': { + margin: [[0, 0, 0, 3]] + } + }, + '& .MuiInputBase-input': { + width: 80 + } + }, + label: { + margin: 0 + }, + notEditing: { + '& > div': { + margin: [[5, 0, 0, 0]], + '& > p:first-child': { + height: 16 + } + } + }, + percentageInput: { + '& > div': { + '& .MuiInputBase-input': { + width: 30 + } + } + } +} + +export { + localStyles, + inputSectionStyles, + fiatBalanceAlertsStyles, + cryptoBalanceAlertsStyles, + percentageAndNumericInputStyles, + multiplePercentageInputStyles, + fieldStyles +} diff --git a/new-lamassu-admin/src/pages/Notifications/Setup.js b/new-lamassu-admin/src/pages/Notifications/Setup.js new file mode 100644 index 00000000..2c824940 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/Setup.js @@ -0,0 +1,148 @@ +import React from 'react' +import * as R from 'ramda' + +import { Switch } from 'src/components/inputs' +import { + Table as FakeTable, + THead, + TBody, + Tr, + Td, + Th +} from 'src/components/fake-table/Table' + +import { + BALANCE_KEY, + TRANSACTIONS_KEY, + COMPLIANCE_KEY, + SECURITY_KEY, + ERRORS_KEY, + ACTIVE_KEY, + CHANNEL_KEY, + EMAIL_KEY, + SMS_KEY +} from './aux' + +const elements = [ + { + header: 'Channel', + name: CHANNEL_KEY, + size: 129, + textAlign: 'left' + }, + { + header: 'Balance', + name: BALANCE_KEY, + size: 152, + textAlign: 'center' + }, + { + header: 'Transactions', + name: TRANSACTIONS_KEY, + size: 184, + textAlign: 'center' + }, + { + header: 'Compliance', + name: COMPLIANCE_KEY, + size: 178, + textAlign: 'center' + }, + { + header: 'Security', + name: SECURITY_KEY, + size: 152, + textAlign: 'center' + }, + { + header: 'Errors', + name: ERRORS_KEY, + size: 142, + textAlign: 'center' + }, + { + header: 'Active', + name: ACTIVE_KEY, + size: 263, + textAlign: 'center' + } +] + +const Row = ({ channel, columns, values, save }) => { + const { active } = values + + const findField = name => R.find(R.propEq('name', name))(columns) + const findSize = name => findField(name).size + const findAlign = name => findField(name).textAlign + + const Cell = ({ name, disabled }) => { + const handleChange = name => event => { + save(R.mergeDeepRight(values, { [name]: event.target.checked })) + } + + return ( + + + + ) + } + + return ( + + + {channel} + + + + + + + + + ) +} + +const Setup = ({ values: setupValues, save }) => { + const saveSetup = R.curry((key, values) => + save(R.mergeDeepRight(setupValues, { [key]: values })) + ) + + return ( +
+ + + {elements.map(({ size, className, textAlign, header }, idx) => ( + + {header} + + ))} + + + + + + +
+ ) +} + +export default Setup diff --git a/new-lamassu-admin/src/pages/Notifications/TransactionAlerts.js b/new-lamassu-admin/src/pages/Notifications/TransactionAlerts.js new file mode 100644 index 00000000..ded538a9 --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/TransactionAlerts.js @@ -0,0 +1,41 @@ +import React from 'react' +import * as R from 'ramda' + +import { HIGH_VALUE_TRANSACTION_KEY, isDisabled } from './aux.js' +import { BigNumericInput } from './Inputs' + +const TransactionAlerts = ({ + value: setupValue, + save, + editingState, + handleEditingClick, + setError +}) => { + const editing = editingState[HIGH_VALUE_TRANSACTION_KEY] + + const handleEdit = R.curry(handleEditingClick) + + const handleSubmit = it => save(it) + + const field = { + name: HIGH_VALUE_TRANSACTION_KEY, + label: 'Alert me over', + value: setupValue[HIGH_VALUE_TRANSACTION_KEY] + } + + return ( +
+ +
+ ) +} + +export default TransactionAlerts diff --git a/new-lamassu-admin/src/pages/Notifications/aux.js b/new-lamassu-admin/src/pages/Notifications/aux.js new file mode 100644 index 00000000..b2f1670d --- /dev/null +++ b/new-lamassu-admin/src/pages/Notifications/aux.js @@ -0,0 +1,61 @@ +import * as R from 'ramda' + +const EMAIL_KEY = 'email' +const SMS_KEY = 'sms' +const BALANCE_KEY = 'balance' +const TRANSACTIONS_KEY = 'transactions' +const COMPLIANCE_KEY = 'compliance' +const SECURITY_KEY = 'security' +const ERRORS_KEY = 'errors' +const ACTIVE_KEY = 'active' +const SETUP_KEY = 'setup' +const CHANNEL_KEY = 'channel' +const TRANSACTION_ALERTS_KEY = 'transactionAlerts' +const HIGH_VALUE_TRANSACTION_KEY = 'highValueTransaction' +const FIAT_BALANCE_ALERTS_KEY = 'fiatBalanceAlerts' +const CASH_IN_FULL_KEY = 'cashInFull' +const CASH_OUT_EMPTY_KEY = 'cashOutEmpty' +const MACHINE_KEY = 'machine' +const PERCENTAGE_KEY = 'percentage' +const NUMERARY_KEY = 'numerary' +const CASSETTE_1_KEY = 'cassete1' +const CASSETTE_2_KEY = 'cassete2' +const OVERRIDES_KEY = 'overrides' +const CRYPTO_BALANCE_ALERTS_KEY = 'cryptoBalanceAlerts' +const LOW_BALANCE_KEY = 'lowBalance' +const HIGH_BALANCE_KEY = 'highBalance' +const ADD_OVERRIDE_FBA_KEY = 'addOverrideFBA' +const ADD_OVERRIDE_CBA_KEY = 'addOverrideCBA' + +const isDisabled = (state, self) => + R.any(x => x === true, R.values(R.omit([self], state))) + +export { + EMAIL_KEY, + SMS_KEY, + BALANCE_KEY, + TRANSACTIONS_KEY, + COMPLIANCE_KEY, + SECURITY_KEY, + ERRORS_KEY, + ACTIVE_KEY, + SETUP_KEY, + CHANNEL_KEY, + TRANSACTION_ALERTS_KEY, + HIGH_VALUE_TRANSACTION_KEY, + FIAT_BALANCE_ALERTS_KEY, + CASH_IN_FULL_KEY, + CASH_OUT_EMPTY_KEY, + MACHINE_KEY, + PERCENTAGE_KEY, + NUMERARY_KEY, + CASSETTE_1_KEY, + CASSETTE_2_KEY, + OVERRIDES_KEY, + CRYPTO_BALANCE_ALERTS_KEY, + LOW_BALANCE_KEY, + HIGH_BALANCE_KEY, + ADD_OVERRIDE_FBA_KEY, + ADD_OVERRIDE_CBA_KEY, + isDisabled +} diff --git a/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js b/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js index 16574e2e..b9044ec9 100644 --- a/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js +++ b/new-lamassu-admin/src/pages/OperatorInfo/OperatorInfo.js @@ -53,6 +53,7 @@ const OperatorInfo = () => { {isSelected(CONTACT_INFORMATION) && } {isSelected(TERMS_CONDITIONS) && } {isSelected(COIN_ATM_RADAR) && } + {isSelected(TERMS_CONDITIONS) && }
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..d36ac905 --- /dev/null +++ b/new-lamassu-admin/src/pages/common.styles.js @@ -0,0 +1,11 @@ +export default { + titleWrapper: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + flexDirection: 'row' + }, + titleAndButtonsContainer: { + display: 'flex' + } +} diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index c2afad75..25bf7a88 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -1,30 +1,19 @@ import * as R from 'ramda' - import React from 'react' - import { Route, Redirect, Switch } from 'react-router-dom' import AuthRegister from 'src/pages/AuthRegister' - import Commissions from 'src/pages/Commissions' - +import Customers from 'src/pages/Customers' import Funding from 'src/pages/Funding' - import Locales from 'src/pages/Locales' - import MachineLogs from 'src/pages/MachineLogs' - +import Notifications from 'src/pages/Notifications/Notifications' import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo' - import ServerLogs from 'src/pages/ServerLogs' - import Services from 'src/pages/Services/Services' - import Transactions from 'src/pages/Transactions/Transactions' - -import Customers from '../pages/Customers' - -import MachineStatus from '../pages/maintenance/MachineStatus' +import MachineStatus from 'src/pages/maintenance/MachineStatus' const tree = [ { key: 'transactions', label: 'Transactions', route: '/transactions' }, @@ -65,6 +54,11 @@ const tree = [ label: '3rd party services', route: '/settings/3rd-party-services' }, + { + key: 'notifications', + label: 'Notifications', + route: '/settings/notifications' + }, { key: 'info', label: 'Operator Info', route: '/settings/operator-info' } ] }, @@ -120,6 +114,7 @@ const Routes = () => ( + diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 3b0902f8..c2e69c81 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -81,6 +81,7 @@ const smallestFontSize = fontSize5 const inputFontSize = fontSize3 const inputFontSizeLg = fontSize1 const inputFontWeight = 500 +const inputFontWeightLg = 700 const inputFontFamily = fontSecondary // Breakpoints @@ -99,6 +100,8 @@ if (version === 8) { tableCellHeight = spacer * 7 - 2 } +const tableDoubleHeaderHeight = tableHeaderHeight * 2 + const tableSmCellHeight = 30 const tableLgCellHeight = 76 @@ -159,6 +162,7 @@ export { inputFontSizeLg, inputFontFamily, inputFontWeight, + inputFontWeightLg, // screen sizes sm, md, @@ -170,6 +174,7 @@ export { mainWidth, // table sizes tableHeaderHeight, + tableDoubleHeaderHeight, tableCellHeight, tableSmCellHeight, tableLgCellHeight,