Add Payment Management Features

- Introduced Payment model and associated components for managing payments within the finance dashboard.
- Added PaymentIcon for visual representation in the sidebar and other components.
- Implemented Payments overview, including statistics and a new modal for creating payments.
- Enhanced InvoiceInfo component to include payment details and actions for acknowledging and posting payments.
- Updated database models to integrate payment functionalities, ensuring comprehensive financial tracking.
This commit is contained in:
Tom Butcher 2025-12-28 02:09:01 +00:00
parent 455c223ec0
commit 6fc952be4d
12 changed files with 893 additions and 4 deletions

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.81149,0,0,0.81149,2,10.044134)">
<path d="M14.864,42.322L22.52,42.322C24.364,42.322 25.582,41.078 25.582,39.328L25.582,33.547C25.582,31.766 24.364,30.547 22.52,30.547L14.864,30.547C13.02,30.547 11.801,31.766 11.801,33.547L11.801,39.328C11.801,41.078 13.02,42.322 14.864,42.322ZM3.51,20.305L70.459,20.305L70.459,12.998L3.51,12.998L3.51,20.305ZM10.251,54.093L63.687,54.093C70.463,54.093 73.938,50.638 73.938,43.969L73.938,10.169C73.938,3.501 70.463,0.02 63.687,0.02L10.251,0.02C3.506,0.02 0,3.501 0,10.169L0,43.969C0,50.638 3.506,54.093 10.251,54.093ZM10.606,47.97C7.701,47.97 6.122,46.471 6.122,43.429L6.122,10.709C6.122,7.667 7.701,6.142 10.606,6.142L63.332,6.142C66.186,6.142 67.816,7.667 67.816,10.709L67.816,43.429C67.816,46.471 66.186,47.97 63.332,47.97L10.606,47.97Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -13,7 +13,8 @@ const FinanceOverview = () => {
const [collapseState, updateCollapseState] = useCollapseState( const [collapseState, updateCollapseState] = useCollapseState(
'FinanceOverview', 'FinanceOverview',
{ {
invoiceStats: true invoiceStats: true,
paymentStats: true
} }
) )
@ -41,6 +42,7 @@ const FinanceOverview = () => {
} }
className='no-t-padding-collapse' className='no-t-padding-collapse'
collapseKey='invoiceStats' collapseKey='invoiceStats'
canCollapse={false}
> >
<Flex <Flex
justify='flex-start' justify='flex-start'
@ -51,6 +53,26 @@ const FinanceOverview = () => {
<StatsDisplay objectType='invoice' /> <StatsDisplay objectType='invoice' />
</Flex> </Flex>
</InfoCollapse> </InfoCollapse>
<InfoCollapse
title='Payment Statistics'
icon={null}
active={collapseState.paymentStats}
onToggle={(isActive) =>
updateCollapseState('paymentStats', isActive)
}
className='no-t-padding-collapse'
collapseKey='paymentStats'
canCollapse={false}
>
<Flex
justify='flex-start'
gap='middle'
wrap='wrap'
align='flex-start'
>
<StatsDisplay objectType='payment' />
</Flex>
</InfoCollapse>
</Flex> </Flex>
</ScrollBox> </ScrollBox>
</Flex> </Flex>
@ -58,4 +80,3 @@ const FinanceOverview = () => {
} }
export default FinanceOverview export default FinanceOverview

View File

