Add payment management features to Finance dashboard, including AuthorisePayment, DeclinePayment, and CancelPayment components for handling payment actions. Update PaymentInfo to integrate new modals for these actions and enhance Payment model to support new states and attributes, improving overall payment processing functionality.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

This commit is contained in:
Tom Butcher 2026-06-21 22:32:51 +01:00
parent 0406c0d0e0
commit 457c427928
7 changed files with 293 additions and 5 deletions

View File

@ -0,0 +1,46 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const AuthorisePayment = ({ onOk, objectData }) => {
const [authoriseLoading, setAuthoriseLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleAuthorise = async () => {
setAuthoriseLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'authorise'
)
if (result) {
message.success('Payment authorised successfully')
onOk(result)
}
} catch (error) {
console.error('Error authorising payment:', error)
} finally {
setAuthoriseLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to authorise this payment?'}
description={`Authorising payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to authorised.`}
onOk={handleAuthorise}
okText='Authorise'
okLoading={authoriseLoading}
/>
)
}
AuthorisePayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default AuthorisePayment

View File

@ -0,0 +1,46 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const CancelPayment = ({ onOk, objectData }) => {
const [cancelLoading, setCancelLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleCancel = async () => {
setCancelLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'cancel'
)
if (result) {
message.success('Payment cancelled successfully')
onOk(result)
}
} catch (error) {
console.error('Error cancelling payment:', error)
} finally {
setCancelLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to cancel this payment?'}
description={`Cancelling payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to cancelled.`}
onOk={handleCancel}
okText='Cancel'
okLoading={cancelLoading}
/>
)
}
CancelPayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default CancelPayment

View File

@ -0,0 +1,46 @@
import { useState, useContext } from 'react'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../../context/ApiServerContext'
import { message } from 'antd'
import MessageDialogView from '../../common/MessageDialogView.jsx'
const DeclinePayment = ({ onOk, objectData }) => {
const [declineLoading, setDeclineLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleDecline = async () => {
setDeclineLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Payment',
'decline'
)
if (result) {
message.success('Payment declined successfully')
onOk(result)
}
} catch (error) {
console.error('Error declining payment:', error)
} finally {
setDeclineLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to decline this payment?'}
description={`Declining payment ${objectData?.name || objectData?._reference || objectData?._id} will update its status to declined.`}
onOk={handleDecline}
okText='Decline'
okLoading={declineLoading}
/>
)
}
DeclinePayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default DeclinePayment

View File

@ -53,6 +53,8 @@ const NewPayment = ({ onOk, reset, defaultValues }) => {
updatedAt: false, updatedAt: false,
_reference: false, _reference: false,
postedAt: false, postedAt: false,
authorisedAt: false,
declinedAt: false,
cancelledAt: false cancelledAt: false
}} }}
isEditing={false} isEditing={false}

View File

