diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98025c4..ea0296f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## (IN PROGRESS)
+* Support EDIFACT claims export. Refs UIEXPMGR-113.
+* Support CSV claims export. Refs UIEXPMGR-114.
+
## [3.2.0](https://github.com/folio-org/ui-export-manager/tree/v3.2.0) (2024-10-31)
[Full Changelog](https://github.com/folio-org/ui-export-manager/compare/v3.1.1...v3.2.0)
diff --git a/src/ExportEdiJobs/ExportEdiJobDetails/ExportEdiJobDetails.js b/src/ExportEdiJobs/ExportEdiJobDetails/ExportEdiJobDetails.js
index eb113c6..a857100 100644
--- a/src/ExportEdiJobs/ExportEdiJobDetails/ExportEdiJobDetails.js
+++ b/src/ExportEdiJobs/ExportEdiJobDetails/ExportEdiJobDetails.js
@@ -1,6 +1,9 @@
-import React, { useCallback } from 'react';
+import { useCallback } from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage, useIntl } from 'react-intl';
+import {
+ FormattedMessage,
+ useIntl,
+} from 'react-intl';
import {
Col,
@@ -26,14 +29,24 @@ import { useNavigation } from '../../hooks';
import { useExportJobQuery } from '../../ExportJob/apiQuery';
import { ExportEdiJobDetailsActionMenu } from '../ExportEdiJobDetailsActionMenu';
+const FILE_DOWNLOAD = 'File download';
+
+const getSentToValue = (exportConfig, intl) => {
+ if (exportConfig?.transmissionMethod === FILE_DOWNLOAD) {
+ return intl.formatMessage({ id: 'ui-export-manager.exportJob.download' });
+ }
+
+ return `${exportConfig?.ediFtp?.serverAddress}${exportConfig?.ediFtp?.orderDirectory || ''}`;
+};
+
export const ExportEdiJobDetails = ({ refetchJobs, uuid }) => {
- const { formatMessage } = useIntl();
+ const intl = useIntl();
const { navigateToEdiJobs } = useNavigation();
- const perms = useExportManagerPerms()
+ const perms = useExportManagerPerms();
const {
hasAllExportManagerPerms
- } = perms
+ } = perms;
const {
isLoading: isJobLoading,
@@ -54,7 +67,7 @@ export const ExportEdiJobDetails = ({ refetchJobs, uuid }) => {
isLoading: isOrganizationLoading,
} = useOrganization(exportConfig?.vendorId);
- const title = formatMessage(
+ const title = intl.formatMessage(
{ id: 'ui-export-manager.exportJob' },
{ jobId: exportJob.jobId },
);
@@ -164,7 +177,7 @@ export const ExportEdiJobDetails = ({ refetchJobs, uuid }) => {
}
- value={`${exportConfig?.ediFtp?.serverAddress}${exportConfig?.ediFtp?.orderDirectory || ''}`}
+ value={getSentToValue(exportConfig, intl)}
/>
diff --git a/src/ExportEdiJobs/ExportEdiJobsFilters/ExportEdiJobsFilters.js b/src/ExportEdiJobs/ExportEdiJobsFilters/ExportEdiJobsFilters.js
index 84bc344..7afe1c0 100644
--- a/src/ExportEdiJobs/ExportEdiJobsFilters/ExportEdiJobsFilters.js
+++ b/src/ExportEdiJobs/ExportEdiJobsFilters/ExportEdiJobsFilters.js
@@ -15,15 +15,18 @@ import {
PluggableOrganizationFilter,
} from '@folio/stripes-acq-components';
-import { EXPORT_JOB_STATUS_OPTIONS } from '../../common/constants';
+import {
+ EXPORT_JOB_STATUS_OPTIONS,
+ ORGANIZATION_INTEGRATION_TYPE_OPTIONS,
+} from '../../common/constants';
import { ExportMethodFilter } from './ExportMethodFilter';
const applyFiltersAdapter = (applyFilters) => ({ name, values }) => applyFilters(name, values);
export const ExportEdiJobsFilters = ({
- disabled = false,
- activeFilters,
- applyFilters,
+ disabled = false,
+ activeFilters,
+ applyFilters,
}) => {
const adaptedApplyFilters = useCallback(
applyFiltersAdapter(applyFilters),
@@ -43,6 +46,17 @@ export const ExportEdiJobsFilters = ({
closedByDefault={false}
/>
+
+
{
+ const configName = ORGANIZATION_INTEGRATION_EXPORT_TYPES
+ .map(type => `"${type}_${organizationId}*"`)
+ .join(JOIN_STRING);
+
+ return `configName==(${configName})`;
+};
+
+const buildTypeCql = () => {
+ const type = ORGANIZATION_INTEGRATION_EXPORT_TYPES
+ .map((t) => `"${t}"`)
+ .join(JOIN_STRING);
+
+ return `type==(${type})`;
+};
export const useConfigs = (organizationId) => {
const ky = useOkapiKy();
@@ -14,14 +35,14 @@ export const useConfigs = (organizationId) => {
const searchParams = {
query: organizationId
- ? `configName==EDIFACT_ORDERS_EXPORT_${organizationId}*`
- : 'type==EDIFACT_ORDERS_EXPORT',
+ ? buildConfigNameCql(organizationId)
+ : buildTypeCql(),
limit: LIMIT_MAX,
};
const { isFetching, data = {} } = useQuery(
[namespace, organizationId],
- () => ky.get('data-export-spring/configs', { searchParams }).json(),
+ () => ky.get(DATA_EXPORT_CONFIGS_API, { searchParams }).json(),
);
return ({
diff --git a/src/ExportEdiJobs/ExportEdiJobsFilters/ExportMethodFilter/useConfigs.test.js b/src/ExportEdiJobs/ExportEdiJobsFilters/ExportMethodFilter/useConfigs.test.js
index 3374844..08f3130 100644
--- a/src/ExportEdiJobs/ExportEdiJobsFilters/ExportMethodFilter/useConfigs.test.js
+++ b/src/ExportEdiJobs/ExportEdiJobsFilters/ExportMethodFilter/useConfigs.test.js
@@ -47,7 +47,7 @@ describe('useConfigs', () => {
'data-export-spring/configs',
{
searchParams: {
- query: 'type==EDIFACT_ORDERS_EXPORT',
+ query: 'type==("CLAIMS" or "EDIFACT_ORDERS_EXPORT")',
limit: LIMIT_MAX,
},
},
diff --git a/src/ExportEdiJobs/apiQuery.js b/src/ExportEdiJobs/apiQuery.js
index 79f971f..5c2fb14 100644
--- a/src/ExportEdiJobs/apiQuery.js
+++ b/src/ExportEdiJobs/apiQuery.js
@@ -1,14 +1,29 @@
-import { useInfiniteQuery } from 'react-query';
import queryString from 'query-string';
+import { useInfiniteQuery } from 'react-query';
-import { useOkapiKy, useNamespace } from '@folio/stripes/core';
+import {
+ useOkapiKy,
+ useNamespace,
+} from '@folio/stripes/core';
import {
buildDateRangeQuery,
+ CQL_OR_OPERATOR,
makeQueryBuilder,
+ ORGANIZATION_INTEGRATION_EXPORT_TYPES,
} from '@folio/stripes-acq-components';
+const buildIntegrationTypesCqlValue = (type) => {
+ if (!Array.isArray(type)) return type;
+
+ const value = type
+ .map((t) => `"${t}"`)
+ .join(` ${CQL_OR_OPERATOR} `);
+
+ return `(${value})`;
+};
+
const buildJobsQuery = makeQueryBuilder(
- 'type="EDIFACT_ORDERS_EXPORT"',
+ `type=${buildIntegrationTypesCqlValue(ORGANIZATION_INTEGRATION_EXPORT_TYPES)}`,
(query) => {
return `name="*${query}*" or description="*${query}*"`;
},
@@ -18,6 +33,9 @@ const buildJobsQuery = makeQueryBuilder(
startTime: buildDateRangeQuery.bind(null, ['startTime']),
vendorId: (id) => `jsonb.exportTypeSpecificParameters.vendorEdiOrdersExportConfig.vendorId=="${id}"`,
exportConfigId: (id) => `jsonb.exportTypeSpecificParameters.vendorEdiOrdersExportConfig.exportConfigId=="${id}"`,
+ integrationType: (type) => (
+ `jsonb.exportTypeSpecificParameters.vendorEdiOrdersExportConfig.integrationType==${buildIntegrationTypesCqlValue(type)}`
+ ),
},
{
jobId: 'name',
@@ -41,7 +59,10 @@ export const useExportEdiJobsQuery = (search, pagination, filters) => {
searchParams: {
limit: pagination.limit,
offset: pagination.offset,
- query: buildJobsQuery(queryString.parse(`${search}&type=EDIFACT_ORDERS_EXPORT`)),
+ query: buildJobsQuery({
+ type: ORGANIZATION_INTEGRATION_EXPORT_TYPES,
+ ...queryString.parse(`${search}`),
+ }),
},
};
diff --git a/src/ExportJobs/ExportJobsFilters/ExportJobsFilters.js b/src/ExportJobs/ExportJobsFilters/ExportJobsFilters.js
index ee67771..ce5ebf9 100644
--- a/src/ExportJobs/ExportJobsFilters/ExportJobsFilters.js
+++ b/src/ExportJobs/ExportJobsFilters/ExportJobsFilters.js
@@ -49,9 +49,9 @@ const typeFilterOptions = EXPORT_JOB_TYPES.map(type => ({
}));
export const ExportJobsFilters = ({
- disabled = false,
- activeFilters,
- applyFilters,
+ disabled = false,
+ activeFilters,
+ applyFilters,
}) => {
const adaptedApplyFilters = useCallback(
applyFiltersAdapter(applyFilters),
diff --git a/src/ExportJobs/apiQuery.js b/src/ExportJobs/apiQuery.js
index 0d33744..7ee2d65 100644
--- a/src/ExportJobs/apiQuery.js
+++ b/src/ExportJobs/apiQuery.js
@@ -1,36 +1,94 @@
-import { useInfiniteQuery } from 'react-query';
import queryString from 'query-string';
+import { useInfiniteQuery } from 'react-query';
import { useOkapiKy } from '@folio/stripes/core';
import {
buildDateRangeQuery,
+ CQL_AND_OPERATOR,
+ CQL_OR_OPERATOR,
makeQueryBuilder,
+ ORGANIZATION_INTEGRATION_EXPORT_TYPES,
} from '@folio/stripes-acq-components';
-const BULK_EDIT_QUERY = 'type==("BULK_EDIT_IDENTIFIERS" or "BULK_EDIT_QUERY" or "BULK_EDIT_UPDATE")';
+import { EXPORT_FILE_TYPE } from '../common/constants';
+import { EXPORT_JOB_TYPE_KEYS } from './constants';
+
+const AND_SEPARATOR = ` ${CQL_AND_OPERATOR} `;
+const OR_SEPARATOR = ` ${CQL_OR_OPERATOR} `;
const BULK_EDIT_TYPE = '"BULK_EDIT_IDENTIFIERS" or "BULK_EDIT_QUERY" or "BULK_EDIT_UPDATE"';
-const BULK_EDIT = 'BULK_EDIT';
+const EDI_ORDERS_FILE_FORMAT_KEY = 'jsonb.exportTypeSpecificParameters.vendorEdiOrdersExportConfig.fileFormat';
+
+const ORDERS_JOB_TYPES_CQL_VALUE = ORGANIZATION_INTEGRATION_EXPORT_TYPES
+ .map((t) => `"${t}"`)
+ .join(` ${CQL_OR_OPERATOR} `);
+
+const buildOrdersJobTypeQueryDict = (fileType) => ({
+ type: `${ORDERS_JOB_TYPES_CQL_VALUE}`,
+ [EDI_ORDERS_FILE_FORMAT_KEY]: `"${fileType}"`,
+});
+
+const typeQueryDict = {
+ [EXPORT_JOB_TYPE_KEYS.BULK_EDIT]: { type: `${BULK_EDIT_TYPE}` },
+ [EXPORT_JOB_TYPE_KEYS.ORDERS_CSV]: buildOrdersJobTypeQueryDict(EXPORT_FILE_TYPE.csv),
+ [EXPORT_JOB_TYPE_KEYS.ORDERS_EDI]: buildOrdersJobTypeQueryDict(EXPORT_FILE_TYPE.edi),
+};
+
+/*
+ * Function to build CQL query from an array of dictionaries (objects)
+ */
+const buildCqlQueryFromDicts = (arr) => {
+ /* Group objects by their keys to optimize the CQL query */
+ const grouped = arr.reduce((acc, obj) => {
+ const key = Object.keys(obj)
+ .sort((a, b) => a.localeCompare(b))
+ .join('_');
+
+ acc[key] = acc[key] || [];
+ acc[key].push(obj);
+
+ return acc;
+ }, {});
+
+ /* Transform grouped objects into CQL conditions */
+ const cqlParts = Object.values(grouped).map((group) => {
+ const uniqueConditions = group.reduce((acc, obj) => {
+ Object.entries(obj).forEach(([key, value]) => {
+ if (!acc[key]) {
+ acc[key] = new Set();
+ }
+ acc[key].add(value);
+ });
+
+ return acc;
+ }, {});
+
+ const conditions = Object.entries(uniqueConditions)
+ .map(([key, values]) => `${key}==(${[...values].join(OR_SEPARATOR)})`)
+ .join(AND_SEPARATOR);
+
+ return `(${conditions})`;
+ });
+
+ return cqlParts.join(OR_SEPARATOR);
+};
+
+/*
+ * Function to map job types to their CQL representation
+*/
+const mapJobTypesToCql = (types) => {
+ const queryDicts = types.map((type) => typeQueryDict[type] ?? { type });
+
+ return buildCqlQueryFromDicts(queryDicts);
+};
const buildJobsQuery = makeQueryBuilder(
'cql.allRecords=1',
- (query) => {
- return `name="*${query}*" or description="*${query}*"`;
- },
+ (query) => `name="*${query}*" or description="*${query}*"`,
'sortby name/sort.descending',
{
endTime: buildDateRangeQuery.bind(null, ['endTime']),
startTime: buildDateRangeQuery.bind(null, ['startTime']),
- type: (query) => {
- if (query === BULK_EDIT) {
- return BULK_EDIT_QUERY;
- } else if (Array.isArray(query)) {
- return `type==(${query.map(v => {
- if (v === BULK_EDIT) {
- return BULK_EDIT_TYPE;
- } else return `"${v}"`;
- }).join(' or ')})`;
- } else return `type==${query}`;
- },
+ type: (query) => mapJobTypesToCql(Array.isArray(query) ? query : [query]),
},
{
jobId: 'name',
diff --git a/src/ExportJobs/apiQuery.test.js b/src/ExportJobs/apiQuery.test.js
index c71089d..562c80b 100644
--- a/src/ExportJobs/apiQuery.test.js
+++ b/src/ExportJobs/apiQuery.test.js
@@ -1,90 +1,111 @@
-import React from 'react';
-import { QueryClient, QueryClientProvider } from 'react-query';
import { renderHook } from '@testing-library/react-hooks';
+import {
+ QueryClient,
+ QueryClientProvider,
+} from 'react-query';
-import '@folio/stripes-acq-components/test/jest/__mock__';
import { useOkapiKy } from '@folio/stripes/core';
-import {
- useExportJobsQuery,
-} from './apiQuery';
+import { useExportJobsQuery } from './apiQuery';
const queryClient = new QueryClient();
-
-// eslint-disable-next-line react/prop-types
const wrapper = ({ children }) => (
{children}
);
-describe('Export jobs api queries', () => {
- describe('useExportJobsQuery', () => {
- it('should fetch export jobs', async () => {
- const exportJobs = [{
- id: 'uuias43',
- name: '123',
- description: '# of Charges: 5',
- }];
-
- useOkapiKy.mockClear().mockReturnValue({
- get: () => ({
- json: () => ({
- jobRecords: exportJobs,
- totalRecords: 1,
- }),
+describe('useExportJobsQuery', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fetch export jobs and map them correctly', async () => {
+ const exportJobs = [{
+ id: 'uuias43',
+ name: '123',
+ description: '# of Charges: 5',
+ }];
+ const totalRecords = 1;
+
+ useOkapiKy.mockReturnValue({
+ get: jest.fn().mockReturnValue({
+ json: () => ({
+ jobRecords: exportJobs,
+ totalRecords,
}),
- });
-
- const { result, waitFor } = renderHook(() => useExportJobsQuery(
- '?limit=100&offset=0&status=SCHEDULED', {
- offset: 30,
- limit: 30,
- },
- {
- status: 'SCHEDULED',
- },
- ), { wrapper });
-
- await waitFor(() => {
- return !result.current.isLoading;
- });
-
- expect(result.current.exportJobs).toEqual(exportJobs);
+ }),
});
- it('should return total records count', async () => {
- const exportJobs = [{
- id: 'uuias43',
- name: '123',
- description: '# of Charges: 5',
- }];
- const totalRecords = 1;
-
- useOkapiKy.mockClear().mockReturnValue({
- get: () => ({
- json: () => ({
- jobRecords: exportJobs,
- totalRecords,
- }),
+ const { result, waitFor } = renderHook(() => useExportJobsQuery(
+ '?limit=100&offset=0&status=SCHEDULED&type=BULK_EDIT', {
+ offset: 30,
+ limit: 30,
+ },
+ {
+ status: 'SCHEDULED',
+ type: 'BULK_EDIT',
+ },
+ ), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(result.current.exportJobs).toEqual(exportJobs);
+ expect(result.current.totalCount).toEqual(totalRecords);
+ });
+
+ it('should handle empty result gracefully', async () => {
+ useOkapiKy.mockReturnValue({
+ get: jest.fn().mockReturnValue({
+ json: () => ({
+ jobRecords: [],
+ totalRecords: 0,
}),
- });
-
- const { result, waitFor } = renderHook(() => useExportJobsQuery(
- '?limit=100&offset=0&status=SCHEDULED', {
- offset: 30,
- limit: 30,
- },
- {
- status: 'SCHEDULED',
- },
- ), { wrapper });
-
- await waitFor(() => {
- return !result.current.isLoading;
- });
-
- expect(result.current.totalCount).toEqual(totalRecords);
+ }),
});
+
+ const { result, waitFor } = renderHook(() => useExportJobsQuery(
+ '?limit=100&offset=0', {
+ offset: 0,
+ limit: 10,
+ },
+ {},
+ ), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(result.current.exportJobs).toEqual([]);
+ });
+
+ it('should correctly call the API with generated CQL query', async () => {
+ const getMock = jest.fn().mockReturnValue({
+ json: () => ({
+ jobRecords: [],
+ totalRecords: 0,
+ }),
+ });
+
+ useOkapiKy.mockReturnValue({ get: getMock });
+
+ const { result, waitFor } = renderHook(() => useExportJobsQuery(
+ '?limit=100&offset=0&status=SCHEDULED', {
+ offset: 30,
+ limit: 30,
+ },
+ {
+ status: 'SCHEDULED',
+ },
+ ), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(getMock).toHaveBeenCalledWith(
+ 'data-export-spring/jobs',
+ expect.objectContaining({
+ searchParams: expect.objectContaining({
+ query: expect.stringContaining('status=="SCHEDULED"'),
+ }),
+ }),
+ );
});
});
diff --git a/src/ExportJobs/constants.js b/src/ExportJobs/constants.js
index 9b9b187..2aab5e9 100644
--- a/src/ExportJobs/constants.js
+++ b/src/ExportJobs/constants.js
@@ -19,7 +19,8 @@ export const EXPORT_JOB_TYPE_KEYS = {
CIRCULATION_LOG: 'CIRCULATION_LOG',
E_HOLDINGS: 'E_HOLDINGS',
BULK_EDIT: 'BULK_EDIT',
- EDIFACT_ORDERS_EXPORT: 'EDIFACT_ORDERS_EXPORT',
+ ORDERS_EDI: 'ORDERS_EDI',
+ ORDERS_CSV: 'ORDERS_CSV',
};
export const EXPORT_JOB_TYPES = [
@@ -28,7 +29,8 @@ export const EXPORT_JOB_TYPES = [
EXPORT_JOB_TYPE_KEYS.CIRCULATION_LOG,
EXPORT_JOB_TYPE_KEYS.E_HOLDINGS,
EXPORT_JOB_TYPE_KEYS.BULK_EDIT,
- EXPORT_JOB_TYPE_KEYS.EDIFACT_ORDERS_EXPORT,
+ EXPORT_JOB_TYPE_KEYS.ORDERS_EDI,
+ EXPORT_JOB_TYPE_KEYS.ORDERS_CSV,
];
export const EXPORT_JOB_TYPES_REQUEST_MAP = {
diff --git a/src/common/constants.js b/src/common/constants.js
index b988b59..9696fbc 100644
--- a/src/common/constants.js
+++ b/src/common/constants.js
@@ -13,6 +13,16 @@ export const EXPORT_JOB_STATUS_OPTIONS = Object.values(EXPORT_JOB_STATUSES).map(
label: ,
}));
+export const ORGANIZATION_INTEGRATION_TYPE = {
+ claiming: 'Claiming',
+ ordering: 'Ordering',
+};
+
+export const ORGANIZATION_INTEGRATION_TYPE_OPTIONS = Object.values(ORGANIZATION_INTEGRATION_TYPE).map((type) => ({
+ value: type,
+ label: ,
+}));
+
export const EXPORT_CONFIGS_API = 'data-export-spring/configs';
export const EXPORT_JOBS_API = 'data-export-spring/jobs';
export const MULTIPLE_EXPORTED_JOB_TYPES = [
@@ -28,3 +38,8 @@ export const EXPORTED_JOB_TYPES = [
'AUTH_HEADINGS_UPDATES',
'FAILED_LINKED_BIB_UPDATES',
];
+
+export const EXPORT_FILE_TYPE = {
+ edi: 'EDI',
+ csv: 'CSV',
+};
diff --git a/translations/ui-export-manager/en.json b/translations/ui-export-manager/en.json
index be09e70..47c6989 100644
--- a/translations/ui-export-manager/en.json
+++ b/translations/ui-export-manager/en.json
@@ -7,6 +7,7 @@
"exportJobs.search": "Search",
"exportJob": "Export job {jobId}",
+ "exportJob.download": "Download",
"exportJob.jobId": "Job ID",
"exportJob.status": "Status",
"exportJob.type": "Job type",
@@ -22,6 +23,10 @@
"exportJob.sentTo": "Sent to",
"exportJob.fileName": "File name",
+ "exportJob.integrationType": "Integration type",
+ "exportJob.integrationType.claiming": "Claims",
+ "exportJob.integrationType.ordering": "Orders",
+
"exportJob.status.SCHEDULED": "Scheduled",
"exportJob.status.IN_PROGRESS": "In progress",
"exportJob.status.SUCCESSFUL": "Successful",
@@ -29,12 +34,13 @@
"exportJob.type.CIRCULATION_LOG": "Circulation log",
"exportJob.type.BURSAR_FEES_FINES": "Bursar",
- "exportJob.type.EDIFACT_ORDERS_EXPORT": "EDIFACT orders export",
"exportJob.type.E_HOLDINGS": "eHoldings",
"exportJob.type.BULK_EDIT": "Bulk edit",
"exportJob.type.AUTH_HEADINGS_UPDATES": "Authority control",
"exportJob.type.BULK_EDIT_IDENTIFIERS": "Bulk edit identifiers",
"exportJob.type.BULK_EDIT_QUERY": "Bulk edit query",
+ "exportJob.type.ORDERS_EDI": "Orders (EDI)",
+ "exportJob.type.ORDERS_CSV": "Orders (CSV)",
"exportJob.details.type.CIRCULATION_LOG": "Circulation log",
"exportJob.details.type.BURSAR_FEES_FINES": "Bursar",