@ -1,6 +1,7 @@
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import DashboardSidebar from '../common/DashboardSidebar' import DashboardSidebar from '../common/DashboardSidebar'
import InvoiceIcon from '../../Icons/InvoiceIcon' import InvoiceIcon from '../../Icons/InvoiceIcon'
import PaymentIcon from '../../Icons/PaymentIcon'
import FinanceIcon from '../../Icons/FinanceIcon' import FinanceIcon from '../../Icons/FinanceIcon'
const items = [ const items = [
@ -16,12 +17,19 @@ const items = [
label: 'Invoices', label: 'Invoices',
icon: <InvoiceIcon />, icon: <InvoiceIcon />,
path: '/dashboard/finance/invoices' path: '/dashboard/finance/invoices'
},
{
key: 'payments',
label: 'Payments',
icon: <PaymentIcon />,
path: '/dashboard/finance/payments'
} }
] ]
const routeKeyMap = { const routeKeyMap = {
'/dashboard/finance/overview': 'overview', '/dashboard/finance/overview': 'overview',
'/dashboard/finance/invoices': 'invoices' '/dashboard/finance/invoices': 'invoices',
'/dashboard/finance/payments': 'payments'
} }
const FinanceSidebar = (props) => { const FinanceSidebar = (props) => {
@ -43,4 +51,3 @@ const FinanceSidebar = (props) => {
} }
export default FinanceSidebar export default FinanceSidebar

View File

@ -0,0 +1,47 @@
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 AcknowledgeInvoice = ({ onOk, objectData }) => {
const [acknowledgeLoading, setAcknowledgeLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handleAcknowledge = async () => {
setAcknowledgeLoading(true)
try {
const result = await sendObjectFunction(
objectData._id,
'Invoice',
'acknowledge'
)
if (result) {
message.success('Invoice acknowledged successfully')
onOk(result)
}
} catch (error) {
console.error('Error acknowledging invoice:', error)
} finally {
setAcknowledgeLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to acknowledge this invoice?'}
description={`Acknowledging invoice ${objectData?.name || objectData?._reference || objectData?._id} will update its status to acknowledged.`}
onOk={handleAcknowledge}
okText='Acknowledge'
okLoading={acknowledgeLoading}
/>
)
}
AcknowledgeInvoice.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default AcknowledgeInvoice

View File

@ -29,6 +29,8 @@ import {
import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx' import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx' import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
import PostInvoice from './PostInvoice.jsx' import PostInvoice from './PostInvoice.jsx'
import AcknowledgeInvoice from './AcknowledgeInvoice.jsx'
import NewPayment from '../Payments/NewPayment.jsx'
const log = loglevel.getLogger('InvoiceInfo') const log = loglevel.getLogger('InvoiceInfo')
log.setLevel(config.logLevel) log.setLevel(config.logLevel)
@ -42,6 +44,7 @@ const InvoiceInfo = () => {
info: true, info: true,
invoiceOrderItems: true, invoiceOrderItems: true,
invoiceShipments: true, invoiceShipments: true,
payments: true,
notes: true, notes: true,
auditLogs: true auditLogs: true
}) })
@ -55,6 +58,8 @@ const InvoiceInfo = () => {
objectData: {} objectData: {}
}) })
const [postInvoiceOpen, setPostInvoiceOpen] = useState(false) const [postInvoiceOpen, setPostInvoiceOpen] = useState(false)
const [acknowledgeInvoiceOpen, setAcknowledgeInvoiceOpen] = useState(false)
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
const actions = { const actions = {
reload: () => { reload: () => {
@ -80,6 +85,14 @@ const InvoiceInfo = () => {
post: () => { post: () => {
setPostInvoiceOpen(true) setPostInvoiceOpen(true)
return true return true
},
acknowledge: () => {
setAcknowledgeInvoiceOpen(true)
return true
},
newPayment: () => {
setNewPaymentOpen(true)
return true
} }
} }
@ -113,6 +126,7 @@ const InvoiceInfo = () => {
{ key: 'info', label: 'Invoice Information' }, { key: 'info', label: 'Invoice Information' },
{ key: 'invoiceOrderItems', label: 'Invoice Order Items' }, { key: 'invoiceOrderItems', label: 'Invoice Order Items' },
{ key: 'invoiceShipments', label: 'Invoice Shipments' }, { key: 'invoiceShipments', label: 'Invoice Shipments' },
{ key: 'payments', label: 'Payments' },
{ key: 'notes', label: 'Notes' }, { key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' } { key: 'auditLogs', label: 'Audit Logs' }
]} ]}
@ -205,6 +219,7 @@ const InvoiceInfo = () => {
isEditing={isEditing} isEditing={isEditing}
objectData={objectData} objectData={objectData}
loading={loading} loading={loading}
size='medium'
/> />
</InfoCollapse> </InfoCollapse>
<InfoCollapse <InfoCollapse
@ -221,12 +236,30 @@ const InvoiceInfo = () => {
isEditing={isEditing} isEditing={isEditing}
objectData={objectData} objectData={objectData}
loading={loading} loading={loading}
size='medium'
/> />
</InfoCollapse> </InfoCollapse>
</Flex> </Flex>
)} )}
</ObjectForm> </ObjectForm>
</ActionHandler> </ActionHandler>
<InfoCollapse
title='Payments'
icon={<PaymentIcon />}
active={collapseState.payments}
onToggle={(expanded) => updateCollapseState('payments', expanded)}
collapseKey='payments'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='payment'
masterFilter={{ 'invoice._id': invoiceId }}
visibleColumns={{ invoice: false }}
/>
)}
</InfoCollapse>
<InfoCollapse <InfoCollapse
title='Notes' title='Notes'
icon={<NoteIcon />} icon={<NoteIcon />}
@ -278,6 +311,46 @@ const InvoiceInfo = () => {
objectData={objectFormState.objectData} objectData={objectFormState.objectData}
/> />
</Modal> </Modal>
<Modal
open={acknowledgeInvoiceOpen}
onCancel={() => {
setAcknowledgeInvoiceOpen(false)
}}
width={515}
footer={null}
destroyOnHidden={true}
centered={true}
>
<AcknowledgeInvoice
onOk={() => {
setAcknowledgeInvoiceOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
<Modal
open={newPaymentOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewPaymentOpen(false)
}}
destroyOnHidden={true}
>
<NewPayment
onOk={() => {
setNewPaymentOpen(false)
actions.reload()
}}
reset={newPaymentOpen}
defaultValues={{
invoice: { ...objectFormState.objectData },
amount: objectFormState.objectData?.grandTotalAmount
}}
/>
</Modal>
</> </>
) )
} }

