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) {
|
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()
|
||||||
|
|
|
||||||
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 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
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(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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
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 * 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} />
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue