@zeniai/client-epic-state 4.19.37-betaVR2 → 4.19.37-betaVR21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/lib/epic.d.ts +2 -1
  2. package/lib/epic.js +2 -1
  3. package/lib/esm/epic.js +2 -1
  4. package/lib/esm/index.js +2 -2
  5. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +4 -8
  6. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +2 -2
  7. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +6 -1
  8. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +7 -3
  9. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.js +36 -0
  10. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +24 -15
  11. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +65 -24
  12. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +32 -32
  13. package/lib/esm/view/expenseAutomationView/payload/missingReceiptsPayload.js +25 -18
  14. package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +108 -22
  15. package/lib/esm/view/expenseAutomationView/selectors/missingReceiptsSelector.js +110 -13
  16. package/lib/index.d.ts +2 -2
  17. package/lib/index.js +36 -32
  18. package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.d.ts +2 -5
  19. package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +3 -7
  20. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +2 -2
  21. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +6 -1
  22. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +6 -2
  23. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.d.ts +8 -0
  24. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMoreBatchDetailsEpic.js +43 -0
  25. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.d.ts +3 -2
  26. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +25 -12
  27. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.d.ts +11 -3
  28. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +63 -22
  29. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.d.ts +5 -7
  30. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +28 -28
  31. package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.d.ts +17 -35
  32. package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.js +27 -20
  33. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +20 -5
  34. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +110 -23
  35. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +7 -1
  36. package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.js +110 -13
  37. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +17 -2
  38. package/package.json +2 -2
  39. package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