View File

@ -0,0 +1,99 @@
import { useState, useRef } from 'react'
import { Button, Flex, Space, Dropdown, Modal } from 'antd'
import NewPayment from './Payments/NewPayment'
import ObjectTable from '../common/ObjectTable'
import PlusIcon from '../../Icons/PlusIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import useColumnVisibility from '../hooks/useColumnVisibility'
import GridIcon from '../../Icons/GridIcon'
import ListIcon from '../../Icons/ListIcon'
import useViewMode from '../hooks/useViewMode'
import ColumnViewButton from '../common/ColumnViewButton'
const Payments = () => {
const [newPaymentOpen, setNewPaymentOpen] = useState(false)
const tableRef = useRef()
const [viewMode, setViewMode] = useViewMode('payments')
const [columnVisibility, setColumnVisibility] =
useColumnVisibility('payments')
const actionItems = {
items: [
{
label: 'New Payment',
key: 'newPayment',
icon: <PlusIcon />
},
{ type: 'divider' },
{
label: 'Reload List',
key: 'reloadList',
icon: <ReloadIcon />
}
],
onClick: ({ key }) => {
if (key === 'reloadList') {
tableRef.current?.reload()
} else if (key === 'newPayment') {
setNewPaymentOpen(true)
}
}
}
return (
<>
<Flex vertical={'true'} gap='large'>
<Flex justify={'space-between'}>
<Space size='small'>
<Dropdown menu={actionItems}>
<Button>Actions</Button>
</Dropdown>
<ColumnViewButton
type='payment'
loading={false}
visibleState={columnVisibility}
updateVisibleState={setColumnVisibility}
/>
</Space>
<Space>
<Button
icon={viewMode === 'cards' ? <ListIcon /> : <GridIcon />}
onClick={() =>
setViewMode(viewMode === 'cards' ? 'list' : 'cards')
}
/>
</Space>
</Flex>
<ObjectTable
ref={tableRef}
visibleColumns={columnVisibility}
type='payment'
cards={viewMode === 'cards'}
/>
</Flex>
<Modal
open={newPaymentOpen}
styles={{ content: { paddingBottom: '24px' } }}
footer={null}
width={800}
onCancel={() => {
setNewPaymentOpen(false)
}}
destroyOnHidden={true}
>
<NewPayment
onOk={() => {
setNewPaymentOpen(false)
tableRef.current?.reload()
}}
reset={newPaymentOpen}
/>
</Modal>
</>
)
}
export default Payments

View File

