import { useQuery, gql } from '@apollo/client' import classnames from 'classnames' import { endOfToday } from 'date-fns' import { subDays, format, add, startOfWeek } from 'date-fns/fp' import * as R from 'ramda' import React, { useState } from 'react' import TitleSection from '../../components/layout/TitleSection' import { Info2, P } from '../../components/typography' import DownIcon from '../../styling/icons/dashboard/down.svg?react' import EqualIcon from '../../styling/icons/dashboard/equal.svg?react' import UpIcon from '../../styling/icons/dashboard/up.svg?react' import { Select } from '../../components/inputs' import { fromNamespace } from '../../utils/config' import { numberToFiatAmount } from '../../utils/number' import { DAY, WEEK, MONTH } from '../../utils/time' import LegendEntry from './components/LegendEntry' import HourOfDayWrapper from './components/wrappers/HourOfDayWrapper' import OverTimeWrapper from './components/wrappers/OverTimeWrapper' import TopMachinesWrapper from './components/wrappers/TopMachinesWrapper' import VolumeOverTimeWrapper from './components/wrappers/VolumeOverTimeWrapper' const MACHINE_OPTIONS = [{ code: 'all', display: 'All machines' }] const REPRESENTING_OPTIONS = [ { code: 'overTime', display: 'Over time' }, { code: 'volumeOverTime', display: 'Volume' }, { code: 'topMachines', display: 'Top machines' }, { code: 'hourOfTheDay', display: 'Hour of the day' }, ] const PERIOD_OPTIONS = [ { code: 'day', display: 'Last 24 hours' }, { code: 'threeDays', display: 'Last 3 days' }, { code: 'week', display: 'Last 7 days' }, { code: 'month', display: 'Last 30 days' }, ] const TIME_OPTIONS = { day: DAY, threeDays: 3 * DAY, week: WEEK, month: MONTH, } const DAY_OPTIONS = R.map( it => ({ code: R.toLower(it), display: it, }), Array.from(Array(7)).map((_, i) => format('EEEE', add({ days: i }, startOfWeek(new Date()))), ), ) const GET_TRANSACTIONS = gql` query transactions( $from: DateTimeISO $until: DateTimeISO $excludeTestingCustomers: Boolean ) { transactions( from: $from until: $until excludeTestingCustomers: $excludeTestingCustomers ) { id txClass txHash toAddress commissionPercentage expired machineName operatorCompleted sendConfirmed dispense hasError: error deviceId fiat fixedFee fiatCode cryptoAtoms cryptoCode toAddress created profit } } ` const GET_DATA = gql` query getData { config machines { name deviceId } fiatRates { code name rate } } ` const VerticalLine = () => (
) const OverviewEntry = ({ label, value, oldValue, currency }) => { const _oldValue = !oldValue || R.equals(oldValue, 0) ? 1 : oldValue const growthRate = ((value - oldValue) * 100) / _oldValue const growthClasses = { 'font-bold': true, 'text-malachite': R.gt(value, oldValue), 'text-tomato': R.gt(oldValue, value), } return (

{label}

{numberToFiatAmount(value)} {!!currency && ` ${currency}`} {R.gt(growthRate, 0) && } {R.lt(growthRate, 0) && } {R.equals(growthRate, 0) && }

{numberToFiatAmount(growthRate)}%

) } const Analytics = () => { const { data: txResponse, loading: txLoading } = useQuery(GET_TRANSACTIONS, { variables: { from: subDays(65, endOfToday()), until: endOfToday(), excludeTestingCustomers: true, }, }) const { data: configResponse, loading: configLoading } = useQuery(GET_DATA) const [representing, setRepresenting] = useState(REPRESENTING_OPTIONS[0]) const [period, setPeriod] = useState(PERIOD_OPTIONS[0]) const [machine, setMachine] = useState(MACHINE_OPTIONS[0]) const [selectedDay, setSelectedDay] = useState( R.equals(representing.code, 'hourOfTheDay') ? DAY_OPTIONS[0] : null, ) const loading = txLoading || configLoading const transactions = R.path(['transactions'])(txResponse) ?? [] const machines = R.path(['machines'])(configResponse) ?? [] const config = R.path(['config'])(configResponse) ?? [] const rates = R.path(['fiatRates'])(configResponse) ?? [] const fiatLocale = fromNamespace('locale')(config).fiatCurrency const timezone = config?.locale_timezone const convertFiatToLocale = item => { if (item.fiatCode === fiatLocale) return item const itemRate = R.find(R.propEq(item.fiatCode, 'code'))(rates) const localeRate = R.find(R.propEq('code', fiatLocale))(rates) const multiplier = localeRate?.rate / itemRate?.rate return { ...item, fiat: parseFloat(item.fiat) * multiplier } } const data = R.map(convertFiatToLocale)( transactions?.filter( tx => (!tx.dispensed || !tx.expired) && (tx.sendConfirmed || tx.dispense) && !tx.hasError, ), ) ?? [] const machineOptions = R.clone(MACHINE_OPTIONS) R.forEach( m => machineOptions.push({ code: m.deviceId, display: m.name }), machines, ) const machineTxs = R.filter( tx => (machine.code === 'all' ? true : tx.deviceId === machine.code), data, ) const filteredData = timeInterval => ({ current: machineTxs.filter(d => { const txDay = new Date(d.created) const isSameWeekday = !R.isNil(selectedDay) ? R.equals(R.toLower(format('EEEE', txDay)), selectedDay.code) : true return isSameWeekday && txDay >= Date.now() - TIME_OPTIONS[timeInterval] }) ?? [], previous: machineTxs.filter(d => { const txDay = new Date(d.created) const isSameWeekday = !R.isNil(selectedDay) ? R.equals(R.toLower(format('EEEE', txDay)), selectedDay.code) : true return ( isSameWeekday && txDay < Date.now() - TIME_OPTIONS[timeInterval] && txDay >= Date.now() - 2 * TIME_OPTIONS[timeInterval] ) }) ?? [], }) const txs = { current: filteredData(period.code).current.length, previous: filteredData(period.code).previous.length, } const median = values => (values.length === 0 ? 0 : R.median(values)) const medianAmount = { current: median(R.map(d => d.fiat, filteredData(period.code).current)), previous: median(R.map(d => d.fiat, filteredData(period.code).previous)), } const txVolume = { current: R.sum(R.map(d => d.fiat, filteredData(period.code).current)), previous: R.sum(R.map(d => d.fiat, filteredData(period.code).previous)), } const commissions = { current: R.sum(R.map(d => d.profit, filteredData(period.code).current)), previous: R.sum(R.map(d => d.profit, filteredData(period.code).previous)), } const handleRepresentationChange = newRepresentation => { setRepresenting(newRepresentation) setSelectedDay( R.equals(newRepresentation.code, 'hourOfTheDay') ? DAY_OPTIONS[0] : null, ) } const getGraphInfo = representing => { switch (representing.code) { case 'overTime': return ( ) case 'volumeOverTime': return ( ) case 'topMachines': return ( ) case 'hourOfTheDay': return ( ) default: throw new Error(`There's no graph info to represent ${representing}`) } } return ( !loading && ( <>
{getGraphInfo(representing)} ) ) } export default Analytics