feat: cashbox history tab

This commit is contained in:
André Sá 2021-11-12 16:12:52 +00:00
parent 683778cb7b
commit dde26b8dc2
4 changed files with 326 additions and 17 deletions

View file

@ -5,6 +5,7 @@ const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
const got = require('got') const got = require('got')
const DataLoader = require('dataloader') const DataLoader = require('dataloader')
const cashbox = require('../../cashbox-batches')
const machineLoader = require('../../machine-loader') const machineLoader = require('../../machine-loader')
const customers = require('../../customers') const customers = require('../../customers')
const { machineAction } = require('../machines') const { machineAction } = require('../machines')
@ -280,7 +281,18 @@ const typeDefs = gql`
cashbox: Int cashbox: Int
} }
type CashboxBatch {
id: ID
deviceId: ID
created: Date
operationType: String
customBillCount: Int
performedBy: String
bills: [Bills]
}
type Query { type Query {
cashboxBatches: [CashboxBatch]
countries: [Country] countries: [Country]
currencies: [Currency] currencies: [Currency]
languages: [Language] languages: [Language]
@ -352,6 +364,7 @@ const typeDefs = gql`
toggleClearNotification(id: ID!, read: Boolean!): Notification toggleClearNotification(id: ID!, read: Boolean!): Notification
clearAllNotifications: Notification clearAllNotifications: Notification
cancelCashOutTransaction(id: ID): Transaction cancelCashOutTransaction(id: ID): Transaction
editBatch(id: ID, performedBy: String): CashboxBatch
} }
` `
@ -378,6 +391,7 @@ const resolvers = {
latestEvent: parent => machineEventsLoader.load(parent.deviceId) latestEvent: parent => machineEventsLoader.load(parent.deviceId)
}, },
Query: { Query: {
cashboxBatches: () => cashbox.getBatches(),
countries: () => countries, countries: () => countries,
currencies: () => currencies, currencies: () => currencies,
languages: () => languages, languages: () => languages,
@ -449,7 +463,8 @@ const resolvers = {
deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId), deletePromoCode: (...[, { codeId }]) => promoCodeManager.deletePromoCode(codeId),
toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read), toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read),
clearAllNotifications: () => notifierQueries.markAllAsRead(), clearAllNotifications: () => notifierQueries.markAllAsRead(),
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id) cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id),
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
} }
} }

View file

@ -0,0 +1,21 @@
var db = require('./db')
exports.up = function (next) {
var sqls = [
`CREATE TYPE cashbox_batch_type AS ENUM(
'cash-in-empty',
'cash-out-1-refill',
'cash-out-1-empty',
'cash-out-2-refill',
'cash-out-2-empty'
)`,
`ALTER TABLE cashbox_batches ADD COLUMN operation_type cashbox_batch_type NOT NULL`,
`ALTER TABLE cashbox_batches ADD COLUMN bill_count_override SMALLINT`,
`ALTER TABLE cashbox_batches ADD COLUMN performed_by VARCHAR(64)`
]
db.multi(sqls, next)
}
exports.down = function (next) {
next()
}

View file