@ -0,0 +1,89 @@
import PropTypes from 'prop-types'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
const NewPayment = ({ onOk, reset, defaultValues }) => {
return (
<NewObjectForm
type={'payment'}
reset={reset}
defaultValues={{
state: { type: 'draft' },
paymentDate: new Date(),
amount: 0,
...defaultValues
}}
>
{({ handleSubmit, submitLoading, objectData, formValid }) => {
const steps = [
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='payment'
column={1}
bordered={false}
isEditing={true}
required={true}
objectData={objectData}
visibleProperties={{
invoice: true,
vendor: true,
client: true,
paymentDate: true,
amount: true,
paymentMethod: true
}}
/>
)
},
{
title: 'Summary',
key: 'summary',
content: (
<ObjectInfo
type='payment'
column={1}
bordered={false}
visibleProperties={{
_id: false,
createdAt: false,
updatedAt: false,
_reference: false,
postedAt: false,
cancelledAt: false
}}
isEditing={false}
objectData={objectData}
/>
)
}
]
return (
<WizardView
steps={steps}
loading={submitLoading}
formValid={formValid}
title='New Payment'
onSubmit={async () => {
const result = await handleSubmit()
if (result) {
onOk()
}
}}
/>
)
}}
</NewObjectForm>
)
}
NewPayment.propTypes = {
onOk: PropTypes.func.isRequired,
reset: PropTypes.bool,
defaultValues: PropTypes.object
}
export default NewPayment

View File

@ -0,0 +1,242 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
import ObjectForm from '../../common/ObjectForm.jsx'
import EditButtons from '../../common/EditButtons.jsx'
import LockIndicator from '../../common/LockIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
import {
getModelByName
} from '../../../../database/ObjectModels.js'
import PostPayment from './PostPayment.jsx'
const log = loglevel.getLogger('PaymentInfo')
log.setLevel(config.logLevel)
const PaymentInfo = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const paymentId = new URLSearchParams(location.search).get('paymentId')
const [collapseState, updateCollapseState] = useCollapseState('PaymentInfo', {
info: true,
notes: true,
auditLogs: true
})
const [objectFormState, setEditFormState] = useState({
isEditing: false,
editLoading: false,
formValid: false,
lock: null,
loading: false,
objectData: {}
})
const [postPaymentOpen, setPostPaymentOpen] = useState(false)
const actions = {
reload: () => {
objectFormRef?.current?.handleFetchObject?.()
return true
},
edit: () => {
objectFormRef?.current?.startEditing?.()
return false
},
cancelEdit: () => {
objectFormRef?.current?.cancelEditing?.()
return true
},
finishEdit: () => {
objectFormRef?.current?.handleUpdate?.()
return true
},
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
},
post: () => {
setPostPaymentOpen(true)
return true
}
}
const editDisabled =
getModelByName('payment')
?.actions?.find((action) => action.name === 'edit')
?.disabled(objectFormState.objectData) ?? false
return (
<>
<Flex
gap='large'
vertical='true'
style={{
maxHeight: '100%',
minHeight: 0
}}
>
<Flex justify={'space-between'}>
<Space size='middle'>
<Space size='small'>
<ObjectActions
type='payment'
id={paymentId}
disabled={objectFormState.loading}
objectData={objectFormState.objectData}
/>
<ViewButton
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Payment Information' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
visibleState={collapseState}
updateVisibleState={updateCollapseState}
/>
<DocumentPrintButton
type='payment'
objectData={objectFormState.objectData}
disabled={objectFormState.loading}
/>
</Space>
<LockIndicator lock={objectFormState.lock} />
</Space>
<Space>
<EditButtons
isEditing={objectFormState.isEditing}
handleUpdate={() => {
actionHandlerRef.current.callAction('finishEdit')
}}
cancelEditing={() => {
actionHandlerRef.current.callAction('cancelEdit')
}}
startEditing={() => {
actionHandlerRef.current.callAction('edit')
}}
editLoading={objectFormState.editLoading}
formValid={objectFormState.formValid}
disabled={
objectFormState.lock?.locked ||
objectFormState.loading ||
editDisabled
}
loading={objectFormState.editLoading}
/>
</Space>
</Flex>
<ScrollBox>
<Flex vertical gap={'large'}>
<ActionHandler
actions={actions}
loading={objectFormState.loading}
ref={actionHandlerRef}
>
<ObjectForm
id={paymentId}
type='payment'
style={{ height: '100%' }}
ref={objectFormRef}
onStateChange={(state) => {
setEditFormState((prev) => ({ ...prev, ...state }))
}}
>
{({ loading, isEditing, objectData }) => (
<Flex vertical gap={'large'}>
<InfoCollapse
title='Payment Information'
icon={<InfoCircleIcon />}
active={collapseState.info}
onToggle={(expanded) =>
updateCollapseState('info', expanded)
}
collapseKey='info'
>
<ObjectInfo
loading={loading}
indicator={<LoadingOutlined />}
isEditing={isEditing}
type='payment'
labelWidth='225px'
objectData={objectData}
/>
</InfoCollapse>
</Flex>
)}
</ObjectForm>
</ActionHandler>
<InfoCollapse
title='Notes'
icon={<NoteIcon />}
active={collapseState.notes}
onToggle={(expanded) => updateCollapseState('notes', expanded)}
collapseKey='notes'
>
<Card>
<NotesPanel _id={paymentId} type='payment' />
</Card>
</InfoCollapse>
<InfoCollapse
title='Audit Logs'
icon={<AuditLogIcon />}
active={collapseState.auditLogs}
onToggle={(expanded) =>
updateCollapseState('auditLogs', expanded)
}
collapseKey='auditLogs'
>
{objectFormState.loading ? (
<InfoCollapsePlaceholder />
) : (
<ObjectTable
type='auditLog'
masterFilter={{ 'parent._id': paymentId }}
visibleColumns={{ _id: false, 'parent._id': false }}
/>
)}
</InfoCollapse>
</Flex>
</ScrollBox>
</Flex>
<Modal
open={postPaymentOpen}
onCancel={() => {
setPostPaymentOpen(false)
}}
width={500}
footer={null}
destroyOnHidden={true}
centered={true}
>
<PostPayment
onOk={() => {
setPostPaymentOpen(false)
actions.reload()
}}
objectData={objectFormState.objectData}
/>
</Modal>
</>
)
}
export default PaymentInfo