@ -24,6 +24,9 @@ import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import ScrollBox from '../../common/ScrollBox.jsx' import ScrollBox from '../../common/ScrollBox.jsx'
import { getModelByName } from '../../../../database/ObjectModels.js' import { getModelByName } from '../../../../database/ObjectModels.js'
import PostPayment from './PostPayment.jsx' import PostPayment from './PostPayment.jsx'
import AuthorisePayment from './AuthorisePayment.jsx'
import DeclinePayment from './DeclinePayment.jsx'
import CancelPayment from './CancelPayment.jsx'
const log = loglevel.getLogger('PaymentInfo') const log = loglevel.getLogger('PaymentInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -48,6 +51,9 @@ const PaymentInfo = () => {
objectData: {} objectData: {}
}) })
const [postPaymentOpen, setPostPaymentOpen] = useState(false) const [postPaymentOpen, setPostPaymentOpen] = useState(false)
const [authorisePaymentOpen, setAuthorisePaymentOpen] = useState(false)
const [declinePaymentOpen, setDeclinePaymentOpen] = useState(false)
const [cancelPaymentOpen, setCancelPaymentOpen] = useState(false)
const actions = { const actions = {
edit: () => { edit: () => {
@ -69,6 +75,18 @@ const PaymentInfo = () => {
post: () => { post: () => {
setPostPaymentOpen(true) setPostPaymentOpen(true)
return true return true
},
authorise: () => {
setAuthorisePaymentOpen(true)
return true
},
decline: () => {
setDeclinePaymentOpen(true)
return true
},
cancel: () => {
setCancelPaymentOpen(true)
return true
} }
} }
@ -234,6 +252,60 @@ const PaymentInfo = () => {
objectData={objectFormState.objectData} objectData={objectFormState.objectData}
/> />
</Modal> </Modal>
<Modal
open={authorisePaymentOpen}
onCancel={() => {
setAuthorisePaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AuthorisePayment
onOk={() => {
setAuthorisePaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={declinePaymentOpen}
onCancel={() => {
setDeclinePaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<DeclinePayment
onOk={() => {
setDeclinePaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={cancelPaymentOpen}
onCancel={() => {
setCancelPaymentOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<CancelPayment
onOk={() => {
setCancelPaymentOpen(false)
objectFormRef?.current.handleFetchObject()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</> </>
) )
} }

View File

@ -140,6 +140,14 @@ const StateTag = ({ state, showBadge = true, style = {} }) => {
status = 'magenta' status = 'magenta'
text = 'Posted' text = 'Posted'
break break
case 'authorised':
status = 'success'
text = 'Authorised'
break
case 'declined':
status = 'error'
text = 'Declined'
break
case 'received': case 'received':
status = 'success' status = 'success'
text = 'Received' text = 'Received'

View File

@ -82,6 +82,35 @@ export const Payment = {
return objectData?.state?.type == 'draft' return objectData?.state?.type == 'draft'
} }
}, },
{
name: 'authorise',
label: 'Authorise',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=authorise`,
disabled: (objectData) => {
return objectData?.state?.type != 'posted'
},
visible: (objectData) => {
return objectData?.state?.type == 'posted'
}
},
{
name: 'decline',
label: 'Decline',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=decline`,
disabled: (objectData) => {
return objectData?.state?.type != 'posted'
},
visible: (objectData) => {
return objectData?.state?.type == 'posted'
}
},
{ {
name: 'cancel', name: 'cancel',
label: 'Cancel', label: 'Cancel',
@ -91,13 +120,10 @@ export const Payment = {
url: (_id) => url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=cancel`, `/dashboard/finance/payments/info?paymentId=${_id}&action=cancel`,
disabled: (objectData) => { disabled: (objectData) => {
return objectData?.state?.type == 'cancelled' return objectData?.state?.type != 'posted'
}, },
visible: (objectData) => { visible: (objectData) => {
return ( return objectData?.state?.type == 'posted'
objectData?.state?.type == 'draft' ||
objectData?.state?.type == 'posted'
)
} }
} }
], ],
@ -197,6 +223,20 @@ export const Payment = {
readOnly: true, readOnly: true,
columnWidth: 175 columnWidth: 175
}, },
{
name: 'authorisedAt',
label: 'Authorised At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'declinedAt',
label: 'Declined At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{ {
name: 'cancelledAt', name: 'cancelledAt',
label: 'Cancelled At', label: 'Cancelled At',
@ -256,6 +296,34 @@ export const Payment = {
prefix: '£', prefix: '£',
roundNumber: 2, roundNumber: 2,
color: 'cyan' color: 'cyan'
},
{
name: 'authorised.authorisedCount.count',
label: 'Authorised',
type: 'number',
color: 'green'
},
{
name: 'authorised.authorisedAmount.sum',
label: 'Authorised Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'green'
},
{
name: 'declined.declinedCount.count',
label: 'Declined',
type: 'number',
color: 'red'
},
{
name: 'declined.declinedAmount.sum',
label: 'Declined Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'red'
} }
] ]
} }