feat: notifications page
This commit is contained in:
parent
2b71c08444
commit
b6e7d98b72
25 changed files with 2615 additions and 198 deletions
|
|
@ -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()
|
||||
|
|
|
|||
53
new-lamassu-admin/src/components/buttons/AddButton.js
Normal file
53
new-lamassu-admin/src/components/buttons/AddButton.js
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}) => (
|
||||
<FastField name={name} component={component} {...props} />
|
||||
)
|
||||
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 ERow = memo(({ elements }) => {
|
||||
const { values, submitForm, resetForm, errors } = useFormikContext()
|
||||
const [editing, setEditing] = useState(false)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const innerSave = () => {
|
||||
submitForm()
|
||||
const ERow = memo(
|
||||
({ elements, editing, setEditing, disableAction, action }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
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 innerCancel = () => {
|
||||
setEditing(false)
|
||||
resetForm()
|
||||
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()}>
|
||||
{elements.map(
|
||||
{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
|
||||
) => (
|
||||
<Td key={idx} size={size} textAlign={textAlign}>
|
||||
{editing && getField(name, input, inputProps)}
|
||||
{!editing && view(values[name])}
|
||||
</Td>
|
||||
<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,
|
||||
input,
|
||||
size,
|
||||
textAlign,
|
||||
type,
|
||||
view = it => it?.toString(),
|
||||
inputProps
|
||||
} = element
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={idx}
|
||||
name={name}
|
||||
input={input}
|
||||
type={type}
|
||||
display={view(values[name])}
|
||||
className={classnames(colClasses)}
|
||||
size={size}
|
||||
textAlign={textAlign}
|
||||
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>
|
||||
)}
|
||||
<Td size={175}>
|
||||
{editing ? (
|
||||
{!editing && disableAction && (
|
||||
<div>{icon(actionCol.name, disableAction)}</div>
|
||||
)}
|
||||
{editing && (
|
||||
<>
|
||||
<Link
|
||||
style={{ marginRight: '20px' }}
|
||||
color="secondary"
|
||||
onClick={innerCancel}>
|
||||
<Link color="secondary" type="reset">
|
||||
Cancel
|
||||
</Link>
|
||||
<Link color="primary" onClick={innerSave}>
|
||||
<Link color="primary" type="submit">
|
||||
Save
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link color="primary" onClick={() => setEditing(true)}>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ERowWithFormik = memo(({ value, validationSchema, save, elements }) => {
|
||||
const ERowWithFormik = memo(
|
||||
({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
save,
|
||||
reset,
|
||||
action,
|
||||
elements,
|
||||
editing,
|
||||
disableAction
|
||||
}) => {
|
||||
return (
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={value}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={save}>
|
||||
onSubmit={save}
|
||||
onReset={reset}>
|
||||
<Form>
|
||||
<ERow elements={elements} />
|
||||
<ERow
|
||||
elements={elements}
|
||||
editing={editing}
|
||||
disableAction={disableAction}
|
||||
action={action}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default ERowWithFormik
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Table>
|
||||
<THead>
|
||||
{elements.map(({ name, size, header, textAlign }, idx) => (
|
||||
<Td header key={idx} size={size} textAlign={textAlign}>
|
||||
{header || startCase(name)}
|
||||
</Td>
|
||||
<THead className={className?.root}>
|
||||
{R.init(elements).map(({ name, size, display, textAlign }, idx) => (
|
||||
<Th
|
||||
id={name}
|
||||
key={idx}
|
||||
size={size}
|
||||
textAlign={textAlign}
|
||||
className={className?.cell}>
|
||||
{display}
|
||||
</Th>
|
||||
))}
|
||||
<Td header size={175} />
|
||||
<Th size={action.size} action>
|
||||
{startCase(action.name)}
|
||||
</Th>
|
||||
</THead>
|
||||
)
|
||||
})
|
||||
|
||||
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}
|
||||
value={it}
|
||||
initialValues={it}
|
||||
elements={elements}
|
||||
save={save}
|
||||
reset={it => reset(it)}
|
||||
action={action}
|
||||
validationSchema={validationSchema}
|
||||
disableAction={disableAction}
|
||||
editing={editing[idx]}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default ETable
|
||||
|
|
|
|||
|
|
@ -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 <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 classes = useStyles()
|
||||
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 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 (
|
||||
<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 classes = useStyles()
|
||||
const cardClasses = { root: classes.cardContentRoot }
|
||||
|
|
@ -135,4 +205,15 @@ const EditCell = ({ save, cancel }) => (
|
|||
</Td>
|
||||
)
|
||||
|
||||
export { Table, THead, TBody, Tr, Td, Th, EditCell }
|
||||
export {
|
||||
Table,
|
||||
THead,
|
||||
TDoubleLevelHead,
|
||||
TBody,
|
||||
Tr,
|
||||
Td,
|
||||
Th,
|
||||
ThDoubleLevel,
|
||||
CellDoubleLevel,
|
||||
EditCell
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Downshift
|
||||
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)}
|
||||
defaultHighlightedIndex={0}
|
||||
selectedItem={value}>
|
||||
|
|
@ -40,9 +53,8 @@ const Autocomplete = memo(
|
|||
}) => (
|
||||
<div className={classes.container}>
|
||||
{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
|
||||
})}
|
||||
<Popper open={isOpen} anchorEl={popperNode}>
|
||||
<Popper
|
||||
open={isOpen}
|
||||
anchorEl={popperNode}
|
||||
modifiers={{ flip: { enabled: true } }}>
|
||||
<div
|
||||
{...(isOpen
|
||||
? getMenuProps({}, { suppressRefError: true })
|
||||
|
|
@ -73,20 +88,23 @@ const Autocomplete = memo(
|
|||
<Paper
|
||||
square
|
||||
style={{
|
||||
marginTop: 8,
|
||||
minWidth: popperNode ? popperNode.clientWidth : null
|
||||
minWidth: popperNode ? popperNode.clientWidth + 2 : null
|
||||
}}>
|
||||
{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
|
||||
})
|
||||
)}
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<TextField
|
||||
InputProps={{
|
||||
inputRef: ref,
|
||||
classes: {
|
||||
root: classes.inputRoot,
|
||||
input: classes.inputInput,
|
||||
underline: success ? classes.success : ''
|
||||
},
|
||||
...InputProps
|
||||
}}
|
||||
{...other}
|
||||
<TextInput
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
error={!!error}
|
||||
InputProps={InputProps}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<MenuItem
|
||||
<StyledMenuItem
|
||||
{...itemProps}
|
||||
key={suggestion.code}
|
||||
key={suggestion[code]}
|
||||
selected={isHighlighted}
|
||||
component="div"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? 500 : 400
|
||||
}}>
|
||||
{suggestion.display}
|
||||
</MenuItem>
|
||||
component="div">
|
||||
{suggestion[display]}
|
||||
</StyledMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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(
|
||||
({
|
||||
name,
|
||||
|
|
@ -76,6 +111,7 @@ const TextInput = memo(
|
|||
suffix,
|
||||
large,
|
||||
className,
|
||||
InputProps,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
|
@ -87,6 +123,8 @@ const TextInput = memo(
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<span>
|
||||
<TextField
|
||||
id={name}
|
||||
onChange={onChange}
|
||||
|
|
@ -97,20 +135,21 @@ const TextInput = memo(
|
|||
className={classnames(classNames)}
|
||||
InputProps={{
|
||||
className: large ? classes.inputRootLg : classes.inputRoot,
|
||||
endAdornment: suffix ? (
|
||||
<InputAdornment
|
||||
className={classes.inputRoot}
|
||||
disableTypography
|
||||
position="end">
|
||||
{suffix}
|
||||
</InputAdornment>
|
||||
) : null
|
||||
...InputProps
|
||||
}}
|
||||
InputLabelProps={{ className: classes.labelRoot }}
|
||||
{...props}
|
||||
/>
|
||||
{suffix && large && (
|
||||
<>
|
||||
<TL2>{suffix}</TL2>
|
||||
</>
|
||||
)}
|
||||
{suffix && !large && <Label2>{suffix}</Label2>}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default TextInput
|
||||
export { TextInput, TextInputDisplay }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Checkbox from './Checkbox'
|
||||
import TextInput from './TextInput'
|
||||
import { TextInput } from './TextInput'
|
||||
import Switch from './Switch'
|
||||
import RadioGroup from './RadioGroup'
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function H3({ children, className, ...props }) {
|
|||
function H4({ children, className, ...props }) {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<h4 className={classnames(classes.h3, className)} {...props}>
|
||||
<h4 className={classnames(classes.h4, className)} {...props}>
|
||||
{children}
|
||||
</h4>
|
||||
)
|
||||
|
|
|
|||
254
new-lamassu-admin/src/pages/Notifications/CryptoBalanceAlerts.js
Normal file
254
new-lamassu-admin/src/pages/Notifications/CryptoBalanceAlerts.js
Normal 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
|
||||
597
new-lamassu-admin/src/pages/Notifications/FiatBalanceAlerts.js
Normal file
597
new-lamassu-admin/src/pages/Notifications/FiatBalanceAlerts.js
Normal 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
|
||||
340
new-lamassu-admin/src/pages/Notifications/Inputs.js
Normal file
340
new-lamassu-admin/src/pages/Notifications/Inputs.js
Normal 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
|
||||
}
|
||||
232
new-lamassu-admin/src/pages/Notifications/Notifications.js
Normal file
232
new-lamassu-admin/src/pages/Notifications/Notifications.js
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
148
new-lamassu-admin/src/pages/Notifications/Setup.js
Normal file
148
new-lamassu-admin/src/pages/Notifications/Setup.js
Normal 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
|
||||
|
|
@ -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
|
||||
61
new-lamassu-admin/src/pages/Notifications/aux.js
Normal file
61
new-lamassu-admin/src/pages/Notifications/aux.js
Normal 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
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ const OperatorInfo = () => {
|
|||
{isSelected(CONTACT_INFORMATION) && <ContactInfo />}
|
||||
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
|
||||
{isSelected(COIN_ATM_RADAR) && <CoinAtmRadar />}
|
||||
{isSelected(TERMS_CONDITIONS) && <TermsConditions />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
11
new-lamassu-admin/src/pages/common.styles.js
Normal file
11
new-lamassu-admin/src/pages/common.styles.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default {
|
||||
titleWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
titleAndButtonsContainer: {
|
||||
display: 'flex'
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = () => (
|
|||
<Route path="/settings/commissions" component={Commissions} />
|
||||
<Route path="/settings/locale" component={Locales} />
|
||||
<Route path="/settings/3rd-party-services" component={Services} />
|
||||
<Route path="/settings/notifications" component={Notifications} />
|
||||
<Route path="/settings/operator-info" component={OperatorInfo} />
|
||||
<Route path="/maintenance/logs" component={MachineLogs} />
|
||||
<Route path="/maintenance/funding" component={Funding} />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue