chore: use monorepo organization
This commit is contained in:
parent
deaf7d6ecc
commit
a687827f7e
1099 changed files with 8184 additions and 11535 deletions
155
packages/admin-ui/src/pages/Maintenance/CashUnitDetails.jsx
Normal file
155
packages/admin-ui/src/pages/Maintenance/CashUnitDetails.jsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import Chip from '@mui/material/Chip'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { Label1, TL2 } from 'src/components/typography'
|
||||
|
||||
import { CashOut } from 'src/components/inputs'
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { getCashUnitCapacity, modelPrettifier } from 'src/utils/machine'
|
||||
|
||||
const CashUnitDetails = ({
|
||||
machine,
|
||||
bills,
|
||||
currency,
|
||||
config,
|
||||
hideMachineData = false
|
||||
}) => {
|
||||
const billCount = R.countBy(it => it.fiat)(bills)
|
||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
||||
const cashout = fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
|
||||
const minWidth = hideMachineData ? 'min-w-15' : 'min-w-40'
|
||||
const VerticalLine = () => <span className="h-full w-[1px] bg-comet2" />
|
||||
|
||||
return (
|
||||
<div className="flex flex-row mt-3 mb-4 gap-10 min-h-30">
|
||||
{!hideMachineData && (
|
||||
<div className="min-w-52">
|
||||
<Label1>Machine Model</Label1>
|
||||
<span>{modelPrettifier[machine.model]}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={`flex flex-col ${minWidth}`}>
|
||||
<Label1>Cash box</Label1>
|
||||
{R.isEmpty(billCount) && <TL2 noMargin>Empty</TL2>}
|
||||
{R.keys(billCount).map((it, idx) => (
|
||||
<span className="flex items-center" key={idx}>
|
||||
<TL2 className="min-w-7" noMargin>
|
||||
{billCount[it]}
|
||||
</TL2>
|
||||
<Chip label={`${it} ${currency}`} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
{machine.numberOfRecyclers === 0 &&
|
||||
R.map(it => (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label1 noMargin>{`Cassette ${it}`}</Label1>
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`cassette${it}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
capacity={getCashUnitCapacity(machine.model, 'cassette')}
|
||||
/>
|
||||
</div>
|
||||
{it !== machine.numberOfCassettes && <VerticalLine />}
|
||||
</>
|
||||
))(R.range(1, machine.numberOfCassettes + 1))}
|
||||
{machine.numberOfRecyclers > 0 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label1 noMargin>{`Loading boxes`}</Label1>
|
||||
<div className="flex flex-col gap-5">
|
||||
{R.range(1, machine.numberOfCassettes + 1).map((it, idx) => (
|
||||
<CashOut
|
||||
key={idx}
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`cassette${it}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageCassette${it}`
|
||||
]
|
||||
}
|
||||
capacity={getCashUnitCapacity(machine.model, 'cassette')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<VerticalLine />
|
||||
{R.map(it => (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label1 noMargin>
|
||||
{`Recycler ${
|
||||
machine.model === 'aveiro'
|
||||
? `${it} f/r`
|
||||
: `${it * 2 - 1} - ${it * 2}`
|
||||
}`}
|
||||
</Label1>
|
||||
<div className="flex flex-col gap-5">
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`recycler${it * 2 - 1}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`recycler${it * 2 - 1}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageRecycler${it * 2 - 1}`
|
||||
]
|
||||
}
|
||||
capacity={getCashUnitCapacity(machine.model, 'recycler')}
|
||||
/>
|
||||
<CashOut
|
||||
width={60}
|
||||
height={40}
|
||||
currency={{ code: currency }}
|
||||
notes={machine.cashUnits[`recycler${it * 2}`]}
|
||||
denomination={
|
||||
getCashoutSettings(machine.id ?? machine.deviceId)[
|
||||
`recycler${it * 2}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageRecycler${it * 2}`
|
||||
]
|
||||
}
|
||||
capacity={getCashUnitCapacity(machine.model, 'recycler')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{it !== machine.numberOfRecyclers / 2 && <VerticalLine />}
|
||||
</>
|
||||
))(R.range(1, machine.numberOfRecyclers / 2 + 1))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashUnitDetails
|
||||
353
packages/admin-ui/src/pages/Maintenance/CashUnits.jsx
Normal file
353
packages/admin-ui/src/pages/Maintenance/CashUnits.jsx
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import { useQuery, useMutation, gql } from '@apollo/client'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { P, Label1 } from 'src/components/typography'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import ReverseHistoryIcon from 'src/styling/icons/circle buttons/history/white.svg?react'
|
||||
import HistoryIcon from 'src/styling/icons/circle buttons/history/zodiac.svg?react'
|
||||
|
||||
import { Button, SupportLinkButton } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import { EmptyTable } from 'src/components/table'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
import { MANUAL, AUTOMATIC } from 'src/utils/constants'
|
||||
import { onlyFirstToUpper } from 'src/utils/string'
|
||||
|
||||
import CashUnitDetails from './CashUnitDetails'
|
||||
import CashCassettesFooter from './CashUnitsFooter'
|
||||
import CashboxHistory from './CashboxHistory'
|
||||
import Wizard from './Wizard/Wizard'
|
||||
import helper from './helper'
|
||||
|
||||
const GET_MACHINES_AND_CONFIG = gql`
|
||||
query getData($billFilters: JSONObject) {
|
||||
machines {
|
||||
name
|
||||
id: deviceId
|
||||
model
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
recycler1
|
||||
recycler2
|
||||
recycler3
|
||||
recycler4
|
||||
recycler5
|
||||
recycler6
|
||||
}
|
||||
numberOfCassettes
|
||||
numberOfRecyclers
|
||||
}
|
||||
unpairedMachines {
|
||||
id: deviceId
|
||||
name
|
||||
}
|
||||
config
|
||||
bills(filters: $billFilters) {
|
||||
id
|
||||
fiat
|
||||
created
|
||||
deviceId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const SET_CASSETTE_BILLS = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$cashUnits: CashUnitsInput
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, cashUnits: $cashUnits) {
|
||||
deviceId
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
recycler1
|
||||
recycler2
|
||||
recycler3
|
||||
recycler4
|
||||
recycler5
|
||||
recycler6
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_BATCHES_CSV = gql`
|
||||
query cashboxBatchesCsv(
|
||||
$from: DateTimeISO
|
||||
$until: DateTimeISO
|
||||
$timezone: String
|
||||
) {
|
||||
cashboxBatchesCsv(from: $from, until: $until, timezone: $timezone)
|
||||
}
|
||||
`
|
||||
|
||||
const widths = {
|
||||
name: 250,
|
||||
cashbox: 200,
|
||||
cassettes: 575,
|
||||
edit: 90
|
||||
}
|
||||
|
||||
const CashCassettes = () => {
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [editingSchema, setEditingSchema] = useState(null)
|
||||
const [selectedRadio, setSelectedRadio] = useState(null)
|
||||
|
||||
const { data, loading: dataLoading } = useQuery(GET_MACHINES_AND_CONFIG, {
|
||||
variables: {
|
||||
billFilters: {
|
||||
batch: 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
const [wizard, setWizard] = useState(false)
|
||||
const [machineId, setMachineId] = useState('')
|
||||
|
||||
const machines = R.path(['machines'])(data) ?? []
|
||||
const unpairedMachines = R.path(['unpairedMachines'])(data) ?? []
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
const [setCassetteBills, { error }] = useMutation(SET_CASSETTE_BILLS, {
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
onCompleted: () => setEditingSchema(false),
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const timezone = R.path(['config', 'locale_timezone'], data)
|
||||
|
||||
const bills = R.groupBy(bill => bill.deviceId)(R.path(['bills'])(data) ?? [])
|
||||
const deviceIds = R.uniq(
|
||||
R.map(R.prop('deviceId'))(R.path(['bills'])(data) ?? [])
|
||||
)
|
||||
const cashout = data?.config && fromNamespace('cashOut')(data.config)
|
||||
const locale = data?.config && fromNamespace('locale')(data.config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
|
||||
const onSave = (id, cashUnits) => {
|
||||
return setCassetteBills({
|
||||
variables: {
|
||||
action: 'setCassetteBills',
|
||||
deviceId: id,
|
||||
cashUnits
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const cashboxReset =
|
||||
data?.config && fromNamespace('cashIn')(data.config).cashboxReset
|
||||
|
||||
const cashboxResetSave = rawConfig => {
|
||||
const config = toNamespace('cashIn')(rawConfig)
|
||||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
const saveCashboxOption = selection => {
|
||||
if (selection) {
|
||||
cashboxResetSave({ cashboxReset: selection })
|
||||
setEditingSchema(false)
|
||||
}
|
||||
}
|
||||
|
||||
const radioButtonOptions = [
|
||||
{ display: 'Automatic', code: AUTOMATIC },
|
||||
{ display: 'Manual', code: MANUAL }
|
||||
]
|
||||
|
||||
const handleRadioButtons = evt => {
|
||||
const selectedRadio = R.path(['target', 'value'])(evt)
|
||||
setSelectedRadio(selectedRadio)
|
||||
}
|
||||
|
||||
const elements = helper.getElements(
|
||||
config,
|
||||
bills,
|
||||
setWizard,
|
||||
widths,
|
||||
setMachineId
|
||||
)
|
||||
|
||||
const InnerCashUnitDetails = ({ it }) => (
|
||||
<CashUnitDetails
|
||||
machine={it}
|
||||
bills={bills[it.id] ?? []}
|
||||
currency={fiatCurrency}
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
!dataLoading && (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Cash boxes & cassettes"
|
||||
buttons={[
|
||||
{
|
||||
text: 'Cash box history',
|
||||
icon: HistoryIcon,
|
||||
inverseIcon: ReverseHistoryIcon,
|
||||
toggle: setShowHistory
|
||||
},
|
||||
{
|
||||
component: showHistory ? (
|
||||
<LogsDowloaderPopover
|
||||
className="ml-4"
|
||||
title="Download logs"
|
||||
name="cashboxHistory"
|
||||
query={GET_BATCHES_CSV}
|
||||
getLogs={logs => R.path(['cashboxBatchesCsv'])(logs)}
|
||||
timezone={timezone}
|
||||
args={{ timezone }}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
]}
|
||||
className="flex items-center mr-[1px]"
|
||||
appendix={
|
||||
<HelpTooltip width={220}>
|
||||
<P>
|
||||
For details on configuring cash boxes and cassettes, please read
|
||||
the relevant knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/4420839641229-Cash-Boxes-Cassettess"
|
||||
label="Cash Boxes & Cassettes"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
}>
|
||||
{!showHistory && (
|
||||
<div className="flex flex-col items-end">
|
||||
<Label1 noMargin className="text-comet">
|
||||
Cash box resets
|
||||
</Label1>
|
||||
<div className="flex items-center justify-end -mr-1">
|
||||
{cashboxReset && (
|
||||
<P noMargin className="mr-2">
|
||||
{onlyFirstToUpper(cashboxReset)}
|
||||
</P>
|
||||
)}
|
||||
<IconButton onClick={() => setEditingSchema(true)}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TitleSection>
|
||||
{!showHistory && (
|
||||
<>
|
||||
<DataTable
|
||||
loading={dataLoading}
|
||||
elements={elements}
|
||||
data={machines}
|
||||
Details={InnerCashUnitDetails}
|
||||
emptyText="No machines so far"
|
||||
expandable
|
||||
tableClassName="mb-20"
|
||||
/>
|
||||
|
||||
{data && R.isEmpty(machines) && (
|
||||
<EmptyTable message="No machines so far" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showHistory && (
|
||||
<CashboxHistory
|
||||
machines={R.concat(machines, unpairedMachines)}
|
||||
currency={fiatCurrency}
|
||||
timezone={timezone}
|
||||
/>
|
||||
)}
|
||||
<CashCassettesFooter
|
||||
currencyCode={fiatCurrency}
|
||||
machines={machines}
|
||||
config={config}
|
||||
bills={R.path(['bills'])(data)}
|
||||
deviceIds={deviceIds}
|
||||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
machine={R.find(R.propEq('id', machineId), machines)}
|
||||
cashoutSettings={getCashoutSettings(machineId)}
|
||||
onClose={() => {
|
||||
setWizard(false)
|
||||
}}
|
||||
error={error?.message}
|
||||
save={onSave}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
{editingSchema && (
|
||||
<Modal
|
||||
title={'Cash box resets'}
|
||||
width={478}
|
||||
handleClose={() => setEditingSchema(null)}
|
||||
open={true}>
|
||||
<P className="text-comet mt-0">
|
||||
We can automatically assume you emptied a bill validator's cash
|
||||
box when the machine detects that it has been removed.
|
||||
</P>
|
||||
<RadioGroup
|
||||
name="set-automatic-reset"
|
||||
value={selectedRadio ?? cashboxReset}
|
||||
options={[radioButtonOptions[0]]}
|
||||
onChange={handleRadioButtons}
|
||||
/>
|
||||
<P className="text-comet mt-0">
|
||||
Assume the cash box is emptied whenever it's removed, creating a
|
||||
new batch on the history screen and setting its current balance to
|
||||
zero.
|
||||
</P>
|
||||
<RadioGroup
|
||||
name="set-manual-reset"
|
||||
value={selectedRadio ?? cashboxReset}
|
||||
options={[radioButtonOptions[1]]}
|
||||
onChange={handleRadioButtons}
|
||||
/>
|
||||
<P className="text-comet mt-0">
|
||||
Cash boxes won't be assumed emptied when removed, nor their counts
|
||||
modified. Instead, to update the count and create a new batch,
|
||||
you'll click the 'Edit' button on this panel.
|
||||
</P>
|
||||
<DialogActions>
|
||||
<Button onClick={() => saveCashboxOption(selectedRadio)}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default CashCassettes
|
||||
116
packages/admin-ui/src/pages/Maintenance/CashUnitsFooter.jsx
Normal file
116
packages/admin-ui/src/pages/Maintenance/CashUnitsFooter.jsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import BigNumber from 'bignumber.js'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { Info1, Info2, Info3 } from 'src/components/typography/index'
|
||||
import TxInIcon from 'src/styling/icons/direction/cash-in.svg?react'
|
||||
import TxOutIcon from 'src/styling/icons/direction/cash-out.svg?react'
|
||||
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
|
||||
const CashCassettesFooter = ({
|
||||
machines,
|
||||
config,
|
||||
currencyCode,
|
||||
bills,
|
||||
deviceIds
|
||||
}) => {
|
||||
const cashout = config && fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
const cashoutReducerFn = (
|
||||
acc,
|
||||
{ cashUnits: { cassette1, cassette2, cassette3, cassette4 }, id }
|
||||
) => {
|
||||
const cassette1Denomination = getCashoutSettings(id).cassette1 ?? 0
|
||||
const cassette2Denomination = getCashoutSettings(id).cassette2 ?? 0
|
||||
const cassette3Denomination = getCashoutSettings(id).cassette3 ?? 0
|
||||
const cassette4Denomination = getCashoutSettings(id).cassette4 ?? 0
|
||||
return [
|
||||
(acc[0] += cassette1 * cassette1Denomination),
|
||||
(acc[1] += cassette2 * cassette2Denomination),
|
||||
(acc[2] += cassette3 * cassette3Denomination),
|
||||
(acc[3] += cassette4 * cassette4Denomination)
|
||||
]
|
||||
}
|
||||
|
||||
const recyclerReducerFn = (
|
||||
acc,
|
||||
{
|
||||
cashUnits: {
|
||||
recycler1,
|
||||
recycler2,
|
||||
recycler3,
|
||||
recycler4,
|
||||
recycler5,
|
||||
recycler6
|
||||
},
|
||||
id
|
||||
}
|
||||
) => {
|
||||
const recycler1Denomination = getCashoutSettings(id).recycler1 ?? 0
|
||||
const recycler2Denomination = getCashoutSettings(id).recycler2 ?? 0
|
||||
const recycler3Denomination = getCashoutSettings(id).recycler3 ?? 0
|
||||
const recycler4Denomination = getCashoutSettings(id).recycler4 ?? 0
|
||||
const recycler5Denomination = getCashoutSettings(id).recycler5 ?? 0
|
||||
const recycler6Denomination = getCashoutSettings(id).recycler6 ?? 0
|
||||
return [
|
||||
(acc[0] += recycler1 * recycler1Denomination),
|
||||
(acc[1] += recycler2 * recycler2Denomination),
|
||||
(acc[0] += recycler3 * recycler3Denomination),
|
||||
(acc[1] += recycler4 * recycler4Denomination),
|
||||
(acc[0] += recycler5 * recycler5Denomination),
|
||||
(acc[1] += recycler6 * recycler6Denomination)
|
||||
]
|
||||
}
|
||||
|
||||
const totalInRecyclers = R.sum(
|
||||
R.reduce(recyclerReducerFn, [0, 0, 0, 0, 0, 0], machines)
|
||||
)
|
||||
|
||||
const totalInCassettes = R.sum(
|
||||
R.reduce(cashoutReducerFn, [0, 0, 0, 0], machines)
|
||||
)
|
||||
|
||||
const totalInCashBox = R.sum(R.map(it => it.fiat)(bills))
|
||||
|
||||
const total = new BigNumber(
|
||||
totalInCassettes + totalInCashBox + totalInRecyclers
|
||||
).toFormat(0)
|
||||
|
||||
return (
|
||||
<div className="fixed h-16 left-0 bottom-0 w-[100vw] bg-white flex justify-around shadow-2xl">
|
||||
<div className="w-300 max-h-16 flex fixed justify-around">
|
||||
<Info3 className="text-comet self-center">Cash value in System</Info3>
|
||||
<div className="flex">
|
||||
<TxInIcon className="self-center h-5 w-5 mr-2" />
|
||||
<Info2 className="self-center mr-2">Cash-in:</Info2>
|
||||
<Info1 className="self-center">
|
||||
{numberToFiatAmount(totalInCashBox)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<TxOutIcon className="self-center h-5 w-5" />
|
||||
<Info2 className="self-center">Cash-out:</Info2>
|
||||
<Info1 className="self-center">
|
||||
{numberToFiatAmount(totalInCassettes)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<TxOutIcon className="self-center h-5 w-5" />
|
||||
<Info2 className="self-center">Recycle:</Info2>
|
||||
<Info1 className="self-center">
|
||||
{numberToFiatAmount(totalInRecyclers)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Info2 className="self-center">Total:</Info2>
|
||||
<Info1 className="self-center">
|
||||
{numberToFiatAmount(total)} {currencyCode}
|
||||
</Info1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashCassettesFooter
|
||||
138
packages/admin-ui/src/pages/Maintenance/CashboxHistory.jsx
Normal file
138
packages/admin-ui/src/pages/Maintenance/CashboxHistory.jsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import TxInIcon from 'src/styling/icons/direction/cash-in.svg?react'
|
||||
import TxOutIcon from 'src/styling/icons/direction/cash-out.svg?react'
|
||||
|
||||
import { NumberInput } from 'src/components/inputs/formik'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
const GET_BATCHES = gql`
|
||||
query cashboxBatches {
|
||||
cashboxBatches {
|
||||
id
|
||||
deviceId
|
||||
created
|
||||
operationType
|
||||
customBillCount
|
||||
performedBy
|
||||
billCount
|
||||
fiatTotal
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CashboxHistory = ({ machines, currency, timezone }) => {
|
||||
const { data: batchesData, loading: batchesLoading } = useQuery(GET_BATCHES)
|
||||
|
||||
const loading = batchesLoading
|
||||
|
||||
const batches = R.path(['cashboxBatches'])(batchesData)
|
||||
|
||||
const getOperationRender = R.reduce(
|
||||
(ret, i) =>
|
||||
R.pipe(
|
||||
R.assoc(
|
||||
`cash-cassette-${i}-refill`,
|
||||
<>
|
||||
<TxOutIcon />
|
||||
<span>Cash cassette {i} refill</span>
|
||||
</>
|
||||
),
|
||||
R.assoc(
|
||||
`cash-cassette-${i}-empty`,
|
||||
<>
|
||||
<TxOutIcon />
|
||||
<span>Cash cassette {i} emptied</span>
|
||||
</>
|
||||
)
|
||||
)(ret),
|
||||
{
|
||||
'cash-box-empty': (
|
||||
<>
|
||||
<TxInIcon />
|
||||
<span>Cash box emptied</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
R.range(1, 5)
|
||||
)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'operation',
|
||||
header: 'Operation',
|
||||
width: 200,
|
||||
textAlign: 'left',
|
||||
view: it => (
|
||||
<div className="flex items-center gap-2">
|
||||
{getOperationRender[it.operationType]}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'machine',
|
||||
header: 'Machine',
|
||||
width: 200,
|
||||
textAlign: 'left',
|
||||
view: R.pipe(
|
||||
R.prop('deviceId'),
|
||||
id => R.find(R.propEq('id', id), machines),
|
||||
R.defaultTo({ name: <i>Unpaired device</i> }),
|
||||
R.prop('name')
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'billCount',
|
||||
header: 'Bill count',
|
||||
width: 115,
|
||||
textAlign: 'left',
|
||||
input: NumberInput,
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
view: it =>
|
||||
R.isNil(it.customBillCount) ? it.billCount : it.customBillCount
|
||||
},
|
||||
{
|
||||
name: 'total',
|
||||
header: 'Total',
|
||||
width: 180,
|
||||
textAlign: 'right',
|
||||
view: it => (
|
||||
<span>
|
||||
{it.fiatTotal} {currency}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
header: 'Date',
|
||||
width: 135,
|
||||
textAlign: 'right',
|
||||
view: it => formatDate(it.created, timezone, 'yyyy-MM-dd')
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
header: 'Time (h:m)',
|
||||
width: 125,
|
||||
textAlign: 'right',
|
||||
view: it => formatDate(it.created, timezone, 'HH:mm')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 mb-20">
|
||||
<DataTable
|
||||
loading={loading}
|
||||
name="cashboxHistory"
|
||||
elements={elements}
|
||||
data={batches}
|
||||
emptyText="No cash box batches so far"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CashboxHistory
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import BigNumber from 'bignumber.js'
|
||||
import React from 'react'
|
||||
|
||||
import MachineActions from 'src/components/machineActions/MachineActions'
|
||||
import { Label1 } from 'src/components/typography/index.jsx'
|
||||
|
||||
import { modelPrettifier } from 'src/utils/machine'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
const Label = ({ children }) => {
|
||||
return <Label1 className="text-comet mb-1">{children}</Label1>
|
||||
}
|
||||
|
||||
const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap mt-3 mb-4 text-sm">
|
||||
<div className="w-1/4">
|
||||
<Label>Machine model</Label>
|
||||
<span>{modelPrettifier[machine.model]}</span>
|
||||
</div>
|
||||
<div className="w-1/4">
|
||||
<Label>Paired at</Label>
|
||||
<span>
|
||||
{timezone &&
|
||||
formatDate(machine.pairedAt, timezone, 'yyyy-MM-dd HH:mm:ss')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-1/2 flex-1/2">
|
||||
<MachineActions
|
||||
machine={machine}
|
||||
onActionSuccess={onActionSuccess}></MachineActions>
|
||||
</div>
|
||||
<div className="w-1/6">
|
||||
<Label>Network speed</Label>
|
||||
<span>
|
||||
{machine.downloadSpeed
|
||||
? new BigNumber(machine.downloadSpeed).toFixed(4).toString() +
|
||||
' MB/s'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-1/6">
|
||||
<Label>Latency</Label>
|
||||
<span>
|
||||
{machine.responseTime
|
||||
? new BigNumber(machine.responseTime).toFixed(3).toString() + ' ms'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-1/6">
|
||||
<Label>Packet loss</Label>
|
||||
<span>
|
||||
{machine.packetLoss
|
||||
? new BigNumber(machine.packetLoss).toFixed(3).toString() + ' %'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MachineDetailsRow
|
||||
154
packages/admin-ui/src/pages/Maintenance/MachineStatus.jsx
Normal file
154
packages/admin-ui/src/pages/Maintenance/MachineStatus.jsx
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import { formatDistance } from 'date-fns'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
import { MainStatus } from 'src/components/Status'
|
||||
import Title from 'src/components/Title'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { Label1 } from 'src/components/typography/index.jsx'
|
||||
import MachineRedirectIcon from 'src/styling/icons/month arrows/right.svg?react'
|
||||
import WarningIcon from 'src/styling/icons/status/pumpkin.svg?react'
|
||||
import ErrorIcon from 'src/styling/icons/status/tomato.svg?react'
|
||||
|
||||
import MachineDetailsRow from './MachineDetailsCard'
|
||||
|
||||
const GET_MACHINES = gql`
|
||||
{
|
||||
machines {
|
||||
name
|
||||
deviceId
|
||||
lastPing
|
||||
pairedAt
|
||||
version
|
||||
paired
|
||||
cashUnits {
|
||||
cashbox
|
||||
cassette1
|
||||
cassette2
|
||||
cassette3
|
||||
cassette4
|
||||
recycler1
|
||||
recycler2
|
||||
recycler3
|
||||
recycler4
|
||||
recycler5
|
||||
recycler6
|
||||
}
|
||||
version
|
||||
model
|
||||
statuses {
|
||||
label
|
||||
type
|
||||
}
|
||||
downloadSpeed
|
||||
responseTime
|
||||
packetLoss
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const MachineStatus = () => {
|
||||
const history = useHistory()
|
||||
const { state } = useLocation()
|
||||
const addedMachineId = state?.id
|
||||
const {
|
||||
data: machinesResponse,
|
||||
refetch,
|
||||
loading: machinesLoading
|
||||
} = useQuery(GET_MACHINES)
|
||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Machine name',
|
||||
width: 250,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => (
|
||||
<div className="flex items-center gap-2">
|
||||
{m.name}
|
||||
<div
|
||||
onClick={() => {
|
||||
history.push(`/machines/${m.deviceId}`)
|
||||
}}>
|
||||
<MachineRedirectIcon />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
width: 350,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => <MainStatus statuses={m.statuses} />
|
||||
},
|
||||
{
|
||||
header: 'Last ping',
|
||||
width: 200,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m =>
|
||||
m.lastPing
|
||||
? formatDistance(new Date(m.lastPing), new Date(), {
|
||||
addSuffix: true
|
||||
})
|
||||
: 'unknown'
|
||||
},
|
||||
{
|
||||
header: 'Software version',
|
||||
width: 200,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => m.version || 'unknown'
|
||||
}
|
||||
]
|
||||
|
||||
const machines = R.path(['machines'])(machinesResponse) ?? []
|
||||
const expandedIndex = R.findIndex(R.propEq('deviceId', addedMachineId))(
|
||||
machines
|
||||
)
|
||||
|
||||
const InnerMachineDetailsRow = ({ it }) => (
|
||||
<MachineDetailsRow it={it} onActionSuccess={refetch} timezone={timezone} />
|
||||
)
|
||||
|
||||
const loading = machinesLoading || configLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Machine status</Title>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<WarningIcon />
|
||||
<Label1 noMargin>Warning</Label1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ErrorIcon />
|
||||
<Label1 noMargin>Error</Label1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
loading={loading}
|
||||
elements={elements}
|
||||
data={machines}
|
||||
Details={InnerMachineDetailsRow}
|
||||
initialExpanded={expandedIndex}
|
||||
emptyText="No machines so far"
|
||||
expandable
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MachineStatus
|
||||
209
packages/admin-ui/src/pages/Maintenance/Wizard/Wizard.jsx
Normal file
209
packages/admin-ui/src/pages/Maintenance/Wizard/Wizard.jsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import Modal from 'src/components/Modal'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { MAX_NUMBER_OF_CASSETTES } from 'src/utils/constants'
|
||||
import {
|
||||
cashUnitCapacity,
|
||||
getCashUnitCapacity,
|
||||
modelPrettifier
|
||||
} from 'src/utils/machine'
|
||||
import { defaultToZero } from 'src/utils/number'
|
||||
|
||||
import WizardSplash from './WizardSplash'
|
||||
import WizardStep from './WizardStep'
|
||||
|
||||
const MODAL_WIDTH = 554
|
||||
const MODAL_HEIGHT = 535
|
||||
|
||||
const CASSETTE_FIELDS = R.map(
|
||||
it => `cassette${it}`,
|
||||
R.range(1, MAX_NUMBER_OF_CASSETTES + 1)
|
||||
)
|
||||
|
||||
const RECYCLER_FIELDS = [
|
||||
'recycler1',
|
||||
'recycler2',
|
||||
'recycler3',
|
||||
'recycler4',
|
||||
'recycler5',
|
||||
'recycler6'
|
||||
]
|
||||
|
||||
const canManuallyLoadRecyclers = ({ model }) => ['grandola'].includes(model)
|
||||
|
||||
const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||
const [{ step, config }, setState] = useState({
|
||||
step: 0,
|
||||
config: { active: true }
|
||||
})
|
||||
|
||||
const isCashOutDisabled =
|
||||
R.isEmpty(cashoutSettings) || !cashoutSettings?.active
|
||||
|
||||
const numberOfCassettes = isCashOutDisabled ? 0 : machine.numberOfCassettes
|
||||
const numberOfRecyclers = machine.numberOfRecyclers
|
||||
|
||||
const LAST_STEP = canManuallyLoadRecyclers(machine)
|
||||
? numberOfCassettes + numberOfRecyclers + 1
|
||||
: numberOfCassettes + 1
|
||||
|
||||
const title = `Update counts`
|
||||
const isLastStep = step === LAST_STEP
|
||||
|
||||
const buildCashUnitObj = (fields, cassetteInput) =>
|
||||
R.pipe(R.pickAll(fields), R.map(defaultToZero))(cassetteInput)
|
||||
|
||||
const onContinue = it => {
|
||||
const newConfig = R.merge(config, it)
|
||||
if (isLastStep) {
|
||||
const wasCashboxEmptied = [
|
||||
config?.wasCashboxEmptied,
|
||||
it?.wasCashboxEmptied
|
||||
].includes('YES')
|
||||
|
||||
const cassettes = buildCashUnitObj(CASSETTE_FIELDS, it)
|
||||
const recyclers = canManuallyLoadRecyclers(machine)
|
||||
? buildCashUnitObj(RECYCLER_FIELDS, it)
|
||||
: []
|
||||
|
||||
const cashUnits = {
|
||||
cashbox: wasCashboxEmptied ? 0 : machine?.cashUnits.cashbox,
|
||||
...cassettes,
|
||||
...recyclers
|
||||
}
|
||||
|
||||
save(machine.id, cashUnits)
|
||||
return onClose()
|
||||
}
|
||||
|
||||
setState({
|
||||
step: step + 1,
|
||||
config: newConfig
|
||||
})
|
||||
}
|
||||
|
||||
const makeCassetteSteps = R.pipe(
|
||||
R.add(1),
|
||||
R.range(1),
|
||||
R.map(i => ({
|
||||
type: `cassette ${i}`,
|
||||
schema: Yup.object().shape({
|
||||
[`cassette${i}`]: Yup.number()
|
||||
.label('Bill count')
|
||||
.positive()
|
||||
.integer()
|
||||
.required()
|
||||
.min(0)
|
||||
.max(
|
||||
getCashUnitCapacity(machine.model, 'cassette'),
|
||||
`${
|
||||
modelPrettifier[machine.model]
|
||||
} maximum cassette capacity is ${getCashUnitCapacity(
|
||||
machine.model,
|
||||
'cassette'
|
||||
)} bills`
|
||||
)
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
const makeRecyclerSteps = R.pipe(
|
||||
R.add(1),
|
||||
R.range(1),
|
||||
R.chain(i => ({
|
||||
type: `recycler ${i}`,
|
||||
schema: Yup.object().shape({
|
||||
[`recycler${i}`]: Yup.number()
|
||||
.label('Bill count')
|
||||
.positive()
|
||||
.integer()
|
||||
.required()
|
||||
.min(0)
|
||||
.max(
|
||||
cashUnitCapacity[machine.model].recycler,
|
||||
`${modelPrettifier[machine.model]}
|
||||
maximum recycler capacity is ${
|
||||
cashUnitCapacity[machine.model].recycler
|
||||
} bills`
|
||||
)
|
||||
})
|
||||
}))
|
||||
)
|
||||
|
||||
const makeCassettesInitialValues = () =>
|
||||
!R.isEmpty(cashoutSettings)
|
||||
? R.reduce(
|
||||
(acc, value) => {
|
||||
acc[`cassette${value}`] = ''
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
R.range(1, numberOfCassettes + 1)
|
||||
)
|
||||
: {}
|
||||
|
||||
const makeRecyclersInitialValues = () =>
|
||||
!R.isEmpty(cashoutSettings)
|
||||
? R.reduce(
|
||||
(acc, value) => {
|
||||
acc[`recycler${value * 2 - 1}`] = ''
|
||||
acc[`recycler${value * 2}`] = ''
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
R.range(1, numberOfRecyclers + 1)
|
||||
)
|
||||
: {}
|
||||
|
||||
const makeInitialValues = () =>
|
||||
R.merge(makeCassettesInitialValues(), makeRecyclersInitialValues())
|
||||
|
||||
const steps = R.pipe(
|
||||
R.concat(
|
||||
makeRecyclerSteps(
|
||||
canManuallyLoadRecyclers(machine) ? numberOfRecyclers : 0
|
||||
)
|
||||
),
|
||||
R.concat(makeCassetteSteps(isCashOutDisabled ? 0 : numberOfCassettes)),
|
||||
R.concat([
|
||||
{
|
||||
type: 'cashbox',
|
||||
schema: Yup.object().shape({
|
||||
wasCashboxEmptied: Yup.string().required('Select one option.')
|
||||
}),
|
||||
cashoutRequired: false
|
||||
}
|
||||
])
|
||||
)([])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={step === 0 ? null : title}
|
||||
handleClose={onClose}
|
||||
width={MODAL_WIDTH}
|
||||
height={MODAL_HEIGHT}
|
||||
open={true}>
|
||||
{step === 0 && (
|
||||
<WizardSplash name={machine?.name} onContinue={() => onContinue()} />
|
||||
)}
|
||||
{step !== 0 && (
|
||||
<WizardStep
|
||||
step={step}
|
||||
name={machine?.name}
|
||||
machine={machine}
|
||||
cashoutSettings={cashoutSettings}
|
||||
error={error}
|
||||
lastStep={isLastStep}
|
||||
steps={steps}
|
||||
fiatCurrency={locale.fiatCurrency}
|
||||
onContinue={onContinue}
|
||||
initialValues={makeInitialValues()}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Wizard
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react'
|
||||
import { H1, P, Info2 } from 'src/components/typography'
|
||||
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import filledCassettes from 'src/styling/icons/cassettes/both-filled.svg'
|
||||
|
||||
const WizardSplash = ({ name, onContinue }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center flex-1 pt-0 pb-12 px-8 gap-4">
|
||||
<img width="148" height="196" alt="cassette" src={filledCassettes}></img>
|
||||
<div className="flex flex-col items-center">
|
||||
<H1 noMargin>Update counts</H1>
|
||||
<Info2 noMargin className="text-comet my-1">
|
||||
{name}
|
||||
</Info2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<WarningIcon className="h-6 w-6" />
|
||||
<P noMargin className="flex-1">
|
||||
Before updating counts on Lamassu Admin, make sure you've done it
|
||||
before on the machines.
|
||||
</P>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<WarningIcon className="h-6 w-6" />
|
||||
<P noMargin className="flex-1">
|
||||
For cash cassettes, please make sure you've removed the remaining
|
||||
bills before adding the new ones.
|
||||
</P>
|
||||
</div>
|
||||
<Button className="ml-auto mt-auto mb-0" onClick={onContinue}>
|
||||
Get started
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardSplash
|
||||
273
packages/admin-ui/src/pages/Maintenance/Wizard/WizardStep.jsx
Normal file
273
packages/admin-ui/src/pages/Maintenance/Wizard/WizardStep.jsx
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import classnames from 'classnames'
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Stepper from 'src/components/Stepper'
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import { Cashbox } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import { Info2, H4, P, Info1 } from 'src/components/typography'
|
||||
import TxOutIcon from 'src/styling/icons/direction/cash-out.svg?react'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
||||
import cashbox from 'src/styling/icons/cassettes/acceptor-left.svg'
|
||||
import cassetteOne from 'src/styling/icons/cassettes/dispenser-1.svg'
|
||||
import cassetteTwo from 'src/styling/icons/cassettes/dispenser-2.svg'
|
||||
import tejo3CassetteOne from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-1-left.svg'
|
||||
import tejo3CassetteTwo from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-2-left.svg'
|
||||
import tejo3CassetteThree from 'src/styling/icons/cassettes/tejo/3-cassettes/3-cassettes-open-3-left.svg'
|
||||
import tejo4CassetteOne from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-1-left.svg'
|
||||
import tejo4CassetteTwo from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-2-left.svg'
|
||||
import tejo4CassetteThree from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-3-left.svg'
|
||||
import tejo4CassetteFour from 'src/styling/icons/cassettes/tejo/4-cassettes/4-cassettes-open-4-left.svg'
|
||||
import { getCashUnitCapacity } from 'src/utils/machine'
|
||||
import { numberToFiatAmount } from 'src/utils/number'
|
||||
import { startCase } from 'src/utils/string'
|
||||
import classes from './WizardStep.module.css'
|
||||
|
||||
const CASHBOX_STEP = 1
|
||||
|
||||
const isCashboxStep = step => step === CASHBOX_STEP
|
||||
const isCassetteStep = (step, numberOfCassettes) =>
|
||||
step > 1 && step <= numberOfCassettes + 1
|
||||
const isRecyclerStep = (step, numberOfCassettes, numberOfRecyclers) =>
|
||||
step > numberOfCassettes + 1 &&
|
||||
step <= numberOfCassettes + numberOfRecyclers + 1
|
||||
|
||||
const cassetesArtworks = (step, numberOfCassettes, numberOfRecyclers) => {
|
||||
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||
return isCassetteStep(step, numberOfCassettes)
|
||||
? [
|
||||
[cassetteOne],
|
||||
[cassetteOne, cassetteTwo],
|
||||
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
||||
[
|
||||
tejo4CassetteOne,
|
||||
tejo4CassetteTwo,
|
||||
tejo4CassetteThree,
|
||||
tejo4CassetteFour
|
||||
]
|
||||
][numberOfCassettes - 1][step - cassetteStepsStart]
|
||||
: [
|
||||
/* TODO: Recycler artwork */
|
||||
[cassetteOne],
|
||||
[cassetteOne, cassetteTwo],
|
||||
[tejo3CassetteOne, tejo3CassetteTwo, tejo3CassetteThree],
|
||||
[
|
||||
tejo4CassetteOne,
|
||||
tejo4CassetteTwo,
|
||||
tejo4CassetteThree,
|
||||
tejo4CassetteFour
|
||||
]
|
||||
][numberOfRecyclers - 1][step - cassetteStepsStart]
|
||||
}
|
||||
|
||||
const getCashUnitFieldName = (step, numberOfCassettes, numberOfRecyclers) => {
|
||||
if (isCashboxStep(step)) return { name: 'cashbox', category: 'cashbox' }
|
||||
const cassetteStepsStart = CASHBOX_STEP + 1
|
||||
if (isCassetteStep(step, numberOfCassettes))
|
||||
return {
|
||||
name: `cassette${step - cassetteStepsStart + 1}`,
|
||||
category: 'cassette'
|
||||
}
|
||||
const recyclerStepsStart = CASHBOX_STEP + numberOfCassettes + 1
|
||||
if (isRecyclerStep(step, numberOfCassettes, numberOfRecyclers))
|
||||
return {
|
||||
name: `recycler${Math.ceil(step - recyclerStepsStart + 1)}`,
|
||||
category: 'recycler'
|
||||
}
|
||||
}
|
||||
|
||||
const WizardStep = ({
|
||||
step,
|
||||
name,
|
||||
machine,
|
||||
cashoutSettings,
|
||||
error,
|
||||
lastStep,
|
||||
steps,
|
||||
fiatCurrency,
|
||||
onContinue,
|
||||
initialValues
|
||||
}) => {
|
||||
const label = lastStep ? 'Finish' : 'Confirm'
|
||||
|
||||
const stepOneRadioOptions = [
|
||||
{ display: 'Yes', code: 'YES' },
|
||||
{ display: 'No', code: 'NO' }
|
||||
]
|
||||
|
||||
const numberOfCassettes = machine.numberOfCassettes
|
||||
const numberOfRecyclers = machine.numberOfRecyclers
|
||||
const { name: cashUnitField, category: cashUnitCategory } =
|
||||
getCashUnitFieldName(step, numberOfCassettes, numberOfRecyclers)
|
||||
const originalCashUnitCount = machine?.cashUnits?.[cashUnitField]
|
||||
const cashUnitDenomination = cashoutSettings?.[cashUnitField]
|
||||
|
||||
const cassetteCount = values => values[cashUnitField] || originalCashUnitCount
|
||||
const cassetteTotal = values => cassetteCount(values) * cashUnitDenomination
|
||||
const getPercentage = R.pipe(
|
||||
cassetteCount,
|
||||
count =>
|
||||
100 * (count / getCashUnitCapacity(machine.model, cashUnitCategory)),
|
||||
R.clamp(0, 100)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 pb-6 gap-6">
|
||||
<div className="mb-4">
|
||||
<Info2 noMargin className="mb-3">
|
||||
{name}
|
||||
</Info2>
|
||||
<Stepper steps={steps.length} currentStep={step} />
|
||||
</div>
|
||||
|
||||
{isCashboxStep(step) && (
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
onSubmit={onContinue}
|
||||
initialValues={{ wasCashboxEmptied: '' }}
|
||||
enableReinitialize
|
||||
validationSchema={steps[0].schema}>
|
||||
{({ errors }) => (
|
||||
<Form className="flex flex-col flex-1">
|
||||
<div className="flex flex-row pb-25">
|
||||
<img
|
||||
className={classes.stepImage}
|
||||
alt={cashUnitCategory}
|
||||
src={cashbox}></img>
|
||||
<div className={classes.formWrapper}>
|
||||
<div className={classes.verticalAlign}>
|
||||
<H4 noMargin>Did you empty the cash box?</H4>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="wasCashboxEmptied"
|
||||
options={stepOneRadioOptions}
|
||||
className={classes.horizontalAlign}
|
||||
/>
|
||||
{errors.wasCashboxEmptied && (
|
||||
<div className="text-tomato">
|
||||
{errors.wasCashboxEmptied}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classnames(
|
||||
classes.horizontalAlign,
|
||||
'items-center'
|
||||
)}>
|
||||
<P>Since previous update</P>
|
||||
<HelpTooltip width={215}>
|
||||
<P>
|
||||
Number of bills inside the cash box, since the last
|
||||
cash box changes.
|
||||
</P>
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.horizontalAlign,
|
||||
'items-baseline'
|
||||
)}>
|
||||
<Info1 noMargin className="mr-1">
|
||||
{machine?.cashUnits.cashbox}
|
||||
</Info1>
|
||||
<P noMargin>accepted bills</P>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mt-auto ml-auto" type="submit">
|
||||
{label}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
|
||||
{!isCashboxStep(step) && (
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
onSubmit={onContinue}
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
validationSchema={steps[step - 1].schema}>
|
||||
{({ values, errors }) => (
|
||||
<Form className="flex flex-col flex-1">
|
||||
<div className={classnames(classes.horizontalAlign, 'pb-6')}>
|
||||
<img
|
||||
className={classes.stepImage}
|
||||
alt={cashUnitCategory}
|
||||
src={cassetesArtworks(
|
||||
step,
|
||||
numberOfCassettes,
|
||||
numberOfRecyclers
|
||||
)}></img>
|
||||
<div className={classes.formWrapper}>
|
||||
<div className={classes.verticalAlign}>
|
||||
<div
|
||||
className={classnames(classes.horizontalAlign, 'mb-6')}>
|
||||
<div
|
||||
className={classnames(classes.horizontalAlign, 'mt-4')}>
|
||||
<TxOutIcon />
|
||||
<H4 className="ml-2 mr-6" noMargin>
|
||||
{startCase(cashUnitField)} (
|
||||
{cashUnitCategory === 'cassette'
|
||||
? `dispenser`
|
||||
: cashUnitCategory === 'recycler'
|
||||
? `recycler`
|
||||
: ``}
|
||||
)
|
||||
</H4>
|
||||
</div>
|
||||
<Cashbox
|
||||
className="h-10 w-9"
|
||||
percent={getPercentage(values)}
|
||||
cashOut
|
||||
/>
|
||||
</div>
|
||||
<H4 noMargin>Refill bill count</H4>
|
||||
<div
|
||||
className={classnames(
|
||||
classes.horizontalAlign,
|
||||
'items-baseline'
|
||||
)}>
|
||||
<Field
|
||||
component={NumberInput}
|
||||
decimalPlaces={0}
|
||||
width={50}
|
||||
placeholder={originalCashUnitCount.toString()}
|
||||
name={cashUnitField}
|
||||
className="mr-1"
|
||||
autoFocus
|
||||
/>
|
||||
<P>
|
||||
{cashUnitDenomination} {fiatCurrency} bills loaded
|
||||
</P>
|
||||
</div>
|
||||
<P noMargin className="text-comet">
|
||||
= {numberToFiatAmount(cassetteTotal(values))}{' '}
|
||||
{fiatCurrency}
|
||||
</P>
|
||||
{!R.isEmpty(errors) && (
|
||||
<ErrorMessage className="max-w-68 mt-6">
|
||||
{R.head(R.values(errors))}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="ml-auto mt-auto" type="submit">
|
||||
{label}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WizardStep
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
.stepImage {
|
||||
width: 148px;
|
||||
height: 196px;
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.verticalAlign {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.horizontalAlign {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
141
packages/admin-ui/src/pages/Maintenance/helper.jsx
Normal file
141
packages/admin-ui/src/pages/Maintenance/helper.jsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import IconButton from '@mui/material/IconButton'
|
||||
import SvgIcon from '@mui/material/SvgIcon'
|
||||
import React from 'react'
|
||||
import * as R from 'ramda'
|
||||
import { CashIn, CashOutLite } from 'src/components/inputs/cashbox/Cashbox'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
|
||||
import { fromNamespace } from 'src/utils/config'
|
||||
import { getCashUnitCapacity } from 'src/utils/machine'
|
||||
|
||||
const getElements = (config, bills, setWizard, widths, setMachineId) => {
|
||||
const fillingPercentageSettings = fromNamespace('notifications', config)
|
||||
const locale = fromNamespace('locale')(config)
|
||||
const cashout = fromNamespace('cashOut')(config)
|
||||
const fiatCurrency = locale?.fiatCurrency
|
||||
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'name',
|
||||
header: 'Machine',
|
||||
width: widths.name,
|
||||
view: m => <>{m.name}</>,
|
||||
input: ({ field: { value: name } }) => <>{name}</>
|
||||
},
|
||||
{
|
||||
name: 'cashbox',
|
||||
header: 'Cashbox',
|
||||
width: widths.cashbox,
|
||||
view: m => (
|
||||
<CashIn
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits.cashbox}
|
||||
total={R.sum(R.map(it => it.fiat, bills[m.id ?? m.deviceId] ?? []))}
|
||||
width={25}
|
||||
height={45}
|
||||
omitInnerPercentage
|
||||
/>
|
||||
),
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cassettes',
|
||||
header: 'Cassettes & Recyclers',
|
||||
width: widths.cassettes,
|
||||
view: m => {
|
||||
return (
|
||||
<div className="flex my-2 mx-0 gap-8">
|
||||
<div className="flex gap-2">
|
||||
{R.range(1, m.numberOfCassettes + 1).map((it, idx) => (
|
||||
<CashOutLite
|
||||
key={idx}
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`cassette${it}`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[`cassette${it}`]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[`fillingPercentageCassette${it}`]
|
||||
}
|
||||
capacity={getCashUnitCapacity(m.model, 'cassette')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{R.map(it => (
|
||||
<>
|
||||
<CashOutLite
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`recycler${it * 2 - 1}`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[
|
||||
`recycler${it * 2 - 1}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageRecycler${it * 2 - 1}`
|
||||
]
|
||||
}
|
||||
capacity={getCashUnitCapacity(m.model, 'recycler')}
|
||||
/>
|
||||
<CashOutLite
|
||||
width={'100%'}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={m.cashUnits[`recycler${it * 2}`]}
|
||||
denomination={
|
||||
getCashoutSettings(m.id ?? m.deviceId)[
|
||||
`recycler${it * 2}`
|
||||
]
|
||||
}
|
||||
threshold={
|
||||
fillingPercentageSettings[
|
||||
`fillingPercentageRecycler${it * 2}`
|
||||
]
|
||||
}
|
||||
capacity={getCashUnitCapacity(m.model, 'recycler')}
|
||||
/>
|
||||
{it !== m.numberOfRecyclers / 2 && (
|
||||
<span className="h-full w-[1px] bg-comet2" />
|
||||
)}
|
||||
</>
|
||||
))(R.range(1, m.numberOfRecyclers / 2 + 1))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: widths.edit,
|
||||
textAlign: 'center',
|
||||
view: m => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
!R.isNil(setMachineId) && setMachineId(m.id ?? m.deviceId)
|
||||
setWizard(true)
|
||||
}}>
|
||||
<SvgIcon>
|
||||
<EditIcon />
|
||||
</SvgIcon>
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
export default { getElements }
|
||||
Loading…
Add table
Add a link
Reference in a new issue