@ -2,7 +2,7 @@ import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React, { useState } from 'react'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Table as EditableTable } from 'src/components/editableTable' import { Table as EditableTable } from 'src/components/editableTable'
@ -10,10 +10,13 @@ import { CashOut, CashIn } from 'src/components/inputs/cashbox/Cashbox'
import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik' import { NumberInput, CashCassetteInput } from 'src/components/inputs/formik'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { EmptyTable } from 'src/components/table' import { EmptyTable } from 'src/components/table'
import { ReactComponent as ReverseHistoryIcon } from 'src/styling/icons/circle buttons/history/white.svg'
import { ReactComponent as HistoryIcon } from 'src/styling/icons/circle buttons/history/zodiac.svg'
import { fromNamespace } from 'src/utils/config' import { fromNamespace } from 'src/utils/config'
import styles from './CashCassettes.styles.js' import styles from './CashCassettes.styles.js'
import CashCassettesFooter from './CashCassettesFooter' import CashCassettesFooter from './CashCassettesFooter'
import CashboxHistory from './CashboxHistory'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -108,6 +111,7 @@ const SET_CASSETTE_BILLS = gql`
const CashCassettes = () => { const CashCassettes = () => {
const classes = useStyles() const classes = useStyles()
const [showHistory, setShowHistory] = useState(false)
const { data } = useQuery(GET_MACHINES_AND_CONFIG) const { data } = useQuery(GET_MACHINES_AND_CONFIG)
@ -200,8 +204,19 @@ const CashCassettes = () => {
return ( return (
<> <>
<TitleSection title="Cash Cassettes" /> <TitleSection
title="Cash Cassettes"
button={{
text: 'Cashbox history',
icon: HistoryIcon,
inverseIcon: ReverseHistoryIcon,
toggle: setShowHistory
}}
iconClassName={classes.listViewButton}
/>
<div className={classes.tableContainer}> <div className={classes.tableContainer}>
{!showHistory && (
<>
<EditableTable <EditableTable
error={error?.message} error={error?.message}
name="cashboxes" name="cashboxes"
@ -218,6 +233,11 @@ const CashCassettes = () => {
{data && R.isEmpty(machines) && ( {data && R.isEmpty(machines) && (
<EmptyTable message="No machines so far" /> <EmptyTable message="No machines so far" />
)} )}
</>
)}
{showHistory && (
<CashboxHistory machines={machines} currency={fiatCurrency} />
)}
</div> </div>
<CashCassettesFooter <CashCassettesFooter
currencyCode={fiatCurrency} currencyCode={fiatCurrency}

View file

@ -0,0 +1,253 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag'
import moment from 'moment'
import * as R from 'ramda'
import React, { useState } from 'react'
import * as Yup from 'yup'
import { Link, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs'
import { NumberInput } from 'src/components/inputs/formik'
import DataTable from 'src/components/tables/DataTable'
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
const GET_BATCHES = gql`
query cashboxBatches {
cashboxBatches {
id
deviceId
created
operationType
customBillCount
performedBy
bills {
fiat
deviceId
created
cashbox
}
}
}
`
const EDIT_BATCH = gql`
mutation editBatch($id: ID, $performedBy: String) {
editBatch(id: $id, performedBy: $performedBy) {
id
}
}
`
const styles = {
operationType: {
marginLeft: 8
},
operationTypeWrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
saveAndCancel: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
}
}
const schema = Yup.object().shape({
performedBy: Yup.string().nullable()
})
const useStyles = makeStyles(styles)
const CashboxHistory = ({ machines, currency }) => {
const classes = useStyles()
const [editing, setEditing] = useState(false)
const [error, setError] = useState(false)
const [fields, setFields] = useState({})
const { data, loading } = useQuery(GET_BATCHES)
const [editBatch] = useMutation(EDIT_BATCH, {
refetchQueries: () => ['cashboxBatches']
})
const batches = R.path(['cashboxBatches'])(data)
const getOperationRender = {
'cash-in-empty': (
<>
<TxInIcon />
<span className={classes.operationType}>Cash-in emptied</span>
</>
),
'cash-out-1-refill': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 1 refill</span>
</>
),
'cash-out-1-empty': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 1 emptied</span>
</>
),
'cash-out-2-refill': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 2 refill</span>
</>
),
'cash-out-2-empty': (
<>
<TxOutIcon />
<span className={classes.operationType}>Cash-out 2 emptied</span>
</>
)
}
const save = row => {
schema
.isValid(fields)
.then(() => {
setError(false)
editBatch({
variables: { id: row.id, performedBy: fields?.performedBy }
})
})
.catch(setError(true))
return close()
}
const close = () => {
setFields({})
return setEditing(false)
}
const elements = [
{
name: 'operation',
header: 'Operation',
width: 200,
textAlign: 'left',
view: it => (
<div className={classes.operationTypeWrapper}>
{getOperationRender[it.operationType]}
</div>
)
},
{
name: 'machine',
header: 'Machine',
width: 200,
textAlign: 'left',
view: it => {
return R.find(R.propEq('id', it.deviceId))(machines).name
}
},
{
name: 'billCount',
header: 'Bill Count',
width: 115,
textAlign: 'left',
input: NumberInput,
inputProps: {
decimalPlaces: 0
},
view: it =>
R.isNil(it.customBillCount) ? it.bills.length : it.customBillCount
},
{
name: 'total',
header: 'Total',
width: 100,
textAlign: 'right',
view: it => (
<span>
{R.sum(R.map(b => R.prop('fiat', b), it.bills))} {currency}
</span>
)
},
{
name: 'date',
header: 'Date',
width: 135,
textAlign: 'right',
view: it => moment.utc(it.created).format('YYYY-MM-DD')
},
{
name: 'time',
header: 'Time (h:m)',
width: 125,
textAlign: 'right',
view: it => moment.utc(it.created).format('HH:mm')
},
{
name: 'performedBy',
header: 'Performed by',
width: 180,
textAlign: 'left',
view: it => {
if (!editing)
return R.isNil(it.performedBy) ? 'Unknown entity' : it.performedBy
return (
<TextInput
onChange={e =>
setFields({ ...fields, performedBy: e.target.value })
}
error={error}
width={190 * 0.85}
value={fields.performedBy ?? ''}
/>
)
}
},
{
name: '',
header: 'Edit',
width: 150,
textAlign: 'right',
view: it => {
if (!editing)
return (
<IconButton
onClick={() => {
setFields({})
setEditing(true)
}}>
<EditIcon />
</IconButton>
)
return (
<div className={classes.saveAndCancel}>
<Link type="submit" color="primary" onClick={() => save(it)}>
Save
</Link>
<Link color="secondary" onClick={close}>
Cancel
</Link>
</div>
)
}
}
]
return (
<>
{!loading && (
<DataTable
name="cashboxHistory"
elements={elements}
data={batches}
emptyText="No cashbox batches so far"
/>
)}
</>
)
}
export default CashboxHistory