+
+ Log. scale
+ setLogarithmic(event.target.checked)} />
+
>
)
diff --git a/new-lamassu-admin/src/pages/Analytics/components/wrappers/VolumeOverTimeWrapper.js b/new-lamassu-admin/src/pages/Analytics/components/wrappers/VolumeOverTimeWrapper.js
new file mode 100644
index 00000000..6257934f
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Analytics/components/wrappers/VolumeOverTimeWrapper.js
@@ -0,0 +1,91 @@
+import { Box } from '@material-ui/core'
+import { makeStyles } from '@material-ui/core/styles'
+import React, { useState } from 'react'
+
+import { Select, Switch } from 'src/components/inputs'
+import { H2 } from 'src/components/typography'
+import { neon, java } from 'src/styling/variables'
+
+import styles from '../../Analytics.styles'
+import Graph from '../../graphs/Graph'
+import LegendEntry from '../LegendEntry'
+
+const useStyles = makeStyles(styles)
+
+const VolumeOverTimeGraphHeader = ({
+ title,
+ representing,
+ period,
+ data,
+ machines,
+ selectedMachine,
+ handleMachineChange,
+ timezone,
+ currency
+}) => {
+ const classes = useStyles()
+
+ const [logarithmic, setLogarithmic] = useState()
+
+ const legend = {
+ cashIn: (
+
+
+
+ ),
+ cashOut: (
+
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+
{title}
+
+
+
+
+
+
+
+ Log. scale
+ setLogarithmic(event.target.checked)} />
+
+
+
+
+
+ >
+ )
+}
+
+export default VolumeOverTimeGraphHeader
diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js b/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js
index 81870a0c..18ac8fdf 100644
--- a/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js
+++ b/new-lamassu-admin/src/pages/Analytics/graphs/Graph.js
@@ -5,6 +5,7 @@ import GraphTooltip from '../components/tooltips/GraphTooltip'
import HourOfDayBarGraph from './HourOfDayBarGraph'
import OverTimeDotGraph from './OverTimeDotGraph'
+import OverTimeLineGraph from './OverTimeLineGraph'
import TopMachinesBarGraph from './TopMachinesBarGraph'
const GraphWrapper = ({
@@ -15,7 +16,8 @@ const GraphWrapper = ({
currency,
selectedMachine,
machines,
- selectedDay
+ selectedDay,
+ log
}) => {
const [selectionCoords, setSelectionCoords] = useState(null)
const [selectionDateInterval, setSelectionDateInterval] = useState(null)
@@ -33,6 +35,20 @@ const GraphWrapper = ({
setSelectionDateInterval={setSelectionDateInterval}
setSelectionData={setSelectionData}
selectedMachine={selectedMachine}
+ log={log}
+ />
+ )
+ case 'volumeOverTime':
+ return (
+
)
case 'topMachinesVolume':
diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js
index acd7a181..9da30a97 100644
--- a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js
+++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeDotGraph.js
@@ -15,6 +15,7 @@ import {
fontSecondary,
subheaderColor
} from 'src/styling/variables'
+import { numberToFiatAmount } from 'src/utils/number'
import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
const Graph = ({
@@ -23,7 +24,8 @@ const Graph = ({
timezone,
setSelectionCoords,
setSelectionData,
- setSelectionDateInterval
+ setSelectionDateInterval,
+ log = false
}) => {
const ref = useRef(null)
@@ -36,7 +38,7 @@ const Graph = ({
top: 25,
right: 3.5,
bottom: 27,
- left: 36.5
+ left: 38
}),
[]
)
@@ -46,6 +48,7 @@ const Graph = ({
const periodDomains = {
day: [NOW - DAY, NOW],
+ threeDays: [NOW - 3 * DAY, NOW],
week: [NOW - WEEK, NOW],
month: [NOW - MONTH, NOW]
}
@@ -58,6 +61,12 @@ const Graph = ({
tick: d3.utcHour.every(1),
labelFormat: '%H:%M'
},
+ threeDays: {
+ freq: 12,
+ step: 6 * 60 * 60 * 1000,
+ tick: d3.utcDay.every(1),
+ labelFormat: '%a %d'
+ },
week: {
freq: 7,
step: 24 * 60 * 60 * 1000,
@@ -164,7 +173,7 @@ const Graph = ({
.domain(periodDomains[period.code])
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
- const y = d3
+ const yLin = d3
.scaleLinear()
.domain([
0,
@@ -173,6 +182,16 @@ const Graph = ({
.nice()
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
+ const yLog = d3
+ .scaleLog()
+ .domain([
+ (d3.min(data, d => new BigNumber(d.fiat).toNumber()) ?? 1) * 0.9,
+ (d3.max(data, d => new BigNumber(d.fiat).toNumber()) ?? 1000) * 1.1
+ ])
+ .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
+
+ const y = log ? yLog : yLin
+
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
const fullBreakpoints = [
graphLimits[1],
@@ -219,14 +238,11 @@ const Graph = ({
d.getTime() + d.getTimezoneOffset() * MINUTE
)
})
+ .tickSizeOuter(0)
)
- .call(g => g.select('.domain').remove())
.call(g =>
g
- .append('line')
- .attr('x1', GRAPH_MARGIN.left)
- .attr('y1', -GRAPH_HEIGHT + GRAPH_MARGIN.top + GRAPH_MARGIN.bottom)
- .attr('x2', GRAPH_MARGIN.left)
+ .select('.domain')
.attr('stroke', primaryColor)
.attr('stroke-width', 1)
),
@@ -237,18 +253,23 @@ const Graph = ({
g =>
g
.attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
- .call(d3.axisLeft(y).ticks(GRAPH_HEIGHT / 100))
- .call(g => g.select('.domain').remove())
- .call(g =>
- g
- .selectAll('.tick line')
- .filter(d => d === 0)
- .clone()
- .attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.left)
- .attr('stroke-width', 1)
- .attr('stroke', primaryColor)
- ),
- [GRAPH_MARGIN, y]
+ .call(
+ d3
+ .axisLeft(y)
+ .ticks(GRAPH_HEIGHT / 100)
+ .tickSizeOuter(0)
+ .tickFormat(d => {
+ if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
+
+ if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
+
+ return numberToFiatAmount(d)
+ })
+ )
+ .select('.domain')
+ .attr('stroke', primaryColor)
+ .attr('stroke-width', 1),
+ [GRAPH_MARGIN, y, log]
)
const buildGrid = useCallback(
@@ -477,25 +498,23 @@ const Graph = ({
const buildAvg = useCallback(
g => {
+ const median = d3.median(data, d => new BigNumber(d.fiat).toNumber()) ?? 0
+
+ if (log && median === 0) return
+
g.attr('stroke', primaryColor)
.attr('stroke-width', 3)
.attr('stroke-dasharray', '10, 5')
.call(g =>
g
.append('line')
- .attr(
- 'y1',
- 0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
- )
- .attr(
- 'y2',
- 0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
- )
+ .attr('y1', 0.5 + y(median))
+ .attr('y2', 0.5 + y(median))
.attr('x1', GRAPH_MARGIN.left)
.attr('x2', GRAPH_WIDTH)
)
},
- [GRAPH_MARGIN, y, data]
+ [GRAPH_MARGIN, y, data, log]
)
const drawData = useCallback(
@@ -554,5 +573,6 @@ export default memo(
Graph,
(prev, next) =>
R.equals(prev.period, next.period) &&
- R.equals(prev.selectedMachine, next.selectedMachine)
+ R.equals(prev.selectedMachine, next.selectedMachine) &&
+ R.equals(prev.log, next.log)
)
diff --git a/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js
new file mode 100644
index 00000000..6c5613a1
--- /dev/null
+++ b/new-lamassu-admin/src/pages/Analytics/graphs/OverTimeLineGraph.js
@@ -0,0 +1,654 @@
+import BigNumber from 'bignumber.js'
+import * as d3 from 'd3'
+import { getTimezoneOffset } from 'date-fns-tz'
+import {
+ add,
+ addMilliseconds,
+ compareDesc,
+ differenceInMilliseconds,
+ format,
+ startOfWeek,
+ startOfYear
+} from 'date-fns/fp'
+import * as R from 'ramda'
+import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'
+
+import {
+ java,
+ neon,
+ subheaderDarkColor,
+ offColor,
+ fontColor,
+ primaryColor,
+ fontSecondary,
+ subheaderColor
+} from 'src/styling/variables'
+import { numberToFiatAmount } from 'src/utils/number'
+import { MINUTE, DAY, WEEK, MONTH } from 'src/utils/time'
+
+const Graph = ({
+ data,
+ period,
+ timezone,
+ setSelectionCoords,
+ setSelectionData,
+ setSelectionDateInterval,
+ log = false
+}) => {
+ const ref = useRef(null)
+
+ const GRAPH_POPOVER_WIDTH = 150
+ const GRAPH_POPOVER_MARGIN = 25
+ const GRAPH_HEIGHT = 401
+ const GRAPH_WIDTH = 1163
+ const GRAPH_MARGIN = useMemo(
+ () => ({
+ top: 25,
+ right: 3.5,
+ bottom: 27,
+ left: 38
+ }),
+ []
+ )
+
+ const offset = getTimezoneOffset(timezone)
+ const NOW = Date.now() + offset
+
+ const periodDomains = {
+ day: [NOW - DAY, NOW],
+ threeDays: [NOW - 3 * DAY, NOW],
+ week: [NOW - WEEK, NOW],
+ month: [NOW - MONTH, NOW]
+ }
+
+ const dataPoints = useMemo(
+ () => ({
+ day: {
+ freq: 24,
+ step: 60 * 60 * 1000,
+ tick: d3.utcHour.every(1),
+ labelFormat: '%H:%M'
+ },
+ threeDays: {
+ freq: 12,
+ step: 6 * 60 * 60 * 1000,
+ tick: d3.utcDay.every(1),
+ labelFormat: '%a %d'
+ },
+ week: {
+ freq: 7,
+ step: 24 * 60 * 60 * 1000,
+ tick: d3.utcDay.every(1),
+ labelFormat: '%a %d'
+ },
+ month: {
+ freq: 30,
+ step: 24 * 60 * 60 * 1000,
+ tick: d3.utcDay.every(1),
+ labelFormat: '%d'
+ }
+ }),
+ []
+ )
+
+ const getPastAndCurrentDayLabels = useCallback(d => {
+ const currentDate = new Date(d)
+ const currentDateDay = currentDate.getUTCDate()
+ const currentDateWeekday = currentDate.getUTCDay()
+ const currentDateMonth = currentDate.getUTCMonth()
+
+ const previousDate = new Date(currentDate.getTime())
+ previousDate.setUTCDate(currentDateDay - 1)
+
+ const previousDateDay = previousDate.getUTCDate()
+ const previousDateWeekday = previousDate.getUTCDay()
+ const previousDateMonth = previousDate.getUTCMonth()
+
+ const daysOfWeek = Array.from(Array(7)).map((_, i) =>
+ format('EEE', add({ days: i }, startOfWeek(new Date())))
+ )
+
+ const months = Array.from(Array(12)).map((_, i) =>
+ format('LLL', add({ months: i }, startOfYear(new Date())))
+ )
+
+ return {
+ previous:
+ currentDateMonth !== previousDateMonth
+ ? months[previousDateMonth]
+ : `${daysOfWeek[previousDateWeekday]} ${previousDateDay}`,
+ current:
+ currentDateMonth !== previousDateMonth
+ ? months[currentDateMonth]
+ : `${daysOfWeek[currentDateWeekday]} ${currentDateDay}`
+ }
+ }, [])
+
+ const buildTicks = useCallback(
+ domain => {
+ const points = []
+
+ const roundDate = d => {
+ const step = dataPoints[period.code].step
+ return new Date(Math.ceil(d.valueOf() / step) * step)
+ }
+
+ for (let i = 0; i <= dataPoints[period.code].freq; i++) {
+ const stepDate = new Date(NOW - i * dataPoints[period.code].step)
+ if (roundDate(stepDate) > domain[1]) continue
+ if (stepDate < domain[0]) continue
+ points.push(roundDate(stepDate))
+ }
+
+ return points
+ },
+ [NOW, dataPoints, period.code]
+ )
+
+ const buildAreas = useCallback(
+ domain => {
+ const points = []
+
+ points.push(domain[1])
+
+ const roundDate = d => {
+ const step = dataPoints[period.code].step
+ return new Date(Math.ceil(d.valueOf() / step) * step)
+ }
+
+ for (let i = 0; i <= dataPoints[period.code].freq; i++) {
+ const stepDate = new Date(NOW - i * dataPoints[period.code].step)
+ if (roundDate(stepDate) > new Date(domain[1])) continue
+ if (stepDate < new Date(domain[0])) continue
+ points.push(roundDate(stepDate))
+ }
+
+ points.push(domain[0])
+
+ return points
+ },
+ [NOW, dataPoints, period.code]
+ )
+
+ const x = d3
+ .scaleUtc()
+ .domain(periodDomains[period.code])
+ .range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
+
+ // Create a second X axis for mouseover events to be placed correctly across the entire graph width and not limited by X's domain
+ const x2 = d3
+ .scaleUtc()
+ .domain(periodDomains[period.code])
+ .range([GRAPH_MARGIN.left, GRAPH_WIDTH])
+
+ const bins = buildAreas(x.domain())
+ .sort((a, b) => compareDesc(a.date, b.date))
+ .map(addMilliseconds(-dataPoints[period.code].step))
+ .map((date, i, dates) => {
+ // move first and last bin in such way
+ // that all bin have uniform width
+ if (i === 0)
+ return addMilliseconds(dataPoints[period.code].step, dates[1])
+ else if (i === dates.length - 1)
+ return addMilliseconds(
+ -dataPoints[period.code].step,
+ dates[dates.length - 2]
+ )
+ else return date
+ })
+ .map(date => {
+ const middleOfBin = addMilliseconds(
+ dataPoints[period.code].step / 2,
+ date
+ )
+
+ const txs = data.filter(tx => {
+ const txCreated = new Date(tx.created)
+ const shift = new Date(txCreated.getTime() + offset)
+
+ return (
+ Math.abs(differenceInMilliseconds(shift, middleOfBin)) <
+ dataPoints[period.code].step / 2
+ )
+ })
+
+ const cashIn = txs
+ .filter(tx => tx.txClass === 'cashIn')
+ .reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
+
+ const cashOut = txs
+ .filter(tx => tx.txClass === 'cashOut')
+ .reduce((sum, tx) => sum + new BigNumber(tx.fiat).toNumber(), 0)
+
+ return { date: middleOfBin, cashIn, cashOut }
+ })
+
+ const min = d3.min(bins, d => Math.min(d.cashIn, d.cashOut)) ?? 0
+ const max = d3.max(bins, d => Math.max(d.cashIn, d.cashOut)) ?? 1000
+
+ const yLin = d3
+ .scaleLinear()
+ .domain([0, (max === min ? min + 1000 : max) * 1.03])
+ .nice()
+ .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
+
+ const yLog = d3
+ .scaleLog()
+ .domain([
+ min === 0 ? 0.9 : min * 0.9,
+ (max === min ? min + Math.pow(10, 2 * min + 1) : max) * 2
+ ])
+ .clamp(true)
+ .range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
+
+ const y = log ? yLog : yLin
+
+ const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
+ const fullBreakpoints = [
+ graphLimits[1],
+ ...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints),
+ dataLimits[0]
+ ]
+
+ const intervals = []
+ for (let i = 0; i < fullBreakpoints.length - 1; i++) {
+ intervals.push([fullBreakpoints[i], fullBreakpoints[i + 1]])
+ }
+
+ return intervals
+ }
+
+ const getAreaIntervalByX = (intervals, xValue) => {
+ return R.find(it => xValue <= it[0] && xValue >= it[1], intervals) ?? [0, 0]
+ }
+
+ const getDateIntervalByX = (areas, intervals, xValue) => {
+ const flattenIntervals = R.uniq(R.flatten(intervals))
+
+ // flattenIntervals and areas should have the same number of elements
+ for (let i = intervals.length - 1; i >= 0; i--) {
+ if (xValue < flattenIntervals[i]) {
+ return [areas[i], areas[i + 1]]
+ }
+ }
+ }
+
+ const buildXAxis = useCallback(
+ g =>
+ g
+ .attr(
+ 'transform',
+ `translate(0, ${GRAPH_HEIGHT - GRAPH_MARGIN.bottom})`
+ )
+ .call(
+ d3
+ .axisBottom(x)
+ .ticks(dataPoints[period.code].tick)
+ .tickFormat(d => {
+ return d3.timeFormat(dataPoints[period.code].labelFormat)(
+ d.getTime() + d.getTimezoneOffset() * MINUTE
+ )
+ })
+ .tickSizeOuter(0)
+ )
+ .call(g =>
+ g
+ .select('.domain')
+ .attr('stroke', primaryColor)
+ .attr('stroke-width', 1)
+ ),
+ [GRAPH_MARGIN, dataPoints, period.code, x]
+ )
+
+ const buildYAxis = useCallback(
+ g =>
+ g
+ .attr('transform', `translate(${GRAPH_MARGIN.left}, 0)`)
+ .call(
+ d3
+ .axisLeft(y)
+ .ticks(GRAPH_HEIGHT / 100)
+ .tickSizeOuter(0)
+ .tickFormat(d => {
+ if (log && !['1', '2', '5'].includes(d.toString()[0])) return ''
+
+ if (d >= 1000) return numberToFiatAmount(d / 1000) + 'k'
+
+ return numberToFiatAmount(d)
+ })
+ )
+ .select('.domain')
+ .attr('stroke', primaryColor)
+ .attr('stroke-width', 1),
+ [GRAPH_MARGIN, y, log]
+ )
+
+ const buildGrid = useCallback(
+ g => {
+ g.attr('stroke', subheaderDarkColor)
+ .attr('fill', subheaderDarkColor)
+ // Vertical lines
+ .call(g =>
+ g
+ .append('g')
+ .selectAll('line')
+ .data(buildTicks(x.domain()))
+ .join('line')
+ .attr('x1', d => 0.5 + x(d))
+ .attr('x2', d => 0.5 + x(d))
+ .attr('y1', GRAPH_MARGIN.top)
+ .attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
+ )
+ // Horizontal lines
+ .call(g =>
+ g
+ .append('g')
+ .selectAll('line')
+ .data(
+ d3
+ .axisLeft(y)
+ .scale()
+ .ticks(GRAPH_HEIGHT / 100)
+ )
+ .join('line')
+ .attr('y1', d => 0.5 + y(d))
+ .attr('y2', d => 0.5 + y(d))
+ .attr('x1', GRAPH_MARGIN.left)
+ .attr('x2', GRAPH_WIDTH)
+ )
+ // Vertical transparent rectangles for events
+ .call(g =>
+ g
+ .append('g')
+ .selectAll('line')
+ .data(buildAreas(x.domain()))
+ .join('rect')
+ .attr('x', d => x(d))
+ .attr('y', GRAPH_MARGIN.top)
+ .attr('width', d => {
+ const xValue = Math.round(x(d) * 100) / 100
+ const intervals = getAreaInterval(
+ buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
+ x.range(),
+ x2.range()
+ )
+ const interval = getAreaIntervalByX(intervals, xValue)
+ return Math.round((interval[0] - interval[1]) * 100) / 100
+ })
+ .attr(
+ 'height',
+ GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top
+ )
+ .attr('stroke', 'transparent')
+ .attr('fill', 'transparent')
+ .on('mouseover', d => {
+ const xValue = Math.round(d.target.x.baseVal.value * 100) / 100
+ const areas = buildAreas(x.domain())
+ const intervals = getAreaInterval(
+ buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
+ x.range(),
+ x2.range()
+ )
+
+ const dateInterval = getDateIntervalByX(areas, intervals, xValue)
+ if (!dateInterval) return
+ const filteredData = data.filter(it => {
+ const created = new Date(it.created)
+ const tzCreated = created.setTime(created.getTime() + offset)
+ return (
+ tzCreated > new Date(dateInterval[1]) &&
+ tzCreated <= new Date(dateInterval[0])
+ )
+ })
+
+ const rectXCoords = {
+ left: R.clone(d.target.getBoundingClientRect().x),
+ right: R.clone(
+ d.target.getBoundingClientRect().x +
+ d.target.getBoundingClientRect().width
+ )
+ }
+
+ const xCoord =
+ d.target.x.baseVal.value < 0.75 * GRAPH_WIDTH
+ ? rectXCoords.right + GRAPH_POPOVER_MARGIN
+ : rectXCoords.left -
+ GRAPH_POPOVER_WIDTH -
+ GRAPH_POPOVER_MARGIN
+ const yCoord = R.clone(d.target.getBoundingClientRect().y)
+
+ setSelectionDateInterval(dateInterval)
+ setSelectionData(filteredData)
+ setSelectionCoords({
+ x: Math.round(xCoord),
+ y: Math.round(yCoord)
+ })
+
+ d3.select(d.target).attr('fill', subheaderColor)
+ })
+ .on('mouseleave', d => {
+ d3.select(d.target).attr('fill', 'transparent')
+ setSelectionDateInterval(null)
+ setSelectionData(null)
+ setSelectionCoords(null)
+ })
+ )
+ // Thick vertical lines
+ .call(g =>
+ g
+ .append('g')
+ .selectAll('line')
+ .data(
+ buildTicks(x.domain()).filter(x => {
+ if (period.code === 'day') return x.getUTCHours() === 0
+ return x.getUTCDate() === 1
+ })
+ )
+ .join('line')
+ .attr('class', 'dateSeparator')
+ .attr('x1', d => 0.5 + x(d))
+ .attr('x2', d => 0.5 + x(d))
+ .attr('y1', GRAPH_MARGIN.top - 50)
+ .attr('y2', GRAPH_HEIGHT - GRAPH_MARGIN.bottom)
+ .attr('stroke-width', 5)
+ .join('text')
+ )
+ // Left side breakpoint label
+ .call(g => {
+ const separator = d3
+ ?.select('.dateSeparator')
+ ?.node()
+ ?.getBBox()
+
+ if (!separator) return
+
+ const breakpoint = buildTicks(x.domain()).filter(x => {
+ if (period.code === 'day') return x.getUTCHours() === 0
+ return x.getUTCDate() === 1
+ })
+
+ const labels = getPastAndCurrentDayLabels(breakpoint)
+
+ return g
+ .append('text')
+ .attr('x', separator.x - 10)
+ .attr('y', separator.y + 33)
+ .attr('text-anchor', 'end')
+ .attr('dy', '.25em')
+ .text(labels.previous)
+ })
+ // Right side breakpoint label
+ .call(g => {
+ const separator = d3
+ ?.select('.dateSeparator')
+ ?.node()
+ ?.getBBox()
+
+ if (!separator) return
+
+ const breakpoint = buildTicks(x.domain()).filter(x => {
+ if (period.code === 'day') return x.getUTCHours() === 0
+ return x.getUTCDate() === 1
+ })
+
+ const labels = getPastAndCurrentDayLabels(breakpoint)
+
+ return g
+ .append('text')
+ .attr('x', separator.x + 10)
+ .attr('y', separator.y + 33)
+ .attr('text-anchor', 'start')
+ .attr('dy', '.25em')
+ .text(labels.current)
+ })
+ },
+ [
+ GRAPH_MARGIN,
+ buildTicks,
+ getPastAndCurrentDayLabels,
+ x,
+ x2,
+ y,
+ period,
+ buildAreas,
+ data,
+ offset,
+ setSelectionCoords,
+ setSelectionData,
+ setSelectionDateInterval
+ ]
+ )
+
+ const formatTicksText = useCallback(
+ () =>
+ d3
+ .selectAll('.tick text')
+ .style('stroke', fontColor)
+ .style('fill', fontColor)
+ .style('stroke-width', 0.5)
+ .style('font-family', fontSecondary),
+ []
+ )
+
+ const formatText = useCallback(
+ () =>
+ d3
+ .selectAll('text')
+ .style('stroke', offColor)
+ .style('fill', offColor)
+ .style('stroke-width', 0.5)
+ .style('font-family', fontSecondary),
+ []
+ )
+
+ const formatTicks = useCallback(() => {
+ d3.selectAll('.tick line')
+ .style('stroke', primaryColor)
+ .style('fill', primaryColor)
+ }, [])
+
+ const drawData = useCallback(
+ g => {
+ g.append('clipPath')
+ .attr('id', 'clip-path')
+ .append('rect')
+ .attr('x', GRAPH_MARGIN.left)
+ .attr('y', GRAPH_MARGIN.top)
+ .attr('width', GRAPH_WIDTH)
+ .attr('height', GRAPH_HEIGHT - GRAPH_MARGIN.bottom - GRAPH_MARGIN.top)
+ .attr('fill', java)
+
+ g.append('g')
+ .attr('clip-path', 'url(#clip-path)')
+ .selectAll('circle .cashIn')
+ .data(bins)
+ .join('circle')
+ .attr('cx', d => x(d.date))
+ .attr('cy', d => y(d.cashIn))
+ .attr('fill', java)
+ .attr('r', d => (d.cashIn === 0 ? 0 : 3.5))
+
+ g.append('path')
+ .datum(bins)
+ .attr('fill', 'none')
+ .attr('stroke', java)
+ .attr('stroke-width', 3)
+ .attr('clip-path', 'url(#clip-path)')
+ .attr(
+ 'd',
+ d3
+ .line()
+ .curve(d3.curveMonotoneX)
+ .x(d => x(d.date))
+ .y(d => y(d.cashIn))
+ )
+
+ g.append('g')
+ .attr('clip-path', 'url(#clip-path)')
+ .selectAll('circle .cashIn')
+ .data(bins)
+ .join('circle')
+ .attr('cx', d => x(d.date))
+ .attr('cy', d => y(d.cashOut))
+ .attr('fill', neon)
+ .attr('r', d => (d.cashOut === 0 ? 0 : 3.5))
+
+ g.append('path')
+ .datum(bins)
+ .attr('fill', 'none')
+ .attr('stroke', neon)
+ .attr('stroke-width', 3)
+ .attr('clip-path', 'url(#clip-path)')
+ .attr(
+ 'd',
+ d3
+ .line()
+ .curve(d3.curveMonotoneX)
+ .x(d => x(d.date))
+ .y(d => y(d.cashOut))
+ )
+ },
+ [x, y, bins, GRAPH_MARGIN]
+ )
+
+ const drawChart = useCallback(() => {
+ const svg = d3
+ .select(ref.current)
+ .attr('viewBox', [0, 0, GRAPH_WIDTH, GRAPH_HEIGHT])
+
+ svg.append('g').call(buildGrid)
+ svg.append('g').call(drawData)
+ svg.append('g').call(buildXAxis)
+ svg.append('g').call(buildYAxis)
+ svg.append('g').call(formatTicksText)
+ svg.append('g').call(formatText)
+ svg.append('g').call(formatTicks)
+
+ return svg.node()
+ }, [
+ buildGrid,
+ buildXAxis,
+ buildYAxis,
+ drawData,
+ formatText,
+ formatTicks,
+ formatTicksText
+ ])
+
+ useEffect(() => {
+ d3.select(ref.current)
+ .selectAll('*')
+ .remove()
+ drawChart()
+ }, [drawChart])
+
+ return
+}
+
+export default memo(
+ Graph,
+ (prev, next) =>
+ R.equals(prev.period, next.period) &&
+ R.equals(prev.selectedMachine, next.selectedMachine) &&
+ R.equals(prev.log, next.log)
+)
diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js
index b4918993..dec75592 100644
--- a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js
+++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js
@@ -7,8 +7,13 @@ import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
-import { HoverableTooltip } from 'src/components/Tooltip'
-import { Link, Button, IconButton } from 'src/components/buttons'
+import { HelpTooltip } from 'src/components/Tooltip'
+import {
+ Link,
+ Button,
+ IconButton,
+ SupportLinkButton
+} from 'src/components/buttons'
import { Switch } from 'src/components/inputs'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection'
@@ -251,13 +256,13 @@ const Blacklist = () => {
value={enablePaperWalletOnly}
/>
{enablePaperWalletOnly ? 'On' : 'Off'}
-
+
The "Enable paper wallet (only)" option means that only paper
wallets will be printed for users, and they won't be permitted
to scan an address from their own wallet.
-
+
{
value={rejectAddressReuse}
/>
{rejectAddressReuse ? 'On' : 'Off'}
-
+
The "Reject reused addresses" option means that all addresses
that are used once will be automatically rejected if there's
an attempt to use them again on a new transaction.
-
+
+ For details please read the relevant knowledgebase article:
+
+
+
{
return (
!loading && (
<>
-
+
+
+ For details on configuring cash-out, please read the relevant
+ knowledgebase article:
+
+
+
+ }>
Transaction fudge factor
{
{fudgeFactorActive ? 'On' : 'Off'}
-
+
Automatically accept customer deposits as complete if their
received amount is 100 crypto atoms or less.
@@ -114,7 +129,13 @@ const CashOut = ({ name: SCREEN_KEY }) => {
(Crypto atoms are the smallest unit in each cryptocurrency.
E.g., satoshis in Bitcoin, or wei in Ethereum.)
-
+ For details please read the relevant knowledgebase article:
+
+
Default Commissions
diff --git a/new-lamassu-admin/src/pages/Commissions/Commissions.js b/new-lamassu-admin/src/pages/Commissions/Commissions.js
index d9d25a52..1934a723 100644
--- a/new-lamassu-admin/src/pages/Commissions/Commissions.js
+++ b/new-lamassu-admin/src/pages/Commissions/Commissions.js
@@ -4,12 +4,16 @@ import gql from 'graphql-tag'
import * as R from 'ramda'
import React, { useState } from 'react'
+import { HelpTooltip } from 'src/components/Tooltip'
+import { SupportLinkButton } from 'src/components/buttons'
import TitleSection from 'src/components/layout/TitleSection'
import { ReactComponent as ReverseListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/white.svg'
import { ReactComponent as ListingViewIcon } from 'src/styling/icons/circle buttons/listing-view/zodiac.svg'
import { ReactComponent as OverrideLabelIcon } from 'src/styling/icons/status/spring2.svg'
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
+import { P } from '../../components/typography'
+
import CommissionsDetails from './components/CommissionsDetails'
import CommissionsList from './components/CommissionsList'
@@ -118,6 +122,24 @@ const Commissions = ({ name: SCREEN_KEY }) => {
}
]}
iconClassName={classes.listViewButton}
+ appendix={
+
+
+ For details about commissions, please read the relevant
+ knowledgebase articles:
+
+
+
+
+ }
/>
{!showMachines && !loading && (
diff --git a/new-lamassu-admin/src/pages/Commissions/components/CommissionsList.js b/new-lamassu-admin/src/pages/Commissions/components/CommissionsList.js
index 9ea641a1..ebf4f82e 100644
--- a/new-lamassu-admin/src/pages/Commissions/components/CommissionsList.js
+++ b/new-lamassu-admin/src/pages/Commissions/components/CommissionsList.js
@@ -37,7 +37,7 @@ const SHOW_ALL = {
const ORDER_OPTIONS = [
{
code: 'machine',
- display: 'Machine Name'
+ display: 'Machine name'
},
{
code: 'cryptoCurrencies',
@@ -53,7 +53,7 @@ const ORDER_OPTIONS = [
},
{
code: 'fixedFee',
- display: 'Fixed Fee'
+ display: 'Fixed fee'
},
{
code: 'minimumTx',
diff --git a/new-lamassu-admin/src/pages/Commissions/helper.js b/new-lamassu-admin/src/pages/Commissions/helper.js
index f93e6789..5447d2c7 100644
--- a/new-lamassu-admin/src/pages/Commissions/helper.js
+++ b/new-lamassu-admin/src/pages/Commissions/helper.js
@@ -91,7 +91,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
},
{
name: 'cryptoCurrencies',
- width: 280,
+ width: 145,
size: 'sm',
view: displayCodeArray(cryptoData),
input: Autocomplete,
@@ -108,7 +108,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
header: cashInHeader,
name: 'cashIn',
display: 'Cash-in',
- width: 130,
+ width: 123,
input: NumberInput,
textAlign: 'right',
suffix: '%',
@@ -121,7 +121,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
header: cashOutHeader,
name: 'cashOut',
display: 'Cash-out',
- width: 130,
+ width: 127,
input: NumberInput,
textAlign: 'right',
suffix: '%',
@@ -133,7 +133,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
{
name: 'fixedFee',
display: 'Fixed fee',
- width: 144,
+ width: 126,
input: NumberInput,
doubleHeader: 'Cash-in only',
textAlign: 'right',
@@ -146,7 +146,7 @@ const getOverridesFields = (getData, currency, auxElements) => {
{
name: 'minimumTx',
display: 'Minimum Tx',
- width: 169,
+ width: 140,
doubleHeader: 'Cash-in only',
textAlign: 'center',
editingAlign: 'right',
@@ -156,6 +156,20 @@ const getOverridesFields = (getData, currency, auxElements) => {
inputProps: {
decimalPlaces: 2
}
+ },
+ {
+ name: 'cashOutFixedFee',
+ display: 'Fixed fee',
+ width: 134,
+ doubleHeader: 'Cash-out only',
+ textAlign: 'center',
+ editingAlign: 'right',
+ input: NumberInput,
+ suffix: currency,
+ bold: bold,
+ inputProps: {
+ decimalPlaces: 2
+ }
}
]
}
@@ -218,6 +232,21 @@ const mainFields = currency => [
inputProps: {
decimalPlaces: 2
}
+ },
+ {
+ name: 'cashOutFixedFee',
+ display: 'Fixed fee',
+ width: 169,
+ size: 'lg',
+ doubleHeader: 'Cash-out only',
+ textAlign: 'center',
+ editingAlign: 'right',
+ input: NumberInput,
+ suffix: currency,
+ bold: bold,
+ inputProps: {
+ decimalPlaces: 2
+ }
}
]
@@ -245,7 +274,7 @@ const getSchema = locale => {
.max(percentMax)
.required(),
fixedFee: Yup.number()
- .label('Fixed Fee')
+ .label('Cash-in fixed fee')
.min(0)
.max(highestBill)
.required(),
@@ -253,6 +282,11 @@ const getSchema = locale => {
.label('Minimum Tx')
.min(0)
.max(highestBill)
+ .required(),
+ cashOutFixedFee: Yup.number()
+ .label('Cash-out fixed fee')
+ .min(0)
+ .max(highestBill)
.required()
})
}
@@ -326,7 +360,7 @@ const getOverridesSchema = (values, rawData, locale) => {
return true
}
})
- .label('Crypto Currencies')
+ .label('Crypto currencies')
.required()
.min(1),
cashIn: Yup.number()
@@ -340,7 +374,7 @@ const getOverridesSchema = (values, rawData, locale) => {
.max(percentMax)
.required(),
fixedFee: Yup.number()
- .label('Fixed Fee')
+ .label('Cash-in fixed fee')
.min(0)
.max(highestBill)
.required(),
@@ -348,6 +382,11 @@ const getOverridesSchema = (values, rawData, locale) => {
.label('Minimum Tx')
.min(0)
.max(highestBill)
+ .required(),
+ cashOutFixedFee: Yup.number()
+ .label('Cash-out fixed fee')
+ .min(0)
+ .max(highestBill)
.required()
})
}
@@ -356,7 +395,8 @@ const defaults = {
cashIn: '',
cashOut: '',
fixedFee: '',
- minimumTx: ''
+ minimumTx: '',
+ cashOutFixedFee: ''
}
const overridesDefaults = {
@@ -365,7 +405,8 @@ const overridesDefaults = {
cashIn: '',
cashOut: '',
fixedFee: '',
- minimumTx: ''
+ minimumTx: '',
+ cashOutFixedFee: ''
}
const getOrder = ({ machine, cryptoCurrencies }) => {
@@ -385,6 +426,7 @@ const createCommissions = (cryptoCode, deviceId, isDefault, config) => {
fixedFee: config.fixedFee,
cashOut: config.cashOut,
cashIn: config.cashIn,
+ cashOutFixedFee: config.cashOutFixedFee,
machine: deviceId,
cryptoCurrencies: [cryptoCode],
default: isDefault,
@@ -437,7 +479,7 @@ const getListCommissionsSchema = locale => {
.label('Machine')
.required(),
cryptoCurrencies: Yup.array()
- .label('Crypto Currency')
+ .label('Crypto currency')
.required()
.min(1),
cashIn: Yup.number()
@@ -451,7 +493,7 @@ const getListCommissionsSchema = locale => {
.max(percentMax)
.required(),
fixedFee: Yup.number()
- .label('Fixed Fee')
+ .label('Cash-in fixed fee')
.min(0)
.max(highestBill)
.required(),
@@ -459,6 +501,11 @@ const getListCommissionsSchema = locale => {
.label('Minimum Tx')
.min(0)
.max(highestBill)
+ .required(),
+ cashOutFixedFee: Yup.number()
+ .label('Cash-out fixed fee')
+ .min(0)
+ .max(highestBill)
.required()
})
}
@@ -487,7 +534,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
{
name: 'cryptoCurrencies',
display: 'Crypto Currency',
- width: 255,
+ width: 150,
view: R.prop(0),
size: 'sm',
editable: false
@@ -496,7 +543,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
header: cashInHeader,
name: 'cashIn',
display: 'Cash-in',
- width: 130,
+ width: 120,
input: NumberInput,
textAlign: 'right',
suffix: '%',
@@ -509,7 +556,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
header: cashOutHeader,
name: 'cashOut',
display: 'Cash-out',
- width: 140,
+ width: 126,
input: NumberInput,
textAlign: 'right',
greenText: true,
@@ -522,7 +569,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
{
name: 'fixedFee',
display: 'Fixed fee',
- width: 144,
+ width: 140,
input: NumberInput,
doubleHeader: 'Cash-in only',
textAlign: 'right',
@@ -535,7 +582,7 @@ const getListCommissionsFields = (getData, currency, defaults) => {
{
name: 'minimumTx',
display: 'Minimum Tx',
- width: 144,
+ width: 140,
input: NumberInput,
doubleHeader: 'Cash-in only',
textAlign: 'right',
@@ -544,6 +591,20 @@ const getListCommissionsFields = (getData, currency, defaults) => {
inputProps: {
decimalPlaces: 2
}
+ },
+ {
+ name: 'cashOutFixedFee',
+ display: 'Fixed fee',
+ width: 140,
+ input: NumberInput,
+ doubleHeader: 'Cash-out only',
+ textAlign: 'center',
+ editingAlign: 'right',
+ suffix: currency,
+ textStyle: obj => getTextStyle(obj),
+ inputProps: {
+ decimalPlaces: 2
+ }
}
]
}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js
index b605eb07..efb142b9 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerData.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js
@@ -73,10 +73,13 @@ const CustomerData = ({
authorizeCustomRequest,
updateCustomEntry,
retrieveAdditionalDataDialog,
- setRetrieve
+ setRetrieve,
+ checkAgainstSanctions
}) => {
const classes = useStyles()
const [listView, setListView] = useState(false)
+ const [previewPhoto, setPreviewPhoto] = useState(null)
+ const [previewCard, setPreviewCard] = useState(null)
const idData = R.path(['idCardData'])(customer)
const rawExpirationDate = R.path(['expirationDate'])(idData)
@@ -172,6 +175,12 @@ const CustomerData = ({
idCardData: R.merge(idData, formatDates(values))
}),
validationSchema: customerDataSchemas.idCardData,
+ checkAgainstSanctions: () =>
+ checkAgainstSanctions({
+ variables: {
+ customerId: R.path(['id'])(customer)
+ }
+ }),
initialValues: initialValues.idCardData,
isAvailable: !R.isNil(idData),
editable: true
@@ -213,9 +222,6 @@ const CustomerData = ({
{
title: 'Name',
titleIcon: ,
- authorize: () => {},
- reject: () => {},
- save: () => {},
isAvailable: false,
editable: true
},
@@ -226,7 +232,7 @@ const CustomerData = ({
authorize: () =>
updateCustomer({ sanctionsOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ sanctionsOverride: OVERRIDE_REJECTED }),
- children: {sanctionsDisplay} ,
+ children: () => {sanctionsDisplay} ,
isAvailable: !R.isNil(sanctions),
editable: true
},
@@ -238,20 +244,33 @@ const CustomerData = ({
authorize: () =>
updateCustomer({ frontCameraOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ frontCameraOverride: OVERRIDE_REJECTED }),
- save: values =>
- replacePhoto({
+ save: values => {
+ setPreviewPhoto(null)
+ return replacePhoto({
newPhoto: values.frontCamera,
photoType: 'frontCamera'
- }),
+ })
+ },
+ cancel: () => setPreviewPhoto(null),
deleteEditedData: () => deleteEditedData({ frontCamera: null }),
- children: customer.frontCameraPath ? (
-
- ) : null,
+ children: values => {
+ if (values.frontCamera !== previewPhoto) {
+ setPreviewPhoto(values.frontCamera)
+ }
+
+ return customer.frontCameraPath ? (
+
+ ) : null
+ },
hasImage: true,
validationSchema: customerDataSchemas.frontCamera,
initialValues: initialValues.frontCamera,
@@ -266,18 +285,33 @@ const CustomerData = ({
authorize: () =>
updateCustomer({ idCardPhotoOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ idCardPhotoOverride: OVERRIDE_REJECTED }),
- save: values =>
- replacePhoto({
+ save: values => {
+ setPreviewCard(null)
+ return replacePhoto({
newPhoto: values.idCardPhoto,
photoType: 'idCardPhoto'
- }),
+ })
+ },
+ cancel: () => setPreviewCard(null),
deleteEditedData: () => deleteEditedData({ idCardPhoto: null }),
- children: customer.idCardPhotoPath ? (
-
- ) : null,
+ children: values => {
+ if (values.idCardPhoto !== previewCard) {
+ setPreviewCard(values.idCardPhoto)
+ }
+
+ return customer.idCardPhotoPath ? (
+
+ ) : null
+ },
hasImage: true,
validationSchema: customerDataSchemas.idCardPhoto,
initialValues: initialValues.idCardPhoto,
@@ -292,6 +326,7 @@ const CustomerData = ({
authorize: () => updateCustomer({ usSsnOverride: OVERRIDE_AUTHORIZED }),
reject: () => updateCustomer({ usSsnOverride: OVERRIDE_REJECTED }),
save: values => editCustomer(values),
+ children: () => {},
deleteEditedData: () => deleteEditedData({ usSsn: null }),
validationSchema: customerDataSchemas.usSsn,
initialValues: initialValues.usSsn,
@@ -427,6 +462,7 @@ const CustomerData = ({
titleIcon,
fields,
save,
+ cancel,
deleteEditedData,
retrieveAdditionalData,
children,
@@ -434,7 +470,8 @@ const CustomerData = ({
initialValues,
hasImage,
hasAdditionalData,
- editable
+ editable,
+ checkAgainstSanctions
},
idx
) => {
@@ -453,8 +490,10 @@ const CustomerData = ({
validationSchema={validationSchema}
initialValues={initialValues}
save={save}
+ cancel={cancel}
deleteEditedData={deleteEditedData}
retrieveAdditionalData={retrieveAdditionalData}
+ checkAgainstSanctions={checkAgainstSanctions}
editable={editable}>
)
}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
index d9256f3b..44478022 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
@@ -1,4 +1,4 @@
-import { useQuery, useMutation } from '@apollo/react-hooks'
+import { useQuery, useMutation, useLazyQuery } from '@apollo/react-hooks'
import {
makeStyles,
Breadcrumbs,
@@ -292,6 +292,14 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql`
}
`
+const CHECK_AGAINST_SANCTIONS = gql`
+ query checkAgainstSanctions($customerId: ID) {
+ checkAgainstSanctions(customerId: $customerId) {
+ ofacSanctioned
+ }
+ }
+`
+
const CustomerProfile = memo(() => {
const history = useHistory()
@@ -400,6 +408,10 @@ const CustomerProfile = memo(() => {
onCompleted: () => getCustomer()
})
+ const [checkAgainstSanctions] = useLazyQuery(CHECK_AGAINST_SANCTIONS, {
+ onCompleted: () => getCustomer()
+ })
+
const updateCustomer = it =>
setCustomer({
variables: {
@@ -662,6 +674,7 @@ const CustomerProfile = memo(() => {
authorizeCustomRequest={authorizeCustomRequest}
updateCustomEntry={updateCustomEntry}
setRetrieve={setRetrieve}
+ checkAgainstSanctions={checkAgainstSanctions}
retrieveAdditionalDataDialog={
{
diff --git a/new-lamassu-admin/src/pages/Customers/CustomersList.js b/new-lamassu-admin/src/pages/Customers/CustomersList.js
index a7ed2a92..df39f4d9 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomersList.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomersList.js
@@ -36,7 +36,7 @@ const CustomersList = ({
view: getName
},
{
- header: 'Total TXs',
+ header: 'Total Txs',
width: 126,
textAlign: 'right',
view: it => `${Number.parseInt(it.totalTxs)}`
diff --git a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
index 6bcf3444..a698a218 100644
--- a/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
+++ b/new-lamassu-admin/src/pages/Customers/components/CustomerSidebar.js
@@ -26,7 +26,7 @@ const CustomerSidebar = ({ isSelected, onClick }) => {
},
{
code: 'customerData',
- display: 'Customer Data',
+ display: 'Customer data',
Icon: CustomerDataIcon,
InverseIcon: CustomerDataReversedIcon
},
diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js
index 1dc71165..503e1e39 100644
--- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js
+++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js
@@ -3,11 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames'
import { Form, Formik, Field as FormikField } from 'formik'
import * as R from 'ramda'
-import { useState, React } from 'react'
+import { useState, React, useRef } from 'react'
import ErrorMessage from 'src/components/ErrorMessage'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { MainStatus } from 'src/components/Status'
+// import { HelpTooltip } from 'src/components/Tooltip'
import { ActionButton } from 'src/components/buttons'
import { Label1, P, H3 } from 'src/components/typography'
import {
@@ -132,23 +133,27 @@ const ReadOnlyField = ({ field, value, ...props }) => {
const EditableCard = ({
fields,
- save,
- authorize,
+ save = () => {},
+ cancel = () => {},
+ authorize = () => {},
hasImage,
- reject,
+ reject = () => {},
state,
title,
titleIcon,
- children,
+ children = () => {},
validationSchema,
initialValues,
deleteEditedData,
retrieveAdditionalData,
hasAdditionalData = true,
- editable
+ editable,
+ checkAgainstSanctions
}) => {
const classes = useStyles()
+ const formRef = useRef()
+
const [editing, setEditing] = useState(false)
const [input, setInput] = useState(null)
const [error, setError] = useState(null)
@@ -178,7 +183,7 @@ const EditableCard = ({
{title}
{
// TODO: Enable for next release
- /* */
+ /* */
}
{state && authorize && (
@@ -187,8 +192,9 @@ const EditableCard = ({