From f155ede177489ba5b6312027a0d01964af1be6dd Mon Sep 17 00:00:00 2001 From: Alisher Musurmonov Date: Fri, 27 Oct 2023 23:45:36 +0500 Subject: [PATCH] UIORGS-387: add donors fetch modal --- lib/Donors/DonorsForm.js | 62 +++++++ lib/Donors/DonorsFormList.js | 157 ++++++++++++++++++ .../DonorsListFilters/DonorsListFilters.js | 56 +++++++ .../FindDonors/DonorsListFilters/index.js | 1 + lib/Donors/FindDonors/FindDonors.js | 122 ++++++++++++++ lib/Donors/FindDonors/constants.js | 30 ++++ lib/Donors/FindDonors/hooks/index.js | 2 + .../hooks/useDonorContacts/index.js | 1 + .../useDonorContacts/useDonorContacts.js | 36 ++++ .../FindDonors/hooks/useDonors/index.js | 1 + .../FindDonors/hooks/useDonors/useDonors.js | 88 ++++++++++ lib/Donors/index.js | 1 + lib/index.js | 1 + translations/stripes-acq-components/en.json | 13 +- 14 files changed, 570 insertions(+), 1 deletion(-) create mode 100644 lib/Donors/DonorsForm.js create mode 100644 lib/Donors/DonorsFormList.js create mode 100644 lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js create mode 100644 lib/Donors/FindDonors/DonorsListFilters/index.js create mode 100644 lib/Donors/FindDonors/FindDonors.js create mode 100644 lib/Donors/FindDonors/constants.js create mode 100644 lib/Donors/FindDonors/hooks/index.js create mode 100644 lib/Donors/FindDonors/hooks/useDonorContacts/index.js create mode 100644 lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js create mode 100644 lib/Donors/FindDonors/hooks/useDonors/index.js create mode 100644 lib/Donors/FindDonors/hooks/useDonors/useDonors.js create mode 100644 lib/Donors/index.js diff --git a/lib/Donors/DonorsForm.js b/lib/Donors/DonorsForm.js new file mode 100644 index 00000000..e43052dc --- /dev/null +++ b/lib/Donors/DonorsForm.js @@ -0,0 +1,62 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { FieldArray } from 'react-final-form-arrays'; + +import { + Col, + Loading, + Row, +} from '@folio/stripes/components'; + +import { useDonorContacts } from './FindDonors/hooks'; +import DonorsFormList from './DonorsFormList'; + +function DonorsForm({ name, donorOrganizationIds }) { + const [donors, setDonors] = useState([]); + const { fetchDonorsMutation, isLoading } = useDonorContacts(); + + const handleFetchDonors = useCallback(ids => { + fetchDonorsMutation({ donorOrganizationIds: ids }) + .then((data) => { + setDonors(data); + }); + }, [fetchDonorsMutation]); + + useEffect(() => { + handleFetchDonors(donorOrganizationIds); + }, [donorOrganizationIds, handleFetchDonors]); + + const donorsMap = donors.reduce((acc, contact) => { + acc[contact.id] = contact; + + return acc; + }, {}); + + if (isLoading) { + return ; + } + + return ( + + + + + + ); +} + +DonorsForm.propTypes = { + name: PropTypes.string.isRequired, + donorOrganizationIds: PropTypes.arrayOf(PropTypes.string), +}; + +DonorsForm.defaultProps = { + donorOrganizationIds: [], +}; + +export default DonorsForm; diff --git a/lib/Donors/DonorsFormList.js b/lib/Donors/DonorsFormList.js new file mode 100644 index 00000000..7eeba8c6 --- /dev/null +++ b/lib/Donors/DonorsFormList.js @@ -0,0 +1,157 @@ +import React, { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { + map, + sortBy, +} from 'lodash'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; + +import { + Button, + Icon, + MultiColumnList, +} from '@folio/stripes/components'; +import { useStripes } from '@folio/stripes/core'; + +import FindDonors from './FindDonors/FindDonors'; +import { acqRowFormatter } from '../utils'; + +const columnMapping = { + name: , + code: , + unassignDonor: null, +}; + +const visibleColumns = [ + 'name', + 'code', + 'unassignDonor', +]; + +const getResultsFormatter = ({ + intl, + fields, +}) => ({ + name: donor => donor.name, + code: donor => donor.code, + unassignDonor: (donor) => ( + + ), +}); + +const getDonorUrl = (orgId) => { + return `/organizations/view/${orgId}`; +}; + +const AddDonorButton = ({ fetchDonors, fields, stripes }) => { + const addDonors = (contacts = []) => { + const addedContactIds = new Set(fields.value); + const newContactsIds = map(contacts.filter(({ id }) => !addedContactIds.has(id)), 'id'); + + if (newContactsIds.length) { + fetchDonors([...addedContactIds, ...newContactsIds]); + newContactsIds.forEach(contactId => fields.push(contactId)); + } + }; + + return ( + } + searchButtonStyle="default" + disableRecordCreation + stripes={stripes} + addDonors={addDonors} + > + + + + + ); +}; + +AddDonorButton.propTypes = { + fetchDonors: PropTypes.func.isRequired, + fields: PropTypes.object, + stripes: PropTypes.object, +}; + +const alignRowProps = { alignLastColToEnd: true }; + +const DonorsFormList = ({ fetchDonors, fields, donorsMap, orgId, categoriesDict }) => { + const intl = useIntl(); + const stripes = useStripes(); + const contacts = (fields.value || []) + .map((contactId, _index) => { + const contact = donorsMap?.[contactId]; + + return { + ...(contact || { isDeleted: true }), + _index, + }; + }); + const contentData = sortBy(contacts, [({ lastName }) => lastName?.toLowerCase()]); + + const anchoredRowFormatter = ({ rowProps, ...rest }) => { + // console.log(rowProps, rest) + return acqRowFormatter({ + ...rest, + rowProps: { + ...rowProps, + to: getDonorUrl(rest.rowData.id), + }, + }); + }; + + const resultsFormatter = useMemo(() => { + return getResultsFormatter({ intl, categoriesDict, fields }); + }, [categoriesDict, fields, intl]); + + return ( + <> + +
+ + + ); +}; + +DonorsFormList.propTypes = { + fetchDonors: PropTypes.func.isRequired, + fields: PropTypes.object, + orgId: PropTypes.string, + categoriesDict: PropTypes.arrayOf(PropTypes.object), + donorsMap: PropTypes.object, +}; + +export default DonorsFormList; diff --git a/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js b/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js new file mode 100644 index 00000000..9374400d --- /dev/null +++ b/lib/Donors/FindDonors/DonorsListFilters/DonorsListFilters.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import { useCallback } from 'react'; + +import { AccordionSet } from '@folio/stripes/components'; +// import { TypeFilter } from '@folio/plugin-find-organization'; + +import { AcqTagsFilter } from '../../../AcqTagsFilter'; +import { AcqCheckboxFilter } from '../../../AcqCheckboxFilter'; +import { + DONOR_FILTERS, + DONOR_STATUS_OPTIONS, +} from '../constants'; + +export const DonorsListFilters = ({ + activeFilters, + applyFilters, + disabled, +}) => { + const adaptedApplyFilters = useCallback(({ name, values }) => applyFilters(name, values), [applyFilters]); + + return ( + + {/* */} + + + + + + ); +}; + +DonorsListFilters.propTypes = { + activeFilters: PropTypes.object.isRequired, + applyFilters: PropTypes.func.isRequired, + disabled: PropTypes.bool, +}; diff --git a/lib/Donors/FindDonors/DonorsListFilters/index.js b/lib/Donors/FindDonors/DonorsListFilters/index.js new file mode 100644 index 00000000..9863de0d --- /dev/null +++ b/lib/Donors/FindDonors/DonorsListFilters/index.js @@ -0,0 +1 @@ +export { DonorsListFilters } from './DonorsListFilters'; diff --git a/lib/Donors/FindDonors/FindDonors.js b/lib/Donors/FindDonors/FindDonors.js new file mode 100644 index 00000000..893543f3 --- /dev/null +++ b/lib/Donors/FindDonors/FindDonors.js @@ -0,0 +1,122 @@ +import { useCallback, useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { noop } from 'lodash'; + +import { FindRecords } from '../../FindRecords'; +import { PLUGIN_RESULT_COUNT_INCREMENT } from '../../constants'; +import { DonorsListFilters } from './DonorsListFilters'; +import { useDonors } from './hooks'; + +const idPrefix = 'uiFindDonors-'; +const modalLabel = ; +const resultsPaneTitle = ; + +const columnWidths = { + isChecked: '8%', + code: '30%', + name: '40%', +}; +const visibleColumns = ['name', 'code']; +const columnMapping = { + name: , + code: , +}; + +const INIT_PAGINATION = { limit: PLUGIN_RESULT_COUNT_INCREMENT, offset: 0 }; + +const FindDonors = ({ + addDonors, + isMultiSelect, + renderNewContactBtn, + ...rest +}) => { + const [totalCount, setTotalCount] = useState(0); + const [records, setRecords] = useState([]); + const [searchParams, setSearchParams] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const [pagination, setPagination] = useState(INIT_PAGINATION); + + const { fetchDonors } = useDonors(); + + const refreshRecords = useCallback((filters) => { + setIsLoading(true); + + setRecords([]); + setTotalCount(0); + setPagination(INIT_PAGINATION); + setSearchParams(filters); + + fetchDonors({ ...INIT_PAGINATION, searchParams: filters }) + .then(({ organizations, totalRecords }) => { + setTotalCount(totalRecords); + setRecords(organizations); + }) + .finally(() => setIsLoading(false)); + }, [fetchDonors]); + + const onNeedMoreData = useCallback((newPagination) => { + setIsLoading(true); + + fetchDonors({ ...newPagination, searchParams }) + .then(({ organizations }) => { + setPagination(newPagination); + setRecords(organizations); + }) + .finally(() => setIsLoading(false)); + }, [fetchDonors, searchParams]); + + const renderFilters = useCallback((activeFilters, applyFilters) => { + return ( + + ); + }, [isLoading]); + + return ( + + ); +}; + +FindDonors.propTypes = { + disabled: PropTypes.bool, + marginBottom0: PropTypes.bool, + marginTop0: PropTypes.bool, + searchButtonStyle: PropTypes.string, + searchLabel: PropTypes.node, + addDonors: PropTypes.func, + renderNewContactBtn: PropTypes.func, + isMultiSelect: PropTypes.bool, +}; + +FindDonors.defaultProps = { + disabled: false, + searchLabel: , + addDonors: noop, + renderNewContactBtn: noop, + isMultiSelect: true, +}; + +export default FindDonors; diff --git a/lib/Donors/FindDonors/constants.js b/lib/Donors/FindDonors/constants.js new file mode 100644 index 00000000..326135fe --- /dev/null +++ b/lib/Donors/FindDonors/constants.js @@ -0,0 +1,30 @@ +import { FormattedMessage } from 'react-intl'; + +export const FINANCE_FUNDS_API = 'finance/funds'; + +export const DONOR_FILTERS = { + IS_VENDOR: 'isVendor', + TAGS: 'tags', + TYPES: 'organizationTypes', +}; + +export const DONOR_STATUSES = { + ACTIVE: 'true', + INACTIVE: 'false', +}; + +export const DONOR_STATUS_OPTIONS = [ + { + value: DONOR_STATUSES.ACTIVE, + label: , + }, + { + value: DONOR_STATUSES.INACTIVE, + label: , + }, +]; + +export const DONORS_SORT_MAP = { + name: 'name', + code: 'code', +}; diff --git a/lib/Donors/FindDonors/hooks/index.js b/lib/Donors/FindDonors/hooks/index.js new file mode 100644 index 00000000..8413d293 --- /dev/null +++ b/lib/Donors/FindDonors/hooks/index.js @@ -0,0 +1,2 @@ +export { useDonors } from './useDonors'; +export { useDonorContacts } from './useDonorContacts'; diff --git a/lib/Donors/FindDonors/hooks/useDonorContacts/index.js b/lib/Donors/FindDonors/hooks/useDonorContacts/index.js new file mode 100644 index 00000000..81bc25f1 --- /dev/null +++ b/lib/Donors/FindDonors/hooks/useDonorContacts/index.js @@ -0,0 +1 @@ +export { useDonorContacts } from './useDonorContacts'; diff --git a/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js b/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js new file mode 100644 index 00000000..d0b8b443 --- /dev/null +++ b/lib/Donors/FindDonors/hooks/useDonorContacts/useDonorContacts.js @@ -0,0 +1,36 @@ +import { useMutation } from 'react-query'; + +import { useOkapiKy } from '@folio/stripes/core'; + +import { batchRequest } from '../../../../utils'; +import { VENDORS_API } from '../../../../constants'; + +const buildQueryByIds = (itemsChunk) => { + const query = itemsChunk + .map(chunkId => `id==${chunkId}`) + .join(' or '); + + return query || ''; +}; + +export const useDonorContacts = () => { + const ky = useOkapiKy(); + + const { isLoading, mutateAsync } = useMutation( + ({ donorOrganizationIds }) => { + return batchRequest( + ({ params: searchParams }) => ky + .get(VENDORS_API, { searchParams }) + .json() + .then(({ organizations }) => organizations), + donorOrganizationIds, + buildQueryByIds, + ); + }, + ); + + return ({ + fetchDonorsMutation: mutateAsync, + isLoading, + }); +}; diff --git a/lib/Donors/FindDonors/hooks/useDonors/index.js b/lib/Donors/FindDonors/hooks/useDonors/index.js new file mode 100644 index 00000000..9f4b89f4 --- /dev/null +++ b/lib/Donors/FindDonors/hooks/useDonors/index.js @@ -0,0 +1 @@ +export { useDonors } from './useDonors'; diff --git a/lib/Donors/FindDonors/hooks/useDonors/useDonors.js b/lib/Donors/FindDonors/hooks/useDonors/useDonors.js new file mode 100644 index 00000000..c5cec978 --- /dev/null +++ b/lib/Donors/FindDonors/hooks/useDonors/useDonors.js @@ -0,0 +1,88 @@ +import { useMutation } from 'react-query'; + +import { useOkapiKy } from '@folio/stripes/core'; + +import { + ASC_DIRECTION, + buildFilterQuery, + connectQuery, +} from '../../../../AcqList'; +import { + PLUGIN_RESULT_COUNT_INCREMENT, + VENDORS_API, +} from '../../../../constants'; +import { + DONOR_FILTERS, + DONORS_SORT_MAP, +} from '../../constants'; + +const DEFAULT_QUERY = 'status=="active" and isDonor=true'; + +const buildCustomFilterQuery = filter => values => { + const value = values.map(val => `"${val}"`).join(' or '); + + return `${filter}=(${value})`; +}; + +const buildCustomSortingQuery = ({ sorting, sortingDirection } = {}) => { + if (sorting) { + const sortIndex = (DONORS_SORT_MAP[sorting] || sorting) + .split(' ') + .map(value => `${value}/sort.${sortingDirection || ASC_DIRECTION}`) + .join(' '); + + return `sortby ${sortIndex}`; + } + + return ''; +}; + +const buildDonorsQuery = searchParams => { + const mainQuery = buildFilterQuery( + searchParams, + (query) => `name="${query}*" or code="${query}*"`, + { + [DONOR_FILTERS.IS_VENDOR]: buildCustomFilterQuery(DONOR_FILTERS.IS_VENDOR), + [DONOR_FILTERS.TAGS]: buildCustomFilterQuery(DONOR_FILTERS.TAGS), + [DONOR_FILTERS.TYPES]: buildCustomFilterQuery(DONOR_FILTERS.TYPES), + }, + ); + + const sortingQuery = buildCustomSortingQuery(searchParams); + const filtersQuery = mainQuery ? `${mainQuery} and ${DEFAULT_QUERY}` : DEFAULT_QUERY; + + return connectQuery(filtersQuery, sortingQuery); +}; + +export const useDonors = () => { + const ky = useOkapiKy(); + + const { + mutateAsync: fetchDonors, + } = useMutation({ + mutationFn: async ({ + searchParams = {}, + limit = PLUGIN_RESULT_COUNT_INCREMENT, + offset = 0, + }) => { + const donorsQuery = buildDonorsQuery(searchParams); + const builtSearchParams = { + query: donorsQuery, + limit, + offset, + }; + + const { + organizations = [], + totalRecords, + } = await ky.get(VENDORS_API, { searchParams: { ...builtSearchParams } }).json(); + + return { + organizations, + totalRecords, + }; + }, + }); + + return { fetchDonors }; +}; diff --git a/lib/Donors/index.js b/lib/Donors/index.js new file mode 100644 index 00000000..f53891a8 --- /dev/null +++ b/lib/Donors/index.js @@ -0,0 +1 @@ +export { default as Donors } from './DonorsForm'; diff --git a/lib/index.js b/lib/index.js index ea8509f8..0760f77a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -16,6 +16,7 @@ export * from './Currency'; export * from './CurrencyExchangeRateFields'; export * from './CurrencySymbol'; export * from './DeleteHoldingsModal'; +export * from './Donors'; export * from './DragDropMCL'; export * from './DynamicSelection'; export * from './DynamicSelectionFilter'; diff --git a/translations/stripes-acq-components/en.json b/translations/stripes-acq-components/en.json index 5b2d35a1..caac0e64 100644 --- a/translations/stripes-acq-components/en.json +++ b/translations/stripes-acq-components/en.json @@ -187,5 +187,16 @@ "acquisition_method.other": "Other", "acquisition_method.purchase": "Purchase", "acquisition_method.purchaseAtVendorSystem": "Purchase at vendor system", - "acquisition_method.technical": "Technical" + "acquisition_method.technical": "Technical", + + "donors.button.addDonor": "Add donor", + "donors.noFindDonorPlugin": "no find-contact plugin", + "donors.button.unassign": "Unassign", + "donors.column.name": "Name", + "donors.column.code": "Code", + "donors.filter.isVendor": "Is vendor", + "donors.filter.isVendor.active": "Active", + "donors.filter.isVendor.inactive": "Inactive", + "donors.modal.title": "Add donors", + "donors.modal.subTitle": "Donors" }