View File

@ -0,0 +1,43 @@
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 PostPayment = ({ onOk, objectData }) => {
const [postLoading, setPostLoading] = useState(false)
const { sendObjectFunction } = useContext(ApiServerContext)
const handlePost = async () => {
setPostLoading(true)
try {
const result = await sendObjectFunction(objectData._id, 'Payment', 'post')
if (result) {
message.success('Payment posted successfully')
onOk(result)
}
} catch (error) {
console.error('Error posting payment:', error)
} finally {
setPostLoading(false)
}
}
return (
<MessageDialogView
title={'Are you sure you want to post this payment?'}
description={`Posting payment ${objectData?.name || objectData?._reference || objectData?._id} will set it to posted status.`}
onOk={handlePost}
okText='Post'
okLoading={postLoading}
/>
)
}
PostPayment.propTypes = {
onOk: PropTypes.func.isRequired,
objectData: PropTypes.object
}
export default PostPayment

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/paymenticon.svg?react'
const PaymentIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default PaymentIcon

View File

@ -31,6 +31,7 @@ import { DocumentJob } from './models/DocumentJob.js'
import { TaxRate } from './models/TaxRate.js' import { TaxRate } from './models/TaxRate.js'
import { TaxRecord } from './models/TaxRecord.js' import { TaxRecord } from './models/TaxRecord.js'
import { Invoice } from './models/Invoice.js' import { Invoice } from './models/Invoice.js'
import { Payment } from './models/Payment.js'
import { Client } from './models/Client.js' import { Client } from './models/Client.js'
import { SalesOrder } from './models/SalesOrder.js' import { SalesOrder } from './models/SalesOrder.js'
import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon' import QuestionCircleIcon from '../components/Icons/QuestionCircleIcon'
@ -69,6 +70,7 @@ export const objectModels = [
TaxRate, TaxRate,
TaxRecord, TaxRecord,
Invoice, Invoice,
Payment,
Client, Client,
SalesOrder SalesOrder
] ]
@ -108,6 +110,7 @@ export {
TaxRate, TaxRate,
TaxRecord, TaxRecord,
Invoice, Invoice,
Payment,
Client, Client,
SalesOrder SalesOrder
} }