@@ -1,27 +1,68 @@
1
- import { of } from 'rxjs';
2
- import { catchError, debounceTime, filter, switchMap } from 'rxjs/operators';
1
+ import { from, merge, of } from 'rxjs';
2
+ import { catchError, debounceTime, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { convertToPeriod } from '../../../../commonStateTypes/timePeriod';
4
+ import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
+ import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
3
6
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
- import { toManualSearchResult, } from '../../payload/missingReceiptsPayload';
7
+ import { supportedTransactionPayloadToManualSearchResult } from '../../payload/missingReceiptsPayload';
5
8
  import { searchTransactionsForManualMatch, searchTransactionsForManualMatchFailure, searchTransactionsForManualMatchSuccess, } from '../../reducers/missingReceiptsViewReducer';
6
- export const searchTransactionsForManualMatchEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(searchTransactionsForManualMatch.match), debounceTime(300), switchMap((action) => {
7
- const { query } = action.payload;
8
- const searchQuery = JSON.stringify({
9
- search_text: query,
10
- search_fields: ['date', 'amount', 'vendor_name'],
11
- });
12
- const encodedQuery = encodeURIComponent(searchQuery);
13
- return zeniAPI
14
- .getJSON(`${zeniAPI.apiEndPoints.searchMicroServiceBaseUrl}/1.0/transaction-search?query=${encodedQuery}`)
15
- .pipe(switchMap((response) => {
16
- if (isSuccessResponse(response) && response.data != null) {
17
- const results = response.data.transactions.map(toManualSearchResult);
18
- return of(searchTransactionsForManualMatchSuccess(results));
9
+ import { toTransactionsSortKey } from '../../types/transactionsViewState';
10
+ /**
11
+ * `auto_categorized` for manual transaction search (Unmatched).
12
+ * Backend contract TBD — align with `fetchTransactionCategorizationEpic` (`selectedTab === 'autoCategorized'`)
13
+ * when the expense-automation API documents the intended filter for this flow.
14
+ */
15
+ export const MANUAL_TRANSACTION_SEARCH_AUTO_CATEGORIZED = false;
16
+ export const searchTransactionsForManualMatchEpic = (actions$, state$, zeniAPI) => {
17
+ const searchActions$ = actions$.pipe(filter(searchTransactionsForManualMatch.match));
18
+ const pagination$ = searchActions$.pipe(filter((a) => a.payload.pageToken != null));
19
+ const newSearch$ = searchActions$.pipe(filter((a) => a.payload.pageToken == null), debounceTime(300));
20
+ return merge(pagination$, newSearch$).pipe(switchMap((action) => {
21
+ const { query, pageToken } = action.payload;
22
+ const state = state$.value;
23
+ const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationMissingReceiptsViewState: { bulkUpload: { manualSearch, sortKey, sortOrder }, }, } = state;
24
+ const currentTenant = getCurrentTenant(state);
25
+ const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
26
+ if (selectedPeriod == null) {
27
+ return of(searchTransactionsForManualMatchFailure({
28
+ error: createZeniAPIStatus('Unexpected Error', 'Manual search requires a selected accounting period.'),
29
+ }));
19
30
  }
20
- return of(searchTransactionsForManualMatchFailure({
21
- error: response.status,
22
- }));
23
- }), catchError((error) => of(searchTransactionsForManualMatchFailure({
24
- error: createZeniAPIStatus('Unexpected Error', 'Search transactions for manual match errored: ' +
25
- JSON.stringify(error)),
26
- }))));
27
- }));
31
+ const period = convertToPeriod(selectedPeriod);
32
+ const queryParam = {
33
+ start_date: period.start,
34
+ end_date: period.end,
35
+ auto_categorized: MANUAL_TRANSACTION_SEARCH_AUTO_CATEGORIZED,
36
+ sort_by: toTransactionsSortKey(sortKey),
37
+ sort_order: sortOrder === 'ascending' ? 'asc' : 'desc',
38
+ page_token: pageToken ?? null,
39
+ page_size: manualSearch.pageSize,
40
+ search_text: query,
41
+ };
42
+ return zeniAPI
43
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/transactions?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
44
+ .pipe(mergeMap((response) => {
45
+ if (isSuccessResponse(response) && response.data != null) {
46
+ const { transactions, next_page_token, total_count } = response.data;
47
+ const append = pageToken != null;
48
+ const results = transactions.map(supportedTransactionPayloadToManualSearchResult);
49
+ const actionsOut = [
50
+ updateTransactions(transactions, (value) => value.transaction_id, 'merge'),
51
+ searchTransactionsForManualMatchSuccess({
52
+ append,
53
+ nextPageToken: next_page_token ?? null,
54
+ results,
55
+ totalCount: total_count,
56
+ }),
57
+ ];
58
+ return from(actionsOut);
59
+ }
60
+ return of(searchTransactionsForManualMatchFailure({
61
+ error: response.status,
62
+ }));
63
+ }), catchError((error) => of(searchTransactionsForManualMatchFailure({
64
+ error: createZeniAPIStatus('Unexpected Error', 'Search transactions for manual match errored: ' +
65
+ JSON.stringify(error)),
66
+ }))));
67
+ }));
68
+ };
@@ -1,41 +1,41 @@
1
- import { of, timer } from 'rxjs';
2
- import { catchError, filter, mergeMap, switchMap, takeUntil, takeWhile, } from 'rxjs/operators';
1
+ import { EMPTY, from, merge, of, timer } from 'rxjs';
2
+ import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
+ import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
3
4
  import { isSuccessResponse } from '../../../../responsePayload';
4
- import { toBatchStatus, } from '../../payload/missingReceiptsPayload';
5
- import { bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetails, pusherBatchStatusUpdate, updateBulkUploadBatchStatus, } from '../../reducers/missingReceiptsViewReducer';
5
+ import { extractTransactionPayloadsFromBatchFiles, toBatchDetails, } from '../../payload/missingReceiptsPayload';
6
+ import { bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, } from '../../reducers/missingReceiptsViewReducer';
7
+ /** First poll after upload; then interval while batch not resolved via Pusher or API. */
8
+ const FALLBACK_FIRST_DELAY_MS = 30000;
9
+ const FALLBACK_POLL_INTERVAL_MS = 10000;
10
+ function isBatchDetailsCompletedStatus(status) {
11
+ return status.toLowerCase() === 'completed';
12
+ }
6
13
  /**
7
- * Watches batch status using two strategies:
8
- * 1. Pusher (primary): Reacts to `pusherBatchStatusUpdate` dispatched by the UI layer
9
- * when a Pusher event arrives. Immediately fetches full details on completion.
10
- * 2. Polling (fallback): Starts a relaxed 10s poll after upload success as a safety net
11
- * in case the Pusher event is missed. Stops on completion or `clearBulkUpload`.
14
+ * On Pusher batch completion: refresh batch list (then fetchMultipleBatchDetailsEpic runs),
15
+ * and request switching to the Unmatched tab. Debounced to reduce duplicate refreshes.
12
16
  */
