feat: notifications page

This commit is contained in:
Luis Félix 2020-01-21 13:13:40 +00:00 committed by Josh Harvey
parent 2b71c08444
commit b6e7d98b72
25 changed files with 2615 additions and 198 deletions

View file

@ -11,7 +11,11 @@ low(adapter).then(it => {
function saveConfig (config) { function saveConfig (config) {
const currentState = db.getState() const currentState = db.getState()
const newState = _.merge(currentState, config) const newState = _.mergeWith((objValue, srcValue) => {
if (_.isArray(objValue)) {
return srcValue
}
}, currentState, config)
db.setState(newState) db.setState(newState)
return db.write() return db.write()

View file

@ -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 (
<button className={classnames(classes.button, className)} {...props}>
<AddIcon />
{children}
</button>
)
})
export default SimpleButton

View file

@ -1,8 +1,17 @@
import ActionButton from './ActionButton' import ActionButton from './ActionButton'
import AddButton from './AddButton'
import Button from './Button' import Button from './Button'
import FeatureButton from './FeatureButton' import FeatureButton from './FeatureButton'
import IDButton from './IDButton' import IDButton from './IDButton'
import Link from './Link' import Link from './Link'
import SimpleButton from './SimpleButton' import SimpleButton from './SimpleButton'
export { Button, Link, SimpleButton, ActionButton, FeatureButton, IDButton } export {
Button,
Link,
SimpleButton,
ActionButton,
FeatureButton,
IDButton,
AddButton
}

View file

@ -1,83 +1,250 @@
import { Form, Formik, FastField, useFormikContext } from 'formik' import React, { memo } from 'react'
import React, { useState, 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 { 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 styles = {
<FastField name={name} component={component} {...props} /> button: {
) border: 'none',
backgroundColor: 'transparent',
const ERow = memo(({ elements }) => { outline: 0,
const { values, submitForm, resetForm, errors } = useFormikContext() cursor: 'pointer'
const [editing, setEditing] = useState(false) },
actionCol: {
const innerSave = () => { display: 'flex',
submitForm() marginLeft: 'auto'
},
actionColDisplayMode: {
justifyContent: 'center'
},
actionColEditMode: {
justifyContent: 'flex-end',
'& > :first-child': {
marginRight: 16
}
},
textInput: {
'& > .MuiInputBase-input': {
width: 282
}
} }
// doubleLevelRow: {
// '& > div': {
// marginRight: 72
// }
// }
}
const innerCancel = () => { const useStyles = makeStyles(styles)
setEditing(false)
resetForm()
}
return ( const ERow = memo(
<Tr ({ elements, editing, setEditing, disableAction, action }) => {
error={errors && errors.length} const classes = useStyles()
errorMessage={errors && errors.toString()}>
{elements.map( const Cell = ({
( name,
{ input,
type,
display,
className,
size,
textAlign,
inputProps,
editing
}) => {
return (
<Td size={size} textAlign={textAlign}>
{editing && (
<Field
id={name}
name={name}
component={input}
className={className}
{...inputProps}
/>
)}
{!editing && type === 'text' && (
<TextInputDisplay display={display} {...inputProps} />
)}
</Td>
)
}
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 <DeleteIcon />
if (action === 'delete' && disabled) return <DisabledDeleteIcon />
}
return (
<Tr
error={errors && errors.length}
errorMessage={errors && errors.toString()}>
{R.init(elements).map((element, idx) => {
const colClasses = {
[classes.textInput]: true
}
if (Array.isArray(element)) {
return (
<CellDoubleLevel key={idx} className={classes.doubleLevelRow}>
{R.map(
(
{
name,
input,
size,
textAlign,
type,
view = it => it?.toString(),
inputProps
},
idx
) => (
<Cell
key={name}
name={name}
input={input}
type={type}
display={view(values[name])}
className={classnames(colClasses)}
size={size}
textAlign={textAlign}
inputProps={inputProps}
editing={editing}
/>
// <Td size={sizes.cashOut1} textAlign="right">
// <Field
// editing={editing}
// field={fields[CASSETTE_1_KEY]}
// displayValue={x => (x === '' ? '-' : x)}
// decoration="%"
// className={classes.eRowField}
// setError={setError}
// />
// </Td>
)
)(R.tail(element))}
</CellDoubleLevel>
)
}
const {
name, name,
input, input,
size, size,
textAlign, textAlign,
type,
view = it => it?.toString(), view = it => it?.toString(),
inputProps inputProps
}, } = element
idx
) => (
<Td key={idx} size={size} textAlign={textAlign}>
{editing && getField(name, input, inputProps)}
{!editing && view(values[name])}
</Td>
)
)}
<Td size={175}>
{editing ? (
<>
<Link
style={{ marginRight: '20px' }}
color="secondary"
onClick={innerCancel}>
Cancel
</Link>
<Link color="primary" onClick={innerSave}>
Save
</Link>
</>
) : (
<Link color="primary" onClick={() => setEditing(true)}>
Edit
</Link>
)}
</Td>
</Tr>
)
})
const ERowWithFormik = memo(({ value, validationSchema, save, elements }) => { return (
return ( <Cell
<Formik key={idx}
enableReinitialize name={name}
initialValues={value} input={input}
validationSchema={validationSchema} type={type}
onSubmit={save}> display={view(values[name])}
<Form> className={classnames(colClasses)}
<ERow elements={elements} /> size={size}
</Form> textAlign={textAlign}
</Formik> inputProps={inputProps}
) editing={editing}
}) />
// <Td key={idx} size={size} textAlign={textAlign}>
// {editing && (
// <Field
// id={name}
// name={name}
// component={input}
// className={classnames(colClasses)}
// {...inputProps}
// />
// )}
// {!editing && type === 'text' && (
// <TextInputDisplay
// display={view(values[name])}
// {...inputProps}
// />
// )}
// </Td>
)
})}
<Td size={actionCol.size} className={classnames(actionColClasses)}>
{!editing && !disableAction && (
<button
type="button"
className={classes.button}
onClick={() => action(values)}>
{icon(actionCol.name, disableAction)}
</button>
)}
{!editing && disableAction && (
<div>{icon(actionCol.name, disableAction)}</div>
)}
{editing && (
<>
<Link color="secondary" type="reset">
Cancel
</Link>
<Link color="primary" type="submit">
Save
</Link>
</>
)}
</Td>
</Tr>
)
}
)
const ERowWithFormik = memo(
({
initialValues,
validationSchema,
save,
reset,
action,
elements,
editing,
disableAction
}) => {
return (
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={save}
onReset={reset}>
<Form>
<ERow
elements={elements}
editing={editing}
disableAction={disableAction}
action={action}
/>
</Form>
</Formik>
)
}
)
export default ERowWithFormik export default ERowWithFormik

View file

@ -1,34 +1,124 @@
import React, { memo } from 'react' 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 { startCase } from 'src/utils/string'
import ERow from './Row' import ERow from './Row'
const ETable = memo(({ elements = [], data = [], save, validationSchema }) => { const ETHead = memo(({ elements, className }) => {
const action = R.last(elements)
return ( return (
<Table> <THead className={className?.root}>
<THead> {R.init(elements).map(({ name, size, display, textAlign }, idx) => (
{elements.map(({ name, size, header, textAlign }, idx) => ( <Th
<Td header key={idx} size={size} textAlign={textAlign}> id={name}
{header || startCase(name)} key={idx}
</Td> size={size}
))} textAlign={textAlign}
<Td header size={175} /> className={className?.cell}>
</THead> {display}
<TBody> </Th>
{data.map((it, idx) => ( ))}
<ERow <Th size={action.size} action>
key={idx} {startCase(action.name)}
value={it} </Th>
elements={elements} </THead>
save={save}
validationSchema={validationSchema}
/>
))}
</TBody>
</Table>
) )
}) })
const ETDoubleHead = memo(({ elements, className }) => {
const action = R.last(elements)
return (
<TDoubleLevelHead className={className?.root}>
{R.init(elements).map((element, idx) => {
if (Array.isArray(element)) {
return (
<ThDoubleLevel
key={idx}
title={element[0].display}
className={className?.cell}>
{R.map(({ name, size, display, textAlign }) => (
<Th key={name} id={name} size={size} textAlign={textAlign}>
{display}
</Th>
))(R.tail(element))}
</ThDoubleLevel>
)
}
const { name, size, display, textAlign } = element
return (
<Th id={idx} key={name} size={size} textAlign={textAlign}>
{display}
</Th>
)
})}
<Th size={action.size} action>
{startCase(action.name)}
</Th>
</TDoubleLevelHead>
)
})
const ETable = memo(
({
elements = [],
data = [],
save,
reset,
action,
initialValues,
validationSchema,
editing,
addingRow,
disableAction,
className,
double
}) => {
return (
<Table className={className}>
{!double && <ETHead elements={elements} />}
{double && (
<ETDoubleHead elements={elements} className={className?.head} />
)}
<TBody>
{addingRow && (
<ERow
elements={elements}
initialValues={initialValues}
save={save}
reset={reset}
validationSchema={validationSchema}
editing
/>
)}
{data.map((it, idx) => (
<ERow
key={idx}
initialValues={it}
elements={elements}
save={save}
reset={it => reset(it)}
action={action}
validationSchema={validationSchema}
disableAction={disableAction}
editing={editing[idx]}
/>
))}
</TBody>
</Table>
)
}
)
export default ETable export default ETable

View file

@ -10,15 +10,17 @@ import {
tableHeaderHeight, tableHeaderHeight,
tableErrorColor, tableErrorColor,
spacer, spacer,
white white,
tableDoubleHeaderHeight,
offColor
} from 'src/styling/variables' } from 'src/styling/variables'
import typographyStyles from 'src/components/typography/styles' import typographyStyles from 'src/components/typography/styles'
const { tl2, p } = typographyStyles const { tl2, p, label1 } = typographyStyles
const useStyles = makeStyles({ const useStyles = makeStyles({
body: { body: {
borderSpacing: '0 4px' borderSpacing: [[0, 4]]
}, },
header: { header: {
extend: tl2, extend: tl2,
@ -26,16 +28,49 @@ const useStyles = makeStyles({
height: tableHeaderHeight, height: tableHeaderHeight,
textAlign: 'left', textAlign: 'left',
color: white, color: white,
// display: 'flex' display: 'flex',
alignItems: 'center'
},
doubleHeader: {
extend: tl2,
backgroundColor: tableHeaderColor,
height: tableDoubleHeaderHeight,
color: white,
display: 'table-row' 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: { td: {
padding: `0 ${spacer * 3}px` padding: [[0, spacer * 3]]
}, },
tdHeader: { tdHeader: {
verticalAlign: 'middle', verticalAlign: 'middle',
display: 'table-cell', display: 'table-cell',
padding: `0 ${spacer * 3}px` padding: [[0, spacer * 3]]
}, },
trError: { trError: {
backgroundColor: tableErrorColor backgroundColor: tableErrorColor
@ -59,9 +94,12 @@ const useStyles = makeStyles({
'&:before': { '&:before': {
height: 0 height: 0
}, },
margin: '4px 0', margin: [[4, 0]],
width: 'min-content', width: '100%',
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.08)' boxShadow: [[0, 0, 4, 0, 'rgba(0, 0, 0, 0.08)']]
},
actionCol: {
marginLeft: 'auto'
} }
}) })
@ -76,16 +114,27 @@ const THead = ({ children, className }) => {
return <div className={classnames(className, classes.header)}>{children}</div> return <div className={classnames(className, classes.header)}>{children}</div>
} }
const TDoubleLevelHead = ({ children, className }) => {
const classes = useStyles()
return (
<div className={classnames(className, classes.doubleHeader)}>
{children}
</div>
)
}
const TBody = ({ children, className }) => { const TBody = ({ children, className }) => {
const classes = useStyles() const classes = useStyles()
return <div className={classnames(className, classes.body)}>{children}</div> return <div className={classnames(className, classes.body)}>{children}</div>
} }
const Td = ({ children, header, className, size = 100, textAlign }) => { const Td = ({ children, header, className, size = 100, textAlign, action }) => {
const classes = useStyles() const classes = useStyles()
const classNames = { const classNames = {
[classes.td]: true, [classes.td]: true,
[classes.tdHeader]: header [classes.tdHeader]: header,
[classes.actionCol]: action
} }
return ( return (
@ -105,6 +154,27 @@ const Th = ({ children, ...props }) => {
) )
} }
const ThDoubleLevel = ({ title, children, className }) => {
const classes = useStyles()
return (
<div className={classnames(className, classes.thDoubleLevel)}>
<div>{title}</div>
<div>{children}</div>
</div>
)
}
const CellDoubleLevel = ({ children, className }) => {
const classes = useStyles()
return (
<div className={classnames(className, classes.cellDoubleLevel)}>
{children}
</div>
)
}
const Tr = ({ error, errorMessage, children, className }) => { const Tr = ({ error, errorMessage, children, className }) => {
const classes = useStyles() const classes = useStyles()
const cardClasses = { root: classes.cardContentRoot } const cardClasses = { root: classes.cardContentRoot }
@ -135,4 +205,15 @@ const EditCell = ({ save, cancel }) => (
</Td> </Td>
) )
export { Table, THead, TBody, Tr, Td, Th, EditCell } export {
Table,
THead,
TDoubleLevelHead,
TBody,
Tr,
Td,
Th,
ThDoubleLevel,
CellDoubleLevel,
EditCell
}

View file

@ -13,7 +13,16 @@ import {
} from './commons' } from './commons'
const Autocomplete = memo( 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 { name, value, onBlur } = props.field
const { touched, errors, setFieldValue } = props.form const { touched, errors, setFieldValue } = props.form
@ -22,7 +31,11 @@ const Autocomplete = memo(
return ( return (
<Downshift <Downshift
id={name} id={name}
itemToString={it => (itemToString ? itemToString(it) : it?.display)} itemToString={it => {
if (itemToString) return itemToString(it)
if (it) return it[display]
return undefined
}}
onChange={it => setFieldValue(name, it)} onChange={it => setFieldValue(name, it)}
defaultHighlightedIndex={0} defaultHighlightedIndex={0}
selectedItem={value}> selectedItem={value}>
@ -40,9 +53,8 @@ const Autocomplete = memo(
}) => ( }) => (
<div className={classes.container}> <div className={classes.container}>
{renderInput({ {renderInput({
id: name, name,
fullWidth: true, fullWidth: true,
classes,
error: error:
(touched[`${name}-input`] || touched[name]) && errors[name], (touched[`${name}-input`] || touched[name]) && errors[name],
success: success:
@ -52,7 +64,10 @@ const Autocomplete = memo(
value: inputValue2 || '', value: inputValue2 || '',
placeholder, placeholder,
onBlur, onBlur,
onClick: () => toggleMenu(), onClick: event => {
setPopperNode(event.currentTarget.parentElement)
toggleMenu()
},
onChange: it => { onChange: it => {
if (it.target.value === '') { if (it.target.value === '') {
clearSelection() clearSelection()
@ -60,12 +75,12 @@ const Autocomplete = memo(
inputValue = it.target.value inputValue = it.target.value
} }
}), }),
ref: node => {
setPopperNode(node)
},
label label
})} })}
<Popper open={isOpen} anchorEl={popperNode}> <Popper
open={isOpen}
anchorEl={popperNode}
modifiers={{ flip: { enabled: true } }}>
<div <div
{...(isOpen {...(isOpen
? getMenuProps({}, { suppressRefError: true }) ? getMenuProps({}, { suppressRefError: true })
@ -73,20 +88,23 @@ const Autocomplete = memo(
<Paper <Paper
square square
style={{ style={{
marginTop: 8, minWidth: popperNode ? popperNode.clientWidth + 2 : null
minWidth: popperNode ? popperNode.clientWidth : null
}}> }}>
{filterSuggestions( {filterSuggestions(
suggestions, suggestions,
inputValue2, inputValue2,
value ? R.of(value) : [] value ? R.of(value) : [],
code,
display
).map((suggestion, index) => ).map((suggestion, index) =>
renderSuggestion({ renderSuggestion({
suggestion, suggestion,
index, index,
itemProps: getItemProps({ item: suggestion }), itemProps: getItemProps({ item: suggestion }),
highlightedIndex, highlightedIndex,
selectedItem: selectedItem2 selectedItem: selectedItem2,
code,
display
}) })
)} )}
</Paper> </Paper>

View file

@ -1,32 +1,31 @@
import MenuItem from '@material-ui/core/MenuItem' import MenuItem from '@material-ui/core/MenuItem'
import TextField from '@material-ui/core/TextField'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import * as R from 'ramda'
import React from 'react' import React from 'react'
import slugify from 'slugify' import slugify from 'slugify'
import { withStyles } from '@material-ui/core/styles'
import { import {
fontColor, fontColor,
inputFontSize, inputFontSize,
inputFontWeight inputFontWeight,
zircon
} from 'src/styling/variables' } from 'src/styling/variables'
import S from 'src/utils/sanctuary' import S from 'src/utils/sanctuary'
function renderInput(inputProps) { import { TextInput } from '../base'
const { onBlur, success, InputProps, classes, ref, ...other } = inputProps
function renderInput({ InputProps, error, name, success, ...props }) {
const { onChange, onBlur, value } = InputProps
return ( return (
<TextField <TextInput
InputProps={{ name={name}
inputRef: ref, onChange={onChange}
classes: { onBlur={onBlur}
root: classes.inputRoot, value={value}
input: classes.inputInput, error={!!error}
underline: success ? classes.success : '' InputProps={InputProps}
}, {...props}
...InputProps
}}
{...other}
/> />
) )
} }
@ -36,29 +35,44 @@ function renderSuggestion({
index, index,
itemProps, itemProps,
highlightedIndex, highlightedIndex,
selectedItem selectedItem,
code,
display
}) { }) {
const isHighlighted = highlightedIndex === index const isHighlighted = highlightedIndex === index
const item = R.o(R.defaultTo(''), R.path(['display']))(selectedItem) const StyledMenuItem = withStyles(theme => ({
const isSelected = R.indexOf(suggestion.display)(item) > -1 root: {
fontSize: 14,
fontWeight: 400,
color: fontColor
},
selected: {
'&.Mui-selected, &.Mui-selected:hover': {
fontWeight: 500,
backgroundColor: zircon
}
}
}))(MenuItem)
return ( return (
<MenuItem <StyledMenuItem
{...itemProps} {...itemProps}
key={suggestion.code} key={suggestion[code]}
selected={isHighlighted} selected={isHighlighted}
component="div" component="div">
style={{ {suggestion[display]}
fontSize: 14, </StyledMenuItem>
fontWeight: isSelected ? 500 : 400
}}>
{suggestion.display}
</MenuItem>
) )
} }
function filterSuggestions(suggestions = [], value = '', currentValues = []) { function filterSuggestions(
suggestions = [],
value = '',
currentValues = [],
code,
display
) {
const options = { const options = {
shouldSort: true, shouldSort: true,
threshold: 0.2, threshold: 0.2,
@ -66,14 +80,15 @@ function filterSuggestions(suggestions = [], value = '', currentValues = []) {
distance: 100, distance: 100,
maxPatternLength: 32, maxPatternLength: 32,
minMatchCharLength: 1, minMatchCharLength: 1,
keys: ['code', 'display'] code,
display
} }
const fuse = new Fuse(suggestions, options) const fuse = new Fuse(suggestions, options)
const result = value ? fuse.search(slugify(value, ' ')) : suggestions const result = value ? fuse.search(slugify(value, ' ')) : suggestions
const currentCodes = S.map(S.prop('code'))(currentValues) const currentCodes = S.map(S.prop(code))(currentValues)
const filtered = S.filter(it => !S.elem(it.code)(currentCodes))(result) const filtered = S.filter(it => !S.elem(it[code])(currentCodes))(result)
const amountToTake = S.min(filtered.length)(5) const amountToTake = S.min(filtered.length)(5)

View file

@ -26,7 +26,11 @@ const useStyles = makeStyles(theme => ({
} }
}, },
'&$checked': { '&$checked': {
transform: 'translateX(58%)',
color: theme.palette.common.white, color: theme.palette.common.white,
'&$disabled': {
color: disabledColor2
},
'& + $track': { '& + $track': {
backgroundColor: secondaryColor, backgroundColor: secondaryColor,
opacity: 1, opacity: 1,

View file

@ -1,6 +1,5 @@
import React, { memo } from 'react' import React, { memo } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import InputAdornment from '@material-ui/core/InputAdornment'
import TextField from '@material-ui/core/TextField' import TextField from '@material-ui/core/TextField'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
@ -10,27 +9,49 @@ import {
secondaryColor, secondaryColor,
inputFontSize, inputFontSize,
inputFontSizeLg, inputFontSizeLg,
inputFontWeight inputFontWeight,
inputFontWeightLg
} from 'src/styling/variables' } from 'src/styling/variables'
import { TL2, Label2, Info1, Info2 } from 'src/components/typography'
const useStyles = makeStyles({ 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: { inputRoot: {
fontSize: inputFontSize, fontSize: inputFontSize,
color: fontColor, color: fontColor,
fontWeight: inputFontWeight, fontWeight: inputFontWeight,
paddingLeft: 4 paddingLeft: 4,
'& > .MuiInputBase-input': {
width: 282
}
}, },
inputRootLg: { inputRootLg: {
fontSize: inputFontSizeLg, fontSize: inputFontSizeLg,
color: fontColor, color: fontColor,
fontWeight: inputFontWeight fontWeight: inputFontWeightLg,
'& > .MuiInputBase-input': {
width: 96
}
}, },
labelRoot: { labelRoot: {
color: fontColor, color: fontColor,
paddingLeft: 4 paddingLeft: 4
}, },
root: { root: {
'& .MuiInput-underline:before': { '& > .MuiInput-underline:before': {
borderBottom: [[2, 'solid', fontColor]] borderBottom: [[2, 'solid', fontColor]]
}, },
'& .Mui-focused': { '& .Mui-focused': {
@ -40,9 +61,6 @@ const useStyles = makeStyles({
paddingTop: 4, paddingTop: 4,
paddingBottom: 3 paddingBottom: 3
}, },
'& .MuiInputBase-input': {
width: 282
},
'& .MuiInputBase-inputMultiline': { '& .MuiInputBase-inputMultiline': {
width: 500, width: 500,
paddingRight: 20 paddingRight: 20
@ -66,6 +84,23 @@ const useStyles = makeStyles({
} }
}) })
const TextInputDisplay = memo(({ display, suffix, large }) => {
const classes = useStyles()
return (
<div className={classes.wrapper}>
<span>
{large && !suffix && <span>{display}</span>}
{!large && !suffix && <span>{display}</span>}
{large && suffix && <Info1>{display}</Info1>}
{!large && suffix && <Info2>{display}</Info2>}
{suffix && large && <TL2>{suffix}</TL2>}
{suffix && !large && <Label2>{suffix}</Label2>}
</span>
</div>
)
})
const TextInput = memo( const TextInput = memo(
({ ({
name, name,
@ -76,6 +111,7 @@ const TextInput = memo(
suffix, suffix,
large, large,
className, className,
InputProps,
...props ...props
}) => { }) => {
const classes = useStyles() const classes = useStyles()
@ -87,30 +123,33 @@ const TextInput = memo(
} }
return ( return (
<TextField <div className={classes.wrapper}>
id={name} <span>
onChange={onChange} <TextField
onBlur={onBlur} id={name}
error={error} onChange={onChange}
value={value} onBlur={onBlur}
classes={{ root: classes.root }} error={error}
className={classnames(classNames)} value={value}
InputProps={{ classes={{ root: classes.root }}
className: large ? classes.inputRootLg : classes.inputRoot, className={classnames(classNames)}
endAdornment: suffix ? ( InputProps={{
<InputAdornment className: large ? classes.inputRootLg : classes.inputRoot,
className={classes.inputRoot} ...InputProps
disableTypography }}
position="end"> InputLabelProps={{ className: classes.labelRoot }}
{suffix} {...props}
</InputAdornment> />
) : null {suffix && large && (
}} <>
InputLabelProps={{ className: classes.labelRoot }} <TL2>{suffix}</TL2>
{...props} </>
/> )}
{suffix && !large && <Label2>{suffix}</Label2>}
</span>
</div>
) )
} }
) )
export default TextInput export { TextInput, TextInputDisplay }

View file

@ -1,5 +1,5 @@
import Checkbox from './Checkbox' import Checkbox from './Checkbox'
import TextInput from './TextInput' import { TextInput } from './TextInput'
import Switch from './Switch' import Switch from './Switch'
import RadioGroup from './RadioGroup' import RadioGroup from './RadioGroup'

View file

@ -5,7 +5,7 @@ import Radio from './base/Radio'
import RadioGroup from './base/RadioGroup' import RadioGroup from './base/RadioGroup'
import Select from './base/Select' import Select from './base/Select'
import Switch from './base/Switch' import Switch from './base/Switch'
import TextInput from './base/TextInput' import { TextInput } from './base/TextInput'
export { export {
Autocomplete, Autocomplete,

View file

@ -36,7 +36,7 @@ function H3({ children, className, ...props }) {
function H4({ children, className, ...props }) { function H4({ children, className, ...props }) {
const classes = useStyles() const classes = useStyles()
return ( return (
<h4 className={classnames(classes.h3, className)} {...props}> <h4 className={classnames(classes.h4, className)} {...props}>
{children} {children}
</h4> </h4>
) )

View file

@ -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 (
<>
<div>
<div className={classnames(classes.defaults, classes.cbaDefaults)}>
<BigNumericInput
title="Default (Low Balance)"
field={defaultsFields[LOW_BALANCE_KEY]}
editing={editingLowBalance}
disabled={isDisabled(editingState, LOW_BALANCE_KEY)}
setEditing={handleEdit(LOW_BALANCE_KEY)}
handleSubmit={handleSubmit}
className={classes.lowBalance}
setError={setError}
/>
<BigNumericInput
title="Default (High Balance)"
field={defaultsFields[HIGH_BALANCE_KEY]}
editing={editingHighBalance}
disabled={isDisabled(editingState, HIGH_BALANCE_KEY)}
setEditing={handleEdit(HIGH_BALANCE_KEY)}
handleSubmit={handleSubmit}
setError={setError}
/>
</div>
</div>
<div className={classes.overrides}>
<div className={classes.overridesTitle}>
<Info2>Overrides</Info2>
{!addingOverride && !overrideOpsDisabled && overrides.length > 0 && (
<Link
color="primary"
onClick={() => handleEdit(ADD_OVERRIDE_CBA_KEY)(true)}>
Add override
</Link>
)}
</div>
{!addingOverride && !overrideOpsDisabled && overrides.length === 0 && (
<AddButton onClick={() => handleEdit(ADD_OVERRIDE_CBA_KEY)(true)}>
Add overrides
</AddButton>
)}
{(addingOverride || overrides.length > 0) && (
<EditableTable
className={classes.overridesTable}
addingRow={addingOverride}
disableAction={overrideOpsDisabled || addingOverride}
editing={R.map(
() => false,
R.range(0, setupValues[OVERRIDES_KEY].length)
)}
save={handleSubmitOverrides}
reset={handleResetForm}
action={deleteOverride}
initialValues={initialValues}
validationSchema={validationSchema}
data={setupValues[OVERRIDES_KEY]}
elements={elements}
/>
)}
</div>
</>
)
}
export default CryptoBalanceAlerts

View file

@ -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 (
// <Formik
// initialValues={initialValues}
// validationSchema={Yup.object().shape(validationSchemaShape)}
// onSubmit={values => {
// 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)
// }}>
// <Form>
// <Tr>
// <Td size={sizes.machine}>
// {machine && machine.name}
// {!machine && (
// <FormikField
// id={fields[MACHINE_KEY].name}
// name={fields[MACHINE_KEY].name}
// component={Autocomplete}
// type="text"
// suggestions={getSuggestions()}
// code="deviceId"
// display="name"
// />
// )}
// </Td>
// <CellDoubleLevel className={classes.doubleLevelRow}>
// <Td size={sizes.percentage} textAlign="right">
// <Field
// editing={editing}
// field={fields[PERCENTAGE_KEY]}
// displayValue={x => (x === '' ? '-' : x)}
// decoration="%"
// className={classes.eRowField}
// setError={setError}
// />
// </Td>
// <Td size={sizes.amount} textAlign="right">
// <Field
// editing={editing}
// field={fields[NUMERARY_KEY]}
// displayValue={x => (x === '' ? '-' : x)}
// decoration="EUR"
// className={classes.eRowField}
// setError={setError}
// />
// </Td>
// </CellDoubleLevel>
// <CellDoubleLevel className={classes.doubleLevelRow}>
// <Td size={sizes.cashOut1} textAlign="right">
// <Field
// editing={editing}
// field={fields[CASSETTE_1_KEY]}
// displayValue={x => (x === '' ? '-' : x)}
// decoration="%"
// className={classes.eRowField}
// setError={setError}
// />
// </Td>
// <Td size={sizes.cashOut2} textAlign="right">
// <Field
// editing={editing}
// field={fields[CASSETTE_2_KEY]}
// displayValue={x => (x === '' ? '-' : x)}
// decoration="%"
// className={classes.eRowField}
// setError={setError}
// />
// </Td>
// </CellDoubleLevel>
// <Td
// size={sizes.edit}
// textAlign="center"
// className={editing && classes.edit}>
// {!editing && !disabled && (
// <button
// className={classes.button}
// onClick={() => handleEdit(machine.name)(true)}>
// <EditIcon />
// </button>
// )}
// {disabled && (
// <div>
// <DisabledEditIcon />
// </div>
// )}
// {editing && (
// <>
// <Link color="primary" type="submit">
// Save
// </Link>
// <Link color="secondary" type="reset">
// Cancel
// </Link>
// </>
// )}
// </Td>
// </Tr>
// </Form>
// </Formik>
// )
// }
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 (
<div>
<div className={classnames(classes.defaults, classes.fbaDefaults)}>
<BigPercentageAndNumericInput
title="Cash-in (Full)"
fields={cashInFields}
editing={cashInEditing}
disabled={isDisabled(editingState, CASH_IN_FULL_KEY)}
setEditing={handleEdit(CASH_IN_FULL_KEY)}
handleSubmit={handleSubmit(CASH_IN_FULL_KEY)}
className={classes.cashInWrapper}
setError={setError}
/>
<div>
<MultiplePercentageInput
title="Cash-out (Empty)"
fields={cashOutFields}
editing={cashOutEditing}
disabled={isDisabled(editingState, CASH_OUT_EMPTY_KEY)}
setEditing={handleEdit(CASH_OUT_EMPTY_KEY)}
handleSubmit={handleSubmit(CASH_OUT_EMPTY_KEY)}
setError={setError}
/>
</div>
</div>
<div className={classes.overrides}>
<div className={classes.overridesTitle}>
<Info2>Overrides</Info2>
{!addingOverride && !overrideOpsDisabled && overrides.length > 0 && (
<Link
color="primary"
onClick={() => handleEdit(ADD_OVERRIDE_FBA_KEY)(true)}>
Add override
</Link>
)}
</div>
{!addingOverride && !overrideOpsDisabled && overrides.length === 0 && (
<AddButton onClick={() => handleEdit(ADD_OVERRIDE_FBA_KEY)(true)}>
Add overrides
</AddButton>
)}
{(addingOverride || overrides.length > 0) && (
<EditableTable
className={{ head: { cell: classes.doubleLevelHead } }}
addingRow={addingOverride}
disableAction={overrideOpsDisabled || addingOverride}
editing={R.map(
() => 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
/>
// <Table>
// <TDoubleLevelHead>
// <Th size={sizes.machine}>Machine</Th>
// <ThDoubleLevel
// title="Cash-in (Cassette Full)"
// className={classes.doubleLevelHead}>
// <Th size={sizes.percentage} textAlign="right">
// Percentage
// </Th>
// <Th size={sizes.amount} textAlign="right">
// Amount
// </Th>
// </ThDoubleLevel>
// <ThDoubleLevel
// title="Cash-out (Cassette Empty)"
// className={classes.doubleLevelHead}>
// <Th size={sizes.cashOut1} textAlign="right">
// Cash-out 1
// </Th>
// <Th size={sizes.cashOut2} textAlign="right">
// Cash-out 2
// </Th>
// </ThDoubleLevel>
// <Th size={sizes.edit} textAlign="center">
// Edit
// </Th>
// </TDoubleLevelHead>
// <TBody>
// {addingOverride && (
// <OverridesRow
// handleSubmitOverrides={handleSubmitOverrides}
// handleEdit={handleEdit}
// sizes={sizes}
// editing={editingState[ADD_OVERRIDE_FBA_KEY]}
// fields={{
// [MACHINE_KEY]: { name: `new-${MACHINE_KEY}` },
// [PERCENTAGE_KEY]: {
// name: `new-${CASH_IN_FULL_KEY}-${PERCENTAGE_KEY}`
// },
// [NUMERARY_KEY]: {
// name: `new-${CASH_IN_FULL_KEY}-${NUMERARY_KEY}`
// },
// [CASSETTE_1_KEY]: {
// name: `new-${CASH_OUT_EMPTY_KEY}-${CASSETTE_1_KEY}`
// },
// [CASSETTE_2_KEY]: {
// name: `new-${CASH_OUT_EMPTY_KEY}-${CASSETTE_2_KEY}`
// }
// }}
// disabled={isDisabled(ADD_OVERRIDE_FBA_KEY)}
// getSuggestions={getSuggestions}
// setError={setError}
// />
// )}
// {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 (
// <OverridesRow
// key={idx}
// machine={machine}
// handleSubmitOverrides={handleSubmitOverrides}
// handleEdit={handleEdit}
// sizes={sizes}
// editing={editing}
// fields={fields}
// disabled={disabled}
// setError={setError}
// />
// )
// })}
// </TBody>
// </Table>
)}
</div>
</div>
)
}
export default FiatBalanceAlerts

View file

@ -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 (
<div className={classnames(classNames)}>
{field.label && <Label1 className={classes.label}>{field.label}</Label1>}
<div className={classes.displayValue}>
{!editing && props.large && (
<>
<Info1>{displayValue(field.value)}</Info1>
</>
)}
{!editing && !props.large && (
<>
<Info2>{displayValue(field.value)}</Info2>
</>
)}
{editing && (
<FormikField
id={field.name}
name={field.name}
component={TextInputFormik}
placeholder={field.placeholder}
type="text"
onFocus={() => setError(null)}
{...props}
/>
)}
{props.large && (
<>
<TL2>{decoration}</TL2>
</>
)}
{!props.large && (
<>
<Label2>{decoration}</Label2>
</>
)}
</div>
</div>
)
}
const useStyles = makeStyles(inputSectionStyles)
const Header = ({ title, editing, disabled, setEditing }) => {
const classes = useStyles()
return (
<div className={classes.header}>
<H4>{title}</H4>
{!editing && !disabled && (
<button onClick={() => setEditing(true)} className={classes.editButton}>
<EditIcon />
</button>
)}
{disabled && (
<div className={classes.disabledButton}>
<DisabledEditIcon />
</div>
)}
{editing && (
<div className={classes.editingButtons}>
<Link color="primary" type="submit">
Save
</Link>
<Link color="secondary" type="reset">
Cancel
</Link>
</div>
)}
</div>
)
}
const BigNumericInput = ({
title,
field,
editing,
disabled,
setEditing,
handleSubmit,
setError,
className
}) => {
const classes = useStyles()
const { name, value } = field
return (
<div className={className}>
<Formik
initialValues={{ [name]: value }}
validationSchema={Yup.object().shape({
[name]: Yup.number()
.integer()
.min(0)
.max(99999)
.required()
})}
onSubmit={values => {
handleSubmit(values)
}}
onReset={(values, bag) => {
setEditing(false)
setError(null)
}}>
<Form>
<Header
title={title}
editing={editing}
disabled={disabled}
setEditing={() => setEditing(true)}
/>
<div className={classes.body}>
<Field
editing={editing}
field={field}
displayValue={x => (x === '' ? '-' : x)}
decoration="EUR"
large
setError={setError}
/>
</div>
</Form>
</Formik>
</div>
)
}
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 (
<div className={className}>
<Formik
initialValues={{
[percentageName]: percentageValue,
[numericName]: numericValue
}}
validationSchema={Yup.object().shape({
[percentageName]: Yup.number()
.integer()
.min(0)
.max(100)
.required(),
[numericName]: Yup.number()
.integer()
.min(0)
.max(99999)
.required()
})}
onSubmit={values => {
handleSubmit(values)
}}
onReset={(values, bag) => {
setEditing(false)
setError(null)
}}>
<Form>
<Header
title={title}
editing={editing}
disabled={disabled}
setEditing={() => setEditing(true)}
/>
<div className={classes.body}>
<div className={classes.percentageDisplay}>
<div style={{ height: `${percentageValue}%` }}></div>
</div>
<div className={classes.inputColumn}>
<Field
editing={editing}
field={percentage}
displayValue={x => (x === '' ? '-' : x)}
decoration="%"
large
setError={setError}
/>
<Field
editing={editing}
field={numeric}
displayValue={x => (x === '' ? '-' : x)}
decoration="EUR"
large
setError={setError}
/>
</div>
</div>
</Form>
</Formik>
</div>
)
}
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 (
<div className={className}>
<Formik
initialValues={initialValues}
validationSchema={Yup.object().shape(validationSchemaShape)}
onSubmit={values => {
handleSubmit(values)
}}
onReset={(values, bag) => {
setEditing(false)
setError(null)
}}>
<Form>
<Header
title={title}
editing={editing}
disabled={disabled}
setEditing={() => setEditing(true)}
/>
<div className={classes.body}>
{fields.map((field, idx) => (
<div key={idx}>
<div className={classes.percentageDisplay}>
<div style={{ height: `${field.value}%` }}></div>
</div>
<div className={classes.inputColumn}>
<TL2 className={classes.title}>{field.title}</TL2>
<Field
editing={editing}
field={field}
displayValue={x => (x === '' ? '-' : x)}
decoration="%"
className={classes.percentageInput}
large
setError={setError}
/>
</div>
</div>
))}
</div>
</Form>
</Formik>
</div>
)
}
export {
Field,
BigNumericInput,
BigPercentageAndNumericInput,
MultiplePercentageInput
}

View file

@ -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 (
<div className={classes.sectionHeader}>
<TL1 className={classes.sectionTitle}>{children}</TL1>
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
</div>
)
}
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 (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Notifications</Title>
</div>
</div>
<div className={classes.section}>
<SectionHeader error={error?.section === SETUP_KEY}>
Setup
</SectionHeader>
<Setup values={state.setup} save={curriedSave(SETUP_KEY)} />
</div>
<div className={classes.section}>
<SectionHeader error={error?.section === TRANSACTION_ALERTS_KEY}>
Transaction alerts
</SectionHeader>
<TransactionAlerts
value={state[TRANSACTION_ALERTS_KEY]}
editingState={editingState}
handleEditingClick={handleEditingClick}
save={curriedSave(TRANSACTION_ALERTS_KEY)}
setError={setError}
/>
</div>
<div className={classes.section}>
<SectionHeader error={error?.section === FIAT_BALANCE_ALERTS_KEY}>
Fiat balance alerts
</SectionHeader>
<FiatBalanceAlerts
values={state[FIAT_BALANCE_ALERTS_KEY]}
editingState={editingState}
handleEditingClick={handleEditingClick}
save={curriedSave(FIAT_BALANCE_ALERTS_KEY)}
setError={setError}
/>
</div>
<div className={classes.section}>
<SectionHeader error={error?.section === CRYPTO_BALANCE_ALERTS_KEY}>
Crypto balance alerts
</SectionHeader>
<CryptoBalanceAlerts
values={state[CRYPTO_BALANCE_ALERTS_KEY]}
editingState={editingState}
handleEditingClick={handleEditingClick}
save={curriedSave(CRYPTO_BALANCE_ALERTS_KEY)}
setError={setError}
/>
</div>
</>
)
}
export default Notifications

View file

@ -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
}

View file

@ -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 (
<Td size={findSize(name)} textAlign={findAlign(name)}>
<Switch
disabled={disabled}
checked={values[name]}
onChange={handleChange(name)}
value={name}
/>
</Td>
)
}
return (
<Tr>
<Td size={findSize(CHANNEL_KEY)} textAlign={findAlign(CHANNEL_KEY)}>
{channel}
</Td>
<Cell name={BALANCE_KEY} disabled={!active} />
<Cell name={TRANSACTIONS_KEY} disabled={!active} />
<Cell name={COMPLIANCE_KEY} disabled={!active} />
<Cell name={SECURITY_KEY} disabled={!active} />
<Cell name={ERRORS_KEY} disabled={!active} />
<Cell name={ACTIVE_KEY} />
</Tr>
)
}
const Setup = ({ values: setupValues, save }) => {
const saveSetup = R.curry((key, values) =>
save(R.mergeDeepRight(setupValues, { [key]: values }))
)
return (
<div>
<FakeTable>
<THead>
{elements.map(({ size, className, textAlign, header }, idx) => (
<Th
key={idx}
size={size}
className={className}
textAlign={textAlign}>
{header}
</Th>
))}
</THead>
<TBody>
<Row
channel="Email"
columns={elements}
values={setupValues[EMAIL_KEY]}
save={saveSetup(EMAIL_KEY)}
/>
<Row
channel="SMS"
columns={elements}
values={setupValues[SMS_KEY]}
save={saveSetup(SMS_KEY)}
/>
</TBody>
</FakeTable>
</div>
)
}
export default Setup

View file

@ -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 (
<div>
<BigNumericInput
title="High value transaction"
field={field}
editing={editing}
disabled={isDisabled(editingState, HIGH_VALUE_TRANSACTION_KEY)}
setEditing={handleEdit(HIGH_VALUE_TRANSACTION_KEY)}
handleSubmit={handleSubmit}
setError={setError}
/>
</div>
)
}
export default TransactionAlerts

View file

@ -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
}

View file

@ -53,6 +53,7 @@ const OperatorInfo = () => {
{isSelected(CONTACT_INFORMATION) && <ContactInfo />} {isSelected(CONTACT_INFORMATION) && <ContactInfo />}
{isSelected(TERMS_CONDITIONS) && <TermsConditions />} {isSelected(TERMS_CONDITIONS) && <TermsConditions />}
{isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />} {isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />}
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
</div> </div>
</div> </div>
</> </>

View file

@ -0,0 +1,11 @@
export default {
titleWrapper: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row'
},
titleAndButtonsContainer: {
display: 'flex'
}
}

View file

@ -1,30 +1,19 @@
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom' import { Route, Redirect, Switch } from 'react-router-dom'
import AuthRegister from 'src/pages/AuthRegister' import AuthRegister from 'src/pages/AuthRegister'
import Commissions from 'src/pages/Commissions' import Commissions from 'src/pages/Commissions'
import Customers from 'src/pages/Customers'
import Funding from 'src/pages/Funding' import Funding from 'src/pages/Funding'
import Locales from 'src/pages/Locales' import Locales from 'src/pages/Locales'
import MachineLogs from 'src/pages/MachineLogs' import MachineLogs from 'src/pages/MachineLogs'
import Notifications from 'src/pages/Notifications/Notifications'
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo' import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
import ServerLogs from 'src/pages/ServerLogs' import ServerLogs from 'src/pages/ServerLogs'
import Services from 'src/pages/Services/Services' import Services from 'src/pages/Services/Services'
import Transactions from 'src/pages/Transactions/Transactions' import Transactions from 'src/pages/Transactions/Transactions'
import MachineStatus from 'src/pages/maintenance/MachineStatus'
import Customers from '../pages/Customers'
import MachineStatus from '../pages/maintenance/MachineStatus'
const tree = [ const tree = [
{ key: 'transactions', label: 'Transactions', route: '/transactions' }, { key: 'transactions', label: 'Transactions', route: '/transactions' },
@ -65,6 +54,11 @@ const tree = [
label: '3rd party services', label: '3rd party services',
route: '/settings/3rd-party-services' route: '/settings/3rd-party-services'
}, },
{
key: 'notifications',
label: 'Notifications',
route: '/settings/notifications'
},
{ key: 'info', label: 'Operator Info', route: '/settings/operator-info' } { key: 'info', label: 'Operator Info', route: '/settings/operator-info' }
] ]
}, },
@ -120,6 +114,7 @@ const Routes = () => (
<Route path="/settings/commissions" component={Commissions} /> <Route path="/settings/commissions" component={Commissions} />
<Route path="/settings/locale" component={Locales} /> <Route path="/settings/locale" component={Locales} />
<Route path="/settings/3rd-party-services" component={Services} /> <Route path="/settings/3rd-party-services" component={Services} />
<Route path="/settings/notifications" component={Notifications} />
<Route path="/settings/operator-info" component={OperatorInfo} /> <Route path="/settings/operator-info" component={OperatorInfo} />
<Route path="/maintenance/logs" component={MachineLogs} /> <Route path="/maintenance/logs" component={MachineLogs} />
<Route path="/maintenance/funding" component={Funding} /> <Route path="/maintenance/funding" component={Funding} />

View file

@ -81,6 +81,7 @@ const smallestFontSize = fontSize5
const inputFontSize = fontSize3 const inputFontSize = fontSize3
const inputFontSizeLg = fontSize1 const inputFontSizeLg = fontSize1
const inputFontWeight = 500 const inputFontWeight = 500
const inputFontWeightLg = 700
const inputFontFamily = fontSecondary const inputFontFamily = fontSecondary
// Breakpoints // Breakpoints
@ -99,6 +100,8 @@ if (version === 8) {
tableCellHeight = spacer * 7 - 2 tableCellHeight = spacer * 7 - 2
} }
const tableDoubleHeaderHeight = tableHeaderHeight * 2
const tableSmCellHeight = 30 const tableSmCellHeight = 30
const tableLgCellHeight = 76 const tableLgCellHeight = 76
@ -159,6 +162,7 @@ export {
inputFontSizeLg, inputFontSizeLg,
inputFontFamily, inputFontFamily,
inputFontWeight, inputFontWeight,
inputFontWeightLg,
// screen sizes // screen sizes
sm, sm,
md, md,
@ -170,6 +174,7 @@ export {
mainWidth, mainWidth,
// table sizes // table sizes
tableHeaderHeight, tableHeaderHeight,
tableDoubleHeaderHeight,
tableCellHeight, tableCellHeight,
tableSmCellHeight, tableSmCellHeight,
tableLgCellHeight, tableLgCellHeight,