View File

@ -0,0 +1,252 @@
import PaymentIcon from '../../components/Icons/PaymentIcon'
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
import CheckIcon from '../../components/Icons/CheckIcon'
import EditIcon from '../../components/Icons/EditIcon'
import XMarkIcon from '../../components/Icons/XMarkIcon'
import BinIcon from '../../components/Icons/BinIcon'
export const Payment = {
name: 'payment',
label: 'Payment',
prefix: 'PAY',
icon: PaymentIcon,
actions: [
{
name: 'info',
label: 'Info',
default: true,
row: true,
icon: InfoCircleIcon,
url: (_id) => `/dashboard/finance/payments/info?paymentId=${_id}`
},
{
name: 'edit',
label: 'Edit',
type: 'button',
icon: EditIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=edit`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{
name: 'cancelEdit',
label: 'Cancel Edit',
type: 'button',
icon: XMarkIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=cancelEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'finishEdit',
label: 'Finish Edit',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=finishEdit`,
visible: (objectData) => {
return objectData?._isEditing && objectData?._isEditing == true
}
},
{
name: 'delete',
label: 'Delete',
type: 'button',
icon: BinIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=delete`,
visible: (objectData) => {
return !(objectData?._isEditing && objectData?._isEditing == true)
},
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
}
},
{ type: 'divider' },
{
name: 'post',
label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
{
name: 'cancel',
label: 'Cancel',
type: 'button',
icon: XMarkIcon,
danger: true,
url: (_id) =>
`/dashboard/finance/payments/info?paymentId=${_id}&action=cancel`,
disabled: (objectData) => {
return objectData?.state?.type == 'cancelled'
},
visible: (objectData) => {
return (
objectData?.state?.type == 'draft' ||
objectData?.state?.type == 'posted'
)
}
}
],
group: ['vendor', 'client', 'invoice'],
filters: ['vendor', 'client', 'invoice'],
sorters: ['createdAt', 'state', 'updatedAt', 'paymentDate'],
columns: [
'_reference',
'state',
'invoice',
'vendor',
'client',
'paymentDate',
'amount',
'paymentMethod',
'createdAt',
'updatedAt'
],
properties: [
{
name: '_id',
label: 'ID',
type: 'id',
columnFixed: 'left',
objectType: 'payment',
columnWidth: 140,
showCopy: true
},
{
name: 'createdAt',
label: 'Created At',
type: 'dateTime',
readOnly: true
},
{
name: '_reference',
label: 'Reference',
type: 'reference',
columnFixed: 'left',
required: true,
objectType: 'payment',
showCopy: true,
readOnly: true
},
{
name: 'updatedAt',
label: 'Updated At',
type: 'dateTime',
readOnly: true
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
name: 'invoice',
label: 'Invoice',
type: 'object',
objectType: 'invoice',
required: true,
showHyperlink: true
},
{
name: 'useRemainingAmount',
label: 'Use Remaining Amount',
type: 'boolean',
required: true
},
{
name: 'vendor',
label: 'Vendor',
type: 'object',
objectType: 'vendor',
showHyperlink: true,
readOnly: true
},
{
name: 'client',
label: 'Client',
type: 'object',
objectType: 'client',
showHyperlink: true,
readOnly: true
},
{
name: 'paymentDate',
label: 'Payment Date',
type: 'dateTime',
required: true
},
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true
},
{
name: 'amount',
label: 'Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
required: true,
columnWidth: 150
},
{
name: 'paymentMethod',
label: 'Payment Method',
type: 'string',
required: false
},
{
name: 'notes',
label: 'Notes',
type: 'text',
required: false
}
],
stats: [
{
name: 'draft.draftCount.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
name: 'draft.draftAmount.sum',
label: 'Draft Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'default'
},
{
name: 'posted.postedCount.count',
label: 'Posted',
type: 'number',
color: 'cyan'
},
{
name: 'posted.postedAmount.sum',
label: 'Posted Amount',
type: 'number',
prefix: '£',
roundNumber: 2,
color: 'cyan'
}
]
}