13
- // Pusher-driven: when Pusher delivers a completed status, fetch details
14
- export const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe(filter(pusherBatchStatusUpdate.match), filter((action) => action.payload.status === 'completed'), mergeMap(() => of(fetchBulkUploadBatchDetails())));
15
- // Polling fallback: relaxed interval as safety net
16
- const FALLBACK_POLL_INTERVAL_MS = 10000;
17
- export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(bulkUploadReceiptsSuccess.match), switchMap((action) => {
17
+ export const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe(filter(pusherBatchStatusUpdate.match), filter((action) => action.payload.status === 'completed'), debounceTime(300), mergeMap(() => from([
18
+ fetchBulkUploadBatches(true),
19
+ requestMissingReceiptsTabNavigation({ tab: 'unmatched' }),
20
+ ])));
21
+ export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(bulkUploadReceiptsSuccess.match), mergeMap((action) => {
18
22
  const { batchId } = action.payload;
19
- return timer(0, FALLBACK_POLL_INTERVAL_MS).pipe(takeUntil(actions$.pipe(filter((a) => clearBulkUpload.match(a) ||
20
- (pusherBatchStatusUpdate.match(a) &&
21
- a.payload.status === 'completed')))), switchMap(() => zeniAPI
22
- .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/receipts/batches/${batchId}/status`)
23
+ return timer(FALLBACK_FIRST_DELAY_MS, FALLBACK_POLL_INTERVAL_MS).pipe(takeUntil(merge(actions$.pipe(filter(clearBulkUpload.match)), actions$.pipe(filter(pusherBatchStatusUpdate.match), filter((a) => a.payload.batchId === batchId)), actions$.pipe(filter(fetchBulkUploadBatchDetailsSuccess.match), filter((a) => a.payload.batchId === batchId)))), switchMap(() => zeniAPI
24
+ .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
23
25
  .pipe(mergeMap((response) => {
24
26
  if (isSuccessResponse(response) && response.data != null) {
25
- const batchStatus = toBatchStatus(response.data);
26
- const actions = [
27
- updateBulkUploadBatchStatus(batchStatus),
28
- ];
29
- if (batchStatus.status === 'completed') {
30
- actions.push(fetchBulkUploadBatchDetails());
27
+ if (isBatchDetailsCompletedStatus(response.data.status)) {
28
+ const actions = [];
29
+ const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
30
+ if (transactionPayloads.length > 0) {
31
+ actions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
32
+ }
33
+ actions.push(fetchBulkUploadBatchDetailsSuccess(toBatchDetails(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
34
+ actions.push(fetchBulkUploadBatches(true));
35
+ actions.push(requestMissingReceiptsTabNavigation({ tab: 'unmatched' }));
36
+ return from(actions);
31
37
  }
32
- return actions;
33
- }
34
- return [];
35
- }), catchError(() => of()))), takeWhile((action) => {
36
- if (fetchBulkUploadBatchDetails.match(action)) {
37
- return false;
38
38
  }
39
- return true;
40
- }, true));
39
+ return EMPTY;
40
+ }), catchError(() => of()))));
41
41
  }));
@@ -1,14 +1,20 @@
1
+ import { toURL } from '../../../commonPayloadTypes/urlPayload';
2
+ import { getTransactionPayloadAmount, } from '../../../entity/transaction/payloadTypes/transactionPayload';
1
3
  // -- Converter functions --
2
- export function toBatchFile(payload) {
4
+ export function toBatchFile(payload, filesEndPoint) {
3
5
  return {
4
6
  attachmentId: payload.attachment_id,
5
7
  candidates: payload.candidates?.map((c) => ({
6
8
  transactionId: c.transaction_id,
7
9
  matchScore: c.match_score,
8
10
  })),
11
+ fileDownloadUrl: payload.file_id != null && filesEndPoint != null
12
+ ? `${filesEndPoint}/1.0/files/${payload.file_id}?query=${encodeURIComponent(JSON.stringify({ download_file: true }))}`
13
+ : null,
9
14
  fileId: payload.file_id,
10
15
  filePreviewUrl: payload.file_preview_url,
11
16
  filename: payload.filename,
17
+ matchSource: payload.match_source,
12
18
  matchedTransactionId: payload.matched_transaction?.transaction_id,
13
19
  status: payload.status,
14
20
  };
@@ -39,11 +45,12 @@ export function extractTransactionPayloadsFromBatchFiles(files) {
39
45
  function matchedTransactionPayloadToSupportedTransactionPayload(payload) {
40
46
  return {
41
47
  transaction_id: payload.transaction_id,
42
- transaction_type: payload.transaction_type,
43
- transaction_date: payload.transaction_date,
48
+ transaction_type: payload.transaction_type ?? 'expense',
49
+ transaction_date: payload.transaction_date ?? '',
44
50
  currency_code: payload.currency_code,
45
51
  total_amount: payload.amount,
46
52
  vendor_name: payload.vendor_name,
53
+ logo: payload.vendor_logo == null ? undefined : toURL(payload.vendor_logo),
47
54
  };
48
55
  }
49
56
  export function toBatchSummary(payload) {
@@ -55,22 +62,19 @@ export function toBatchSummary(payload) {
55
62
  total: payload.total,
56
63
  };
57
64
  }
58
- export function toBatchStatus(payload) {
59
- return {
60
- batchId: payload.batch_id,
61
- progress: payload.progress,
62
- status: payload.status,
63
- };
64
- }
65
- export function toBatchDetails(payload) {
65
+ export function toBatchDetails(payload, filesEndPoint) {
66
66
  return {
67
67
  batchId: payload.batch_id,
68
68
  createdAt: payload.created_at,
69
- files: payload.files.map(toBatchFile),
69
+ files: payload.files.map((f) => toBatchFile(f, filesEndPoint)),
70
70
  status: payload.status,
71
71
  summary: toBatchSummary(payload.summary),
72
72
  };
73
73
  }
74
+ /** Batch list `status` is a string from the API — compare case-insensitively. */
75
+ export function isBatchListStatusCompleted(status) {
76
+ return status.toLowerCase() === 'completed';
77
+ }
74
78
  export function toBatchListItem(payload) {
75
79
  return {
76
80
  batchId: payload.batch_id,
@@ -83,15 +87,18 @@ export function toBatchListItem(payload) {
83
87
  totalFiles: payload.total_files,
84
88
  };
85
89
  }
86
- export function toManualSearchResult(payload) {
90
+ /** Maps expense-automation transaction rows to manual-search list rows (Unmatched tab). */
91
+ export function supportedTransactionPayloadToManualSearchResult(payload) {
92
+ const amountPayload = getTransactionPayloadAmount(payload);
87
93
  return {
88
- amount: payload.amount,
89
- currencyCode: payload.currency,
90
- entityId: payload.entity_id,
91
- memo: payload.memo,
94
+ amount: amountPayload.amount,
95
+ currencyCode: amountPayload.currencyCode,
96
+ entityId: payload.vendor_id ?? payload.customer_id ?? '',
97
+ memo: payload.transaction_memo ?? '',
92
98
  transactionDate: payload.transaction_date,
93
99
  transactionId: payload.transaction_id,
94
100
  transactionType: payload.transaction_type,
95
- vendorName: payload.vendor_name,
101
+ vendorName: payload.vendor_name ?? '',
102
+ vendorLogo: toURL(payload.logo)?.href,
96
103
  };
97
104
  }
@@ -1,7 +1,9 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
+ export const getCompletedTransactionsCacheKey = (periodId, sortKey, sortOrder, subTab) => `completed-${subTab}-${sortKey}-${sortOrder === 'ascending' ? 'asc' : 'desc'}-${periodId}`;
3
4
  export const initialBulkUploadState = {
4
5
  batchDetailsById: {},
6
+ batchDetailsPaginationState: { fetchState: 'Not-Started', error: undefined },
5
7
  batchListByPeriod: {},
6
8
  batchListFetchState: { fetchState: 'Not-Started', error: undefined },
7
9
  batchStatusById: {},
@@ -10,10 +12,15 @@ export const initialBulkUploadState = {
10
12
  confirmMatchStatus: { fetchState: 'Not-Started', error: undefined },
11
13
  currentBatchId: undefined,
12
14
  currentResultsTab: 'unmatched',
15
+ failedBatchDetailIds: [],
13
16
  manualSearch: {
14
17
  fetchState: { fetchState: 'Not-Started', error: undefined },
18
+ isLoadingMore: false,
19
+ nextPageToken: null,
20
+ pageSize: 25,
15
21
  results: [],
16
22
  searchQuery: '',
23
+ totalCount: 0,
17
24
  },
18
25
  phase: 'idle',
19
26
  sortKey: 'date',
@@ -23,6 +30,7 @@ export const initialBulkUploadState = {
23
30
  };
24
31
  export const initialState = {
25
32
  bulkUpload: initialBulkUploadState,
33
+ pendingMissingReceiptsTabNavigation: null,
26
34
  uploadReceiptStatusById: {},
27
35
  refreshStatus: {
28
36
  fetchState: 'Not-Started',
@@ -175,14 +183,16 @@ const expenseAutomationMissingReceiptsView = createSlice({
175
183
  };
176
184
  draft.bulkUpload.phase = 'idle';
177
185
  },
178
- updateBulkUploadBatchStatus(draft, action) {
179
- const batchStatus = action.payload;
180
- draft.bulkUpload.batchStatusById[batchStatus.batchId] = batchStatus;
181
- },
182
186
  pusherBatchStatusUpdate(draft, action) {
183
187
  const batchStatus = action.payload;
184
188
  draft.bulkUpload.batchStatusById[batchStatus.batchId] = batchStatus;
185
189
  },
190
+ requestMissingReceiptsTabNavigation(draft, action) {
191
+ draft.pendingMissingReceiptsTabNavigation = action.payload.tab;
192
+ },
193
+ clearMissingReceiptsTabNavigation(draft) {
194
+ draft.pendingMissingReceiptsTabNavigation = null;
195
+ },
186
196
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
187
197
  fetchBulkUploadBatchDetails(_draft) {
188
198
  // trigger epic, no state change needed
@@ -199,6 +209,58 @@ const expenseAutomationMissingReceiptsView = createSlice({
199
209
  storeBatchDetails(draft, action) {
200
210
  const details = action.payload;
201
211
  draft.bulkUpload.batchDetailsById[details.batchId] = details;
212
+ /**
213
+ * Multi-batch fetch paths use `storeBatchDetails` (not `fetchBulkUploadBatchDetailsSuccess`).
214
+ * Without this, `phase` stays `matching` after upload forever and the Unmatched tab keeps
215
+ * showing the skeleton (`bulkUploadPhase === 'matching'` in web-components).
216
+ */
217
+ if (draft.bulkUpload.phase === 'matching' &&
218
+ draft.bulkUpload.currentBatchId === details.batchId) {
219
+ draft.bulkUpload.phase = 'completed';
220
+ }
221
+ },
222
+ batchDetailFetchFailed(draft, action) {
223
+ const { batchId } = action.payload;
224
+ if (!draft.bulkUpload.failedBatchDetailIds.includes(batchId)) {
225
+ draft.bulkUpload.failedBatchDetailIds.push(batchId);
226
+ }
227
+ if (draft.bulkUpload.phase === 'matching' &&
228
+ draft.bulkUpload.currentBatchId === batchId) {
229
+ draft.bulkUpload.phase = 'completed';
230
+ }
231
+ },
232
+ setInitialBatchDetailsLoading(draft) {
233
+ draft.bulkUpload.batchDetailsPaginationState = {
234
+ fetchState: 'In-Progress',
235
+ error: undefined,
236
+ };
237
+ },
238
+ fetchMoreBatchDetails(_draft) {
239
+ _draft.bulkUpload.batchDetailsPaginationState = {
240
+ fetchState: 'In-Progress',
241
+ error: undefined,
242
+ };
243
+ },
244
+ fetchMoreBatchDetailsComplete(draft) {
245
+ draft.bulkUpload.batchDetailsPaginationState = {
246
+ fetchState: 'Completed',
247
+ error: undefined,
248
+ };
249
+ const currentId = draft.bulkUpload.currentBatchId;
250
+ const currentDetails = currentId != null
251
+ ? draft.bulkUpload.batchDetailsById[currentId]
252
+ : undefined;
253
+ if (draft.bulkUpload.phase === 'matching' &&
254
+ currentId != null &&
255
+ currentDetails?.status === 'completed') {
256
+ draft.bulkUpload.phase = 'completed';
257
+ }
258
+ },
259
+ fetchMoreBatchDetailsFailure(draft, action) {
260
+ draft.bulkUpload.batchDetailsPaginationState = {
261
+ fetchState: 'Error',
262
+ error: action.payload.error,
263
+ };
202
264
  },
203
265
  fetchBulkUploadBatches: {
204
266
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -273,7 +335,6 @@ const expenseAutomationMissingReceiptsView = createSlice({
273
335
  },
274
336
  setBulkUploadCompletedSubTab(draft, action) {
275
337
  draft.bulkUpload.completedSubTab = action.payload;
276
- draft.bulkUpload.completedTransactionsByPeriod = {};
277
338
  },
278
339
  setBulkUploadSortConfig(draft, action) {
279
340
  draft.bulkUpload.sortKey = action.payload.sortKey;
@@ -281,28 +342,51 @@ const expenseAutomationMissingReceiptsView = createSlice({
281
342
  },
282
343
  clearBulkUpload(draft) {
283
344
  Object.assign(draft.bulkUpload, initialBulkUploadState);
345
+ draft.pendingMissingReceiptsTabNavigation = null;
284
346
  },
285
347
  searchTransactionsForManualMatch: {
286
348
  reducer(draft, action) {
287
- draft.bulkUpload.manualSearch.searchQuery = action.payload.query;
288
- draft.bulkUpload.manualSearch.fetchState = {
289
- fetchState: 'In-Progress',
290
- error: undefined,
291
- };
292
- draft.bulkUpload.manualSearch.results = [];
349
+ const { query, pageToken } = action.payload;
350
+ const isLoadMore = pageToken != null;
351
+ draft.bulkUpload.manualSearch.searchQuery = query;
352
+ if (isLoadMore) {
353
+ draft.bulkUpload.manualSearch.isLoadingMore = true;
354
+ }
355
+ else {
356
+ draft.bulkUpload.manualSearch.isLoadingMore = false;
357
+ draft.bulkUpload.manualSearch.results = [];
358
+ draft.bulkUpload.manualSearch.nextPageToken = null;
359
+ draft.bulkUpload.manualSearch.totalCount = 0;
360
+ draft.bulkUpload.manualSearch.fetchState = {
361
+ fetchState: 'In-Progress',
362
+ error: undefined,
363
+ };
364
+ }
293
365
  },
294
- prepare(query) {
295
- return { payload: { query } };
366
+ prepare(query, pageToken) {
367
+ return { payload: { query, pageToken: pageToken ?? undefined } };
296
368
  },
297
369
  },
298
370
  searchTransactionsForManualMatchSuccess(draft, action) {
299
- draft.bulkUpload.manualSearch.results = action.payload;
371
+ const { append, nextPageToken, results, totalCount } = action.payload;
372
+ if (append) {
373
+ const merged = [...draft.bulkUpload.manualSearch.results, ...results];
374
+ const byId = new Map(merged.map((r) => [r.transactionId, r]));
375
+ draft.bulkUpload.manualSearch.results = Array.from(byId.values());
376
+ }
377
+ else {
378
+ draft.bulkUpload.manualSearch.results = results;
379
+ }
380
+ draft.bulkUpload.manualSearch.nextPageToken = nextPageToken;
381
+ draft.bulkUpload.manualSearch.totalCount = totalCount;
382
+ draft.bulkUpload.manualSearch.isLoadingMore = false;
300
383
  draft.bulkUpload.manualSearch.fetchState = {
301
384
  fetchState: 'Completed',
302
385
  error: undefined,
303
386
  };
304
387
  },
305
388
  searchTransactionsForManualMatchFailure(draft, action) {
389
+ draft.bulkUpload.manualSearch.isLoadingMore = false;
306
390
  draft.bulkUpload.manualSearch.fetchState = {
307
391
  fetchState: 'Error',
308
392
  error: action.payload.error,
@@ -328,15 +412,16 @@ const expenseAutomationMissingReceiptsView = createSlice({
328
412
  }
329
413
  }
330
414
  },
331
- prepare(pageToken) {
332
- return { payload: { pageToken } };
415
+ prepare(pageToken, cacheOverride) {
416
+ return { payload: { pageToken, cacheOverride } };
333
417
  },
334
418
  },
335
419
  fetchCompletedTransactionsSuccess(draft, action) {
336
420
  const { transactionIds, nextPageToken, totalCount, selectedPeriod, isInitialLoad } = action.payload;
337
421
  const periodId = toMonthYearPeriodId(selectedPeriod);
422
+ const cacheKey = getCompletedTransactionsCacheKey(periodId, draft.bulkUpload.sortKey, draft.bulkUpload.sortOrder, draft.bulkUpload.completedSubTab);
338
423
  if (isInitialLoad) {
339
- draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
424
+ draft.bulkUpload.completedTransactionsByPeriod[cacheKey] = {
340
425
  transactionIds,
341
426
  nextPageToken,
342
427
  totalCount,
@@ -344,7 +429,7 @@ const expenseAutomationMissingReceiptsView = createSlice({
344
429
  };
345
430
  }
346
431
  else {
347
- const existing = draft.bulkUpload.completedTransactionsByPeriod[periodId];
432
+ const existing = draft.bulkUpload.completedTransactionsByPeriod[cacheKey];
348
433
  if (existing != null) {
349
434
  existing.transactionIds.push(...transactionIds);
350
435
  existing.nextPageToken = nextPageToken;
@@ -352,7 +437,7 @@ const expenseAutomationMissingReceiptsView = createSlice({
352
437
  existing.fetchState = { fetchState: 'Completed', error: undefined };
353
438
  }
354
439
  else {
355
- draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
440
+ draft.bulkUpload.completedTransactionsByPeriod[cacheKey] = {
356
441
  transactionIds,
357
442
  nextPageToken,
358
443
  totalCount,
@@ -363,7 +448,8 @@ const expenseAutomationMissingReceiptsView = createSlice({
363
448
  },
364
449
  fetchCompletedTransactionsFailure(draft, action) {
365
450
  const periodId = toMonthYearPeriodId(action.payload.selectedPeriod);
366
- const existing = draft.bulkUpload.completedTransactionsByPeriod[periodId];
451
+ const cacheKey = getCompletedTransactionsCacheKey(periodId, draft.bulkUpload.sortKey, draft.bulkUpload.sortOrder, draft.bulkUpload.completedSubTab);
452
+ const existing = draft.bulkUpload.completedTransactionsByPeriod[cacheKey];
367
453
  if (existing != null) {
368
454
  existing.fetchState = {
369
455
  fetchState: 'Error',
@@ -371,7 +457,7 @@ const expenseAutomationMissingReceiptsView = createSlice({
371
457
  };
372
458
  }
373
459
  else {
374
- draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
460
+ draft.bulkUpload.completedTransactionsByPeriod[cacheKey] = {
375
461
  fetchState: { fetchState: 'Error', error: action.payload.error },
376
462
  nextPageToken: null,
377
463
  totalCount: 0,
@@ -383,7 +469,7 @@ const expenseAutomationMissingReceiptsView = createSlice({
383
469
  });
384
470
  export const { fetchMissingReceipts, fetchMissingReceiptsSuccess, fetchMissingReceiptsFailure, uploadMissingReceiptSuccess, updateMissingReceiptUploadState, updateMissingReceiptsUIState, markMissingReceiptAsDone, clearExpenseAutomationMissingReceiptsView,
385
471
  // Bulk Upload
386
- bulkUploadReceipts, updateBulkUploadProgress, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, updateBulkUploadBatchStatus, pusherBatchStatusUpdate, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, storeBatchDetails, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults,
472
+ bulkUploadReceipts, updateBulkUploadProgress, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, clearMissingReceiptsTabNavigation, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, storeBatchDetails, batchDetailFetchFailed, setInitialBatchDetailsLoading, fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults,
387
473
  // Completed Transactions
388
474
  fetchCompletedTransactions, fetchCompletedTransactionsSuccess, fetchCompletedTransactionsFailure, } = expenseAutomationMissingReceiptsView.actions;
389
475
  export default expenseAutomationMissingReceiptsView.reducer;