Tom Butcher eea7e1f724
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Add InfoActionButtons to Various Dashboard Components
- Integrated InfoActionButtons into multiple components across the Finance and Inventory sections, enhancing user interaction and providing contextual actions for invoices, payment policies, payments, tax records, and various inventory items.
- Updated the respective components to include the new buttons with appropriate object types and list mode settings, improving the overall functionality and user experience in the dashboard.
2026-09-14 23:33:03 +01:00

292 lines
8.2 KiB
JavaScript

import PropTypes from 'prop-types'
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { Button, Dropdown, Modal, message } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { TableStateContext } from '../context/TableStateContext'
import MailIcon from '../../Icons/MailIcon'
import EmailTemplateIcon from '../../Icons/EmailTemplateIcon'
import NewEmailMessage from '../Management/EmailMessages/NewEmailMessage'
import {
getDefaultEmailRecipient,
resolveEmailRecipients,
resolveEmailRecipientsForObjects
} from '../utils/emailRecipients'
import LoadingPleaseWaitModal from './LoadingPleaseWaitModal'
import { fetchAllObjectPages } from '../utils/fetchAllObjectPages'
import { resolveObjectsList } from '../utils/templateObjectData'
const EmailSendButton = ({
type,
objectData,
objects,
listMode = false,
disabled = false,
...buttonProps
}) => {
const {
fetchObject,
fetchObjects,
connected,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const tableState = useContext(TableStateContext)
const [templates, setTemplates] = useState([])
const [recipients, setRecipients] = useState([])
const [selectedTemplate, setSelectedTemplate] = useState(null)
const [selectedObjects, setSelectedObjects] = useState(() =>
resolveObjectsList(objects, objectData)
)
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [listLoading, setListLoading] = useState(false)
const fetchObjectsRef = useRef(fetchObjects)
const fetchObjectRef = useRef(fetchObject)
fetchObjectsRef.current = fetchObjects
fetchObjectRef.current = fetchObject
const resolvedObjects = useMemo(
() => resolveObjectsList(objects, objectData),
[objects, objectData]
)
const filter = useMemo(
() => ({ objectType: type, global: false, active: true }),
[type]
)
const matches = useCallback(
(template) =>
template?.objectType === type &&
template?.global === false &&
template?.active === true,
[type]
)
const loadTemplates = useCallback(
async (silent = false) => {
if (!type || token == null) return
if (!silent) setLoading(true)
try {
const result = await fetchObjectsRef.current('emailTemplate', {
filter,
limit: 100
})
setTemplates(Array.isArray(result?.data) ? result.data : [])
} finally {
if (!silent) setLoading(false)
}
},
[filter, token, type]
)
useEffect(() => {
loadTemplates()
}, [loadTemplates])
useEffect(() => {
if (listMode) {
setRecipients([])
return undefined
}
let cancelled = false
setRecipients([])
resolveEmailRecipients({
type,
objectData: resolvedObjects[0],
fetchObject: (...args) => fetchObjectRef.current(...args)
}).then((result) => {
if (!cancelled) setRecipients(result)
})
return () => {
cancelled = true
}
}, [listMode, resolvedObjects, type])
useEffect(() => {
if (!connected) return undefined
return subscribeToObjectTypeUpdates('emailTemplate', filter, () =>
loadTemplates(true)
)
}, [connected, filter, loadTemplates, subscribeToObjectTypeUpdates])
useEffect(() => {
if (!connected) return undefined
const unsubscribes = templates
.filter((template) => template?._id)
.map((template) =>
subscribeToObjectUpdates(
template._id.toLowerCase(),
'emailTemplate',
(update) => {
setTemplates((previous) => {
const merged = { ...template, ...update }
if (!matches(merged)) {
return previous.filter((entry) => entry._id !== template._id)
}
return previous.map((entry) =>
entry._id === template._id ? merged : entry
)
})
}
)
)
return () => unsubscribes.forEach((unsubscribe) => unsubscribe?.())
}, [connected, matches, subscribeToObjectUpdates, templates])
const handleTemplateSelect = useCallback(
async (template) => {
setSelectedTemplate(template)
if (listMode) {
setListLoading(true)
try {
const list = await fetchAllObjectPages({
fetchObjects: fetchObjectsRef.current,
type,
filter: tableState?.getObjectListFilter?.(type) || {},
sorter: tableState?.getObjectListSorter?.(type) || {}
})
if (!list.length) {
message.warning('No objects in this list to email')
return
}
const nextRecipients = await resolveEmailRecipientsForObjects({
type,
objects: list,
fetchObject: (...args) => fetchObjectRef.current(...args)
})
if (!nextRecipients.length) {
message.warning('No email recipients available for these objects')
return
}
setSelectedObjects(list)
setRecipients(nextRecipients)
setOpen(true)
} catch (error) {
console.error('Error fetching objects for email:', error)
message.error('Failed to load objects for email')
} finally {
setListLoading(false)
}
return
}
setSelectedObjects(resolvedObjects)
setOpen(true)
},
[listMode, resolvedObjects, tableState, type]
)
const menuItems = useMemo(() => {
const byTag = new Map()
const untagged = []
const makeItem = (template) => ({
key: template._id,
label: template.name,
icon: <EmailTemplateIcon />,
onClick: () => {
handleTemplateSelect(template)
}
})
templates.forEach((template) => {
const tags = Array.isArray(template.tags) ? template.tags.filter(Boolean) : []
if (!tags.length) {
untagged.push(template)
} else {
tags.forEach((tag) => byTag.set(tag, [...(byTag.get(tag) || []), template]))
}
})
return [
...untagged
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(makeItem),
...[...byTag.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([tag, taggedTemplates]) => ({
key: tag,
label: tag,
children: taggedTemplates
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(makeItem)
}))
]
}, [handleTemplateSelect, templates])
const defaultRecipient = useMemo(
() => getDefaultEmailRecipient(recipients),
[recipients]
)
const unavailable =
disabled ||
loading ||
listLoading ||
!templates.length ||
(!listMode && (!resolvedObjects.length || !recipients.length))
const title = listMode
? !templates.length
? 'No email templates available for this object type'
: 'Send email'
: !recipients.length
? 'No email recipients available for this object'
: !templates.length
? 'No email templates available for this object type'
: 'Send email'
return (
<>
<Dropdown menu={{ items: menuItems }} trigger={['hover']} disabled={unavailable}>
<Button
{...buttonProps}
icon={<MailIcon />}
disabled={unavailable}
loading={loading || listLoading}
title={title}
/>
</Dropdown>
<LoadingPleaseWaitModal open={listLoading} />
<Modal
open={open}
onCancel={() => setOpen(false)}
footer={null}
destroyOnHidden
width={{ xs: '100%', sm: '100%', md: '100%', lg: '90%', xl: '80%' }}
>
<NewEmailMessage
onOk={() => setOpen(false)}
defaultValues={{
objectType: type,
objects: selectedObjects,
emailTemplate: selectedTemplate,
...defaultRecipient
}}
recipientCandidates={recipients}
/>
</Modal>
</>
)
}
EmailSendButton.propTypes = {
type: PropTypes.string.isRequired,
objectData: PropTypes.object,
objects: PropTypes.array,
listMode: PropTypes.bool,
disabled: PropTypes.bool
}
export default EmailSendButton