@zeniai/client-epic-state 5.0.72 → 5.0.74

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 (163) hide show
  1. package/lib/commonStateTypes/cacheOverrideFetchReducer.d.ts +17 -0
  2. package/lib/commonStateTypes/cacheOverrideFetchReducer.js +20 -0
  3. package/lib/commonStateTypes/filterPrimitives.d.ts +23 -0
  4. package/lib/commonStateTypes/filterPrimitives.js +21 -0
  5. package/lib/commonStateTypes/pusherActions.d.ts +11 -0
  6. package/lib/commonStateTypes/pusherActions.js +6 -0
  7. package/lib/coreEpics.js +1 -1
  8. package/lib/entity/account/accountSelector.d.ts +27 -1
  9. package/lib/entity/account/accountSelector.js +47 -1
  10. package/lib/entity/account/accountState.d.ts +6 -1
  11. package/lib/entity/account/accountState.js +11 -4
  12. package/lib/entity/account/subAccountSelector.d.ts +1 -1
  13. package/lib/entity/aiAccountantCustomer/aiAccountantCustomerPayload.js +1 -3
  14. package/lib/entity/aiAccountantCustomer/aiAccountantCustomerReducer.js +1 -2
  15. package/lib/entity/aiAccountantCustomer/aiAccountantCustomerSelector.d.ts +1 -1
  16. package/lib/entity/chargeCard/chargeCardReducer.d.ts +19 -2
  17. package/lib/entity/chargeCard/chargeCardReducer.js +35 -2
  18. package/lib/entity/chargeCardTransaction/chargeCardTransactionReducer.d.ts +14 -1
  19. package/lib/entity/chargeCardTransaction/chargeCardTransactionReducer.js +36 -2
  20. package/lib/entity/class/classSelector.d.ts +35 -0
  21. package/lib/entity/class/classSelector.js +50 -0
  22. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksPayload.d.ts +2 -0
  23. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksPayload.js +2 -0
  24. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksState.d.ts +2 -0
  25. package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +1 -1
  26. package/lib/entity/sectionProjectView/sectionProjectViewSelector.js +1 -1
  27. package/lib/entity/tenant/SessionManager.js +6 -3
  28. package/lib/entity/tenant/clearAllEpic.d.ts +2 -2
  29. package/lib/entity/tenant/clearAllEpic.js +2 -2
  30. package/lib/entity/tenant/epic/saveAPIKeyConnectionEpic.js +6 -2
  31. package/lib/entity/tenant/epic/saveOAuthConnectionEpic.js +6 -2
  32. package/lib/entity/tenant/tenantReducer.js +6 -1
  33. package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +3 -1
  34. package/lib/epic.d.ts +9 -9
  35. package/lib/epic.js +10 -10
  36. package/lib/esm/commonStateTypes/cacheOverrideFetchReducer.js +16 -0
  37. package/lib/esm/commonStateTypes/filterPrimitives.js +20 -0
  38. package/lib/esm/commonStateTypes/pusherActions.js +3 -0
  39. package/lib/esm/coreEpics.js +1 -1
  40. package/lib/esm/entity/account/accountSelector.js +46 -1
  41. package/lib/esm/entity/account/accountState.js +7 -1
  42. package/lib/esm/entity/aiAccountantCustomer/aiAccountantCustomerPayload.js +1 -3
  43. package/lib/esm/entity/aiAccountantCustomer/aiAccountantCustomerReducer.js +1 -2
  44. package/lib/esm/entity/chargeCard/chargeCardReducer.js +34 -1
  45. package/lib/esm/entity/chargeCardTransaction/chargeCardTransactionReducer.js +35 -1
  46. package/lib/esm/entity/class/classSelector.js +48 -0
  47. package/lib/esm/entity/monthEndCloseChecks/monthEndCloseChecksPayload.js +2 -0
  48. package/lib/esm/entity/sectionProjectView/sectionProjectViewSelector.js +1 -1
  49. package/lib/esm/entity/tenant/SessionManager.js +4 -1
  50. package/lib/esm/entity/tenant/clearAllEpic.js +2 -2
  51. package/lib/esm/entity/tenant/epic/saveAPIKeyConnectionEpic.js +6 -2
  52. package/lib/esm/entity/tenant/epic/saveOAuthConnectionEpic.js +6 -2
  53. package/lib/esm/entity/tenant/tenantReducer.js +6 -1
  54. package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +3 -1
  55. package/lib/esm/epic.js +10 -10
  56. package/lib/esm/index.js +26 -16
  57. package/lib/esm/reducer.js +2 -2
  58. package/lib/esm/view/aiAccountantView/epics/fetchAiAccountantCustomersEpic.js +2 -5
  59. package/lib/esm/view/aiAccountantView/epics/fetchAiAccountantJobsEpic.js +2 -5
  60. package/lib/esm/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +4 -1
  61. package/lib/esm/view/companyHealthMetricView/companyHealthMetricViewSelector.js +1 -1
  62. package/lib/esm/view/companyTaskManagerView/companyTaskManagerViewSelector.js +1 -1
  63. package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +1 -1
  64. package/lib/esm/view/companyView/helpers/cockpitHelpers.js +1 -1
  65. package/lib/esm/view/companyView/selector/companyManagementViewSelector.js +1 -1
  66. package/lib/esm/view/companyView/selector/companyPortfolioViewSelector.js +1 -1
  67. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +2 -2
  68. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +2 -1
  69. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/restoreBulkUploadAutomatchingOnMountEpic.js +8 -6
  70. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +7 -1
  71. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +11 -1
  72. package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +2 -1
  73. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +137 -1
  74. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +89 -13
  75. package/lib/esm/view/expenseAutomationView/transactionFilterHelpers.js +258 -0
  76. package/lib/esm/view/expenseAutomationView/types/completedSubTab.js +17 -0
  77. package/lib/esm/view/expenseAutomationView/types/missingReceiptsViewState.js +0 -2
  78. package/lib/esm/view/financeStatement/financeStatementSelector.js +1 -1
  79. package/lib/esm/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.js +3 -2
  80. package/lib/esm/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.js +3 -2
  81. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/chargeCardDetailReducer.js +55 -18
  82. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailEpic.js +16 -2
  83. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailPageEpic.js +5 -4
  84. package/lib/esm/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.js +26 -9
  85. package/lib/esm/view/spendManagement/chargeCards/chargeCardList/fetchChargeCardListPageEpic.js +9 -6
  86. package/lib/esm/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.js +3 -2
  87. package/lib/esm/view/spendManagement/chargeCards/chargeCardPaymentHistory/fetchChargeCardPaymentPageEpic.js +1 -1
  88. package/lib/esm/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.js +7 -3
  89. package/lib/esm/view/spendManagement/spendManagementFilterHelpers.js +4 -2
  90. package/lib/esm/view/subscriptionView/subscriptionViewSelector.js +1 -1
  91. package/lib/esm/view/taskManager/cannedResponsesView/epics/fetchCannedResponsesEpic.js +1 -1
  92. package/lib/esm/view/taskManager/cannedResponsesView/epics/saveCannedResponseEpic.js +1 -1
  93. package/lib/esm/view/taskManager/taskDetailView/epics/unsnoozeTaskEpic.js +1 -1
  94. package/lib/esm/view/taskManager/taskListView/taskListReducer.js +1 -1
  95. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +7 -1
  96. package/lib/index.d.ts +25 -19
  97. package/lib/index.js +81 -58
  98. package/lib/reducer.d.ts +2 -2
  99. package/lib/reducer.js +2 -2
  100. package/lib/view/aiAccountantView/aiAccountantViewPayload.d.ts +2 -2
  101. package/lib/view/aiAccountantView/aiAccountantViewReducer.d.ts +1 -1
  102. package/lib/view/aiAccountantView/epics/fetchAiAccountantCustomersEpic.js +2 -5
  103. package/lib/view/aiAccountantView/epics/fetchAiAccountantJobsEpic.js +2 -5
  104. package/lib/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +4 -1
  105. package/lib/view/companyHealthMetricView/companyHealthMetricViewSelector.js +1 -1
  106. package/lib/view/companyTaskManagerView/companyTaskManagerViewSelector.d.ts +1 -1
  107. package/lib/view/companyTaskManagerView/companyTaskManagerViewSelector.js +1 -1
  108. package/lib/view/companyView/helpers/cockpitHelpers.js +1 -1
  109. package/lib/view/companyView/selector/companyManagementViewSelector.js +1 -1
  110. package/lib/view/companyView/selector/companyPortfolioViewSelector.js +1 -1
  111. package/lib/view/companyView/types/cockpitTypes.d.ts +1 -1
  112. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +2 -2
  113. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +2 -1
  114. package/lib/view/expenseAutomationView/epics/missingReceipts/restoreBulkUploadAutomatchingOnMountEpic.js +8 -6
  115. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +7 -1
  116. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.d.ts +2 -2
  117. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +10 -0
  118. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  119. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +21 -0
  120. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +2 -1
  121. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +2 -1
  122. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +14 -4
  123. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +138 -2
  124. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +2 -1
  125. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +14 -0
  126. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +4 -1
  127. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +91 -13
  128. package/lib/view/expenseAutomationView/transactionFilterHelpers.d.ts +66 -0
  129. package/lib/view/expenseAutomationView/transactionFilterHelpers.js +262 -0
  130. package/lib/view/expenseAutomationView/types/completedSubTab.d.ts +10 -0
  131. package/lib/view/expenseAutomationView/types/completedSubTab.js +21 -0
  132. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +1 -2
  133. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.js +1 -4
  134. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +43 -2
  135. package/lib/view/financeStatement/financeStatementSelector.js +1 -1
  136. package/lib/view/profitAndLossProjectView/profitAndLossProjectViewSelectorTypes.d.ts +1 -1
  137. package/lib/view/spendManagement/billPay/billList/billListState.d.ts +1 -1
  138. package/lib/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.d.ts +3 -1
  139. package/lib/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.js +3 -2
  140. package/lib/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.d.ts +3 -1
  141. package/lib/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.js +3 -2
  142. package/lib/view/spendManagement/chargeCards/chargeCardDetail/chargeCardDetailReducer.js +55 -18
  143. package/lib/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailEpic.js +16 -2
  144. package/lib/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailPageEpic.js +5 -4
  145. package/lib/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.d.ts +18 -3
  146. package/lib/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.js +27 -10
  147. package/lib/view/spendManagement/chargeCards/chargeCardList/fetchChargeCardListPageEpic.js +9 -6
  148. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistory.d.ts +1 -1
  149. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.d.ts +3 -1
  150. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.js +3 -2
  151. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/fetchChargeCardPaymentPageEpic.js +1 -1
  152. package/lib/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.d.ts +3 -1
  153. package/lib/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.js +7 -3
  154. package/lib/view/spendManagement/reimbursement/remiListView/remiListState.d.ts +1 -1
  155. package/lib/view/spendManagement/spendManagementFilterHelpers.d.ts +12 -3
  156. package/lib/view/spendManagement/spendManagementFilterHelpers.js +4 -2
  157. package/lib/view/subscriptionView/subscriptionViewSelector.js +1 -1
  158. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  159. package/lib/view/taskManager/taskListView/taskListReducer.js +1 -1
  160. package/lib/view/transactionDetail/transactionDetailSelector.js +7 -1
  161. package/lib/view/zeniOAuthView/zeniOAuthSelector.d.ts +1 -1
  162. package/package.json +9 -4
  163. package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
@@ -7,9 +7,7 @@ import { fetchAiAccountantCustomers, updateAiAccountantCustomers, updateAiAccoun
7
7
  export const fetchAiAccountantCustomersEpic = (actions$, _, zeniAPI) => actions$.pipe(filter(fetchAiAccountantCustomers.match), switchMap(() => {
8
8
  const base = `${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/cockpit-panels`;
9
9
  const url = `${base}/ai-accountant/customers`;
10
- return zeniAPI
11
- .getJSON(url)
12
- .pipe(mergeMap((response) => {
10
+ return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
13
11
  if (isSuccessResponse(response)) {
14
12
  const customers = response.data?.customers ?? [];
15
13
  const users = response.data?.users ?? [];
@@ -22,6 +20,5 @@ export const fetchAiAccountantCustomersEpic = (actions$, _, zeniAPI) => actions$
22
20
  else {
23
21
  return of(updateAiAccountantCustomersFailure(response.status));
24
22
  }
25
- }), catchError((error) => of(updateAiAccountantCustomersFailure(createZeniAPIStatus('Unexpected error', 'AI Accountant customers fetch failed: ' +
26
- JSON.stringify(error))))));
23
+ }), catchError((error) => of(updateAiAccountantCustomersFailure(createZeniAPIStatus('Unexpected error', 'AI Accountant customers fetch failed: ' + JSON.stringify(error))))));
27
24
  }));
@@ -20,9 +20,7 @@ export const fetchAiAccountantJobsEpic = (actions$, state$, zeniAPI) => actions$
20
20
  }
21
21
  const base = `${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/cockpit-panels`;
22
22
  const url = `${base}/ai-accountant/jobs?${params.toString()}`;
23
- return zeniAPI
24
- .getJSON(url)
25
- .pipe(mergeMap((response) => {
23
+ return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
26
24
  if (isSuccessResponse(response)) {
27
25
  const jobs = response.data?.jobs ?? [];
28
26
  const users = response.data?.users ?? [];
@@ -41,7 +39,6 @@ export const fetchAiAccountantJobsEpic = (actions$, state$, zeniAPI) => actions$
41
39
  }
42
40
  }), catchError((error) => of(updateAiAccountantJobsFailure({
43
41
  tenantId,
44
- error: createZeniAPIStatus('Unexpected error', 'AI Accountant jobs fetch failed: ' +
45
- JSON.stringify(error)),
42
+ error: createZeniAPIStatus('Unexpected error', 'AI Accountant jobs fetch failed: ' + JSON.stringify(error)),
46
43
  }))));
47
44
  }));
@@ -51,7 +51,10 @@ export const triggerAiAccountantJobEpic = (actions$, state$, zeniAPI) => actions
51
51
  messageText: 'failed',
52
52
  type: 'error',
53
53
  variables: [
54
- { variableName: '__reason__', variableValue: 'Unexpected error' },
54
+ {
55
+ variableName: '__reason__',
56
+ variableValue: 'Unexpected error',
57
+ },
55
58
  ],
56
59
  }),
57
60
  ])));
@@ -291,7 +291,7 @@ const applyAdvancedFiltersOnCompaniesList = (allCompanies, filters, filteredComp
291
291
  const key = currentFilter.field;
292
292
  const categoryValue = getCategoryValueForCompany(key, company);
293
293
  if (categoryValue == null) {
294
- if (currentFilter.matchingOperator === 'not-equal') {
294
+ if (currentFilter.matchingOperator === 'not_equal') {
295
295
  return true;
296
296
  }
297
297
  return false;
@@ -4,7 +4,7 @@ import { getTagsByIds } from '../../entity/tag/tagSelector';
4
4
  import { getTaskById } from '../../entity/task/taskSelector';
5
5
  import { getUserByUserId, getUsersByUserIds, } from '../../entity/user/userSelector';
6
6
  export const getCompanyTaskManagerView = createSelector((state) => state.companyTaskManagerViewState, (state) => state.companyState, (state) => state.userState, (state) => state.taskState, (state) => state.tagState, (companyTaskManagerViewState, companyState, userState, taskState, tagState) => {
7
- const { taskIds, uiState, fetchState, error, totalTaskCount, metrics, metricsFetchState } = companyTaskManagerViewState;
7
+ const { taskIds, uiState, fetchState, error, totalTaskCount, metrics, metricsFetchState, } = companyTaskManagerViewState;
8
8
  const tasksList = [];
9
9
  taskIds.forEach((taskId) => {
10
10
  const task = getTaskById(taskState, taskId);
@@ -1,6 +1,6 @@
1
1
  import { of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
- import { isSuccessResponse, } from '../../../responsePayload';
3
+ import { isSuccessResponse } from '../../../responsePayload';
4
4
  import { fetchTaskManagerMetrics, updateTaskManagerMetrics, updateTaskManagerMetricsOnFailure, } from '../companyTaskManagerViewReducer';
5
5
  export const fetchTaskManagerMetricsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchTaskManagerMetrics.match), switchMap(() => {
6
6
  const query = JSON.stringify({ is_filter_by_logged_in_user: true });
@@ -48,7 +48,7 @@ export const filterCompanyOnCategoriesWithSingleValue = (categoryValue, currentF
48
48
  return filterCompanyOnRangeCategoryType(categoryValue, currentFilter);
49
49
  }
50
50
  // if filter category is not annualRecurringRevenue OR monthlyRecurringRevenue
51
- if (currentFilter.matchingOperator === 'not-equal') {
51
+ if (currentFilter.matchingOperator === 'not_equal') {
52
52
  return currentFilter.values.indexOf(categoryValue) === -1;
53
53
  }
54
54
  return currentFilter.values.indexOf(categoryValue) > -1;
@@ -150,7 +150,7 @@ const applyAdvancedFiltersOnCompaniesList = (allCompanies, filters, filteredComp
150
150
  const key = currentFilter.field;
151
151
  const categoryValue = getCategoryValueForCompany(key, company);
152
152
  if (categoryValue == null) {
153
- if (currentFilter.matchingOperator === 'not-equal') {
153
+ if (currentFilter.matchingOperator === 'not_equal') {
154
154
  return true;
155
155
  }
156
156
  return false;
@@ -116,7 +116,7 @@ const applyAdvancedFiltersOnCompaniesList = (allCompanies, filters, filteredComp
116
116
  const key = currentFilter.field;
117
117
  const categoryValue = getCategoryValueForCompany(key, company);
118
118
  if (categoryValue == null) {
119
- if (currentFilter.matchingOperator === 'not-equal') {
119
+ if (currentFilter.matchingOperator === 'not_equal') {
120
120
  return true;
121
121
  }
122
122
  return false;
@@ -3,10 +3,10 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
4
4
  import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
5
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
- import { toBatchListItem, } from '../../payload/missingReceiptsPayload';
7
- import { fetchBulkUploadBatches, fetchBulkUploadBatchesFailure, fetchBulkUploadBatchesSuccess, } from '../../reducers/missingReceiptsViewReducer';
8
6
  import { BULK_UPLOAD_BATCH_LIST_DATE_RANGE_DAYS } from '../../helpers/bulkUploadTiming';
9
7
  import { rollingCalendarDateRangeInclusive } from '../../helpers/rollingCalendarDateRangeInclusive';
8
+ import { toBatchListItem, } from '../../payload/missingReceiptsPayload';
9
+ import { fetchBulkUploadBatches, fetchBulkUploadBatchesFailure, fetchBulkUploadBatchesSuccess, } from '../../reducers/missingReceiptsViewReducer';
10
10
  export const fetchBulkUploadBatchesEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchBulkUploadBatches.match), switchMap((action) => {
11
11
  const { cacheOverride } = action.payload;
12
12
  const state = state$.value;
@@ -5,6 +5,7 @@ import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
5
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
6
6
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
7
  import { fetchCompletedTransactions, fetchCompletedTransactionsFailure, fetchCompletedTransactionsSuccess, getCompletedTransactionsCacheKey, } from '../../reducers/missingReceiptsViewReducer';
8
+ import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
8
9
  export const fetchCompletedTransactionsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchCompletedTransactions.match), switchMap((action) => {
9
10
  const state = state$.value;
10
11
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationMissingReceiptsViewState: { bulkUpload }, } = state;
@@ -39,7 +40,7 @@ export const fetchCompletedTransactionsEpic = (actions$, state$, zeniAPI) => act
39
40
  if (sortOrder != null) {
40
41
  queryPayload.sort_order = sortOrder === 'ascending' ? 'asc' : 'desc';
41
42
  }
42
- if (completedSubTab !== 'all') {
43
+ if (completedSubTab !== DEFAULT_COMPLETED_SUB_TAB) {
43
44
  queryPayload.match_type = completedSubTab;
44
45
  }
45
46
  const encodedQuery = encodeURIComponent(JSON.stringify(queryPayload));
@@ -1,12 +1,12 @@
1
1
  import { EMPTY, concat, from, of } from 'rxjs';
2
2
  import { catchError, concatMap, filter, mergeMap, take } from 'rxjs/operators';
3
+ import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
4
+ import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
3
5
  import { isSuccessResponse } from '../../../../responsePayload';
4
- import { isBatchDetailsStatusPendingOrProcessing, toBatchListItem, } from '../../payload/missingReceiptsPayload';
5
- import { fetchBulkUploadBatchesSuccess, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, } from '../../reducers/missingReceiptsViewReducer';
6
6
  import { BULK_UPLOAD_BATCH_LIST_DATE_RANGE_DAYS } from '../../helpers/bulkUploadTiming';
7
7
  import { rollingCalendarDateRangeInclusive } from '../../helpers/rollingCalendarDateRangeInclusive';
8
- import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
9
- import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
8
+ import { isBatchDetailsStatusPendingOrProcessing, toBatchListItem, } from '../../payload/missingReceiptsPayload';
9
+ import { fetchBulkUploadBatchesSuccess, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, } from '../../reducers/missingReceiptsViewReducer';
10
10
  /**
11
11
  * On missing-receipts entry / period switch, hydrate the automatching banner for the logged-in
12
12
  * user without relying on UI-driven list fetches (the Missing tab does not fetch the batch list
@@ -49,10 +49,12 @@ export const restoreBulkUploadAutomatchingOnMountEpic = (actions$, state$, zeniA
49
49
  bulkUpload.batchListFetchState.fetchState === 'Completed';
50
50
  const listSource = alreadyCached
51
51
  ? of(null)
52
- : zeniAPI.getJSON(listUrl).pipe(catchError(() => of(null)));
52
+ : zeniAPI
53
+ .getJSON(listUrl)
54
+ .pipe(catchError(() => of(null)));
53
55
  return listSource.pipe(mergeMap((listResponse) => {
54
56
  const batchList = alreadyCached
55
- ? bulkUpload.batchListByPeriod[periodId] ?? []
57
+ ? (bulkUpload.batchListByPeriod[periodId] ?? [])
56
58
  : listResponse != null &&
57
59
  isSuccessResponse(listResponse) &&
58
60
  listResponse.data != null
@@ -2,10 +2,11 @@ import { EMPTY, from, merge, of } from 'rxjs';
2
2
  import { catchError, debounceTime, filter, mergeMap, switchMap, } from 'rxjs/operators';
3
3
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { rollingCalendarDateRangeInclusive } from '../../helpers/rollingCalendarDateRangeInclusive';
5
6
  import { supportedTransactionPayloadToManualSearchResult } from '../../payload/missingReceiptsPayload';
6
7
  import { searchTransactionsForManualMatch, searchTransactionsForManualMatchFailure, searchTransactionsForManualMatchSuccess, } from '../../reducers/missingReceiptsViewReducer';
8
+ import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
7
9
  import { toTransactionsSortKey } from '../../types/transactionsViewState';
8
- import { rollingCalendarDateRangeInclusive } from '../../helpers/rollingCalendarDateRangeInclusive';
9
10
  /**
10
11
  * `auto_categorized` for manual transaction search (Unmatched).
11
12
  * Backend contract TBD — align with `fetchTransactionCategorizationEpic` (`selectedTab === 'autoCategorized'`)
@@ -46,6 +47,11 @@ export const searchTransactionsForManualMatchEpic = (actions$, state$, zeniAPI)
46
47
  page_token: pageToken ?? null,
47
48
  page_size: manualSearch.pageSize,
48
49
  search_text: query,
50
+ // Manual search ignores the Completed sub-tab axis — always span the
51
+ // default sentinel so the receipt-matching UX returns results
52
+ // regardless of where the transaction would otherwise be bucketed on
53
+ // the Completed tab.
54
+ sub_tab: DEFAULT_COMPLETED_SUB_TAB,
49
55
  };
50
56
  return zeniAPI
51
57
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/transactions?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
@@ -5,7 +5,8 @@ import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
5
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
6
6
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
7
  import { getLineItemsByTransactionsIdsFromResponse } from '../../helpers/transactionCategorizationLocalDataHelper';
8
- import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
8
+ import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateParentTotalCountForTab, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
9
+ import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
9
10
  import { TRANSACTIONS_TABS, toTransactionsSortKey, } from '../../types/transactionsViewState';
10
11
  export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match), switchMap((action) => {
11
12
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationTransactionsViewState, } = state$.value;
@@ -23,6 +24,9 @@ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) =>
23
24
  page_token: pageToken,
24
25
  page_size: 25,
25
26
  search_text: uiState.searchString,
27
+ sub_tab: selectedTab === 'autoCategorized'
28
+ ? expenseAutomationTransactionsViewState.selectedTransactionCategorizationCompletedSubTab
29
+ : DEFAULT_COMPLETED_SUB_TAB,
26
30
  };
27
31
  return zeniAPI
28
32
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/transactions?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
@@ -45,6 +49,12 @@ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) =>
45
49
  selectedTab,
46
50
  totalCount: response.data.total_count,
47
51
  }));
52
+ updateActions.push(updateParentTotalCountForTab({
53
+ selectedPeriod,
54
+ selectedTab,
55
+ parentTotalCount: response.data.parent_tab_total_count ??
56
+ response.data.total_count,
57
+ }));
48
58
  updateActions.push(resetOtherTabsFetchState({
49
59
  refreshViewInBackground,
50
60
  tabs: TRANSACTIONS_TABS.filter((tab) => selectedTab !== tab),
@@ -1,5 +1,6 @@
1
1
  import { createAction, createSlice } from '@reduxjs/toolkit';
2
2
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
+ import { DEFAULT_COMPLETED_SUB_TAB, } from '../types/completedSubTab';
3
4
  import { MIN_MANUAL_TRANSACTION_SEARCH_LENGTH, } from '../types/missingReceiptsViewState';
4
5
  export const getCompletedTransactionsCacheKey = (periodId, sortKey, sortOrder, subTab) => {
5
6
  const sortKeyPart = sortKey ?? 'none';
@@ -12,7 +13,7 @@ export const initialBulkUploadState = {
12
13
  batchListByPeriod: {},
13
14
  batchListFetchState: { fetchState: 'Not-Started', error: undefined },
14
15
  batchStatusById: {},
15
- completedSubTab: 'all',
16
+ completedSubTab: DEFAULT_COMPLETED_SUB_TAB,
16
17
  completedTransactionsByPeriod: {},
17
18
  confirmMatchStatus: { fetchState: 'Not-Started', error: undefined },
18
19
  currentBatchId: undefined,
@@ -4,6 +4,7 @@ import uniq from 'lodash/uniq';
4
4
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
5
  import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
6
6
  import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
7
+ import { DEFAULT_COMPLETED_SUB_TAB, } from '../types/completedSubTab';
7
8
  export const initialTransactionTabViewState = {
8
9
  saveStatus: {
9
10
  fetchState: 'Not-Started',
@@ -27,6 +28,10 @@ export const initialTransactionTabViewState = {
27
28
  totalCount: {},
28
29
  limit: 10,
29
30
  },
31
+ filters: {
32
+ categoryCombinationOperator: 'AND',
33
+ categories: [],
34
+ },
30
35
  fetchState: 'Not-Started',
31
36
  error: undefined,
32
37
  hasValidState() {
@@ -41,12 +46,74 @@ export const initialTransactionTabViewState = {
41
46
  selectedTransactionLineId: undefined,
42
47
  };
43
48
  export const initialState = {
49
+ autoCategorizedSubTabCache: {},
50
+ parentTotalCountByTab: {
51
+ review: {},
52
+ autoCategorized: {},
53
+ },
44
54
  selectedTransactionCategorizationTab: 'review',
55
+ selectedTransactionCategorizationCompletedSubTab: DEFAULT_COMPLETED_SUB_TAB,
45
56
  transactionCategorizationView: {
46
57
  review: { ...initialTransactionTabViewState },
47
58
  autoCategorized: { ...initialTransactionTabViewState },
48
59
  },
49
60
  };
61
+ /**
62
+ * Capture the autoCategorized tab's current paginated dataset for a sub-tab.
63
+ * The mid-flight `'In-Progress'` state is normalised to `'Completed'` because
64
+ * the in-flight request is cancelled by `switchMap` in the fetch epic when the
65
+ * sub-tab changes — restoring `'In-Progress'` later would leave the UI stuck
66
+ * showing a phantom loading state with no fetch actually running. Length-zero
67
+ * datasets degrade to `'Not-Started'` so the next visit triggers a fresh
68
+ * fetch from page 1 instead of short-circuiting on stale empty caches.
69
+ *
70
+ * Snapshot scope (deliberately narrow):
71
+ * - Captured: `pageToken`, `totalCount`, `transactionIdsBySelectedPeriod`,
72
+ * `fetchState`, `error`, `scrollPosition`. The first five are the keys the
73
+ * fetch epic's cache check reads to decide whether to short-circuit a
74
+ * sub-tab revisit; `scrollPosition` is captured per sub-tab so revisiting
75
+ * restores the user's last offset and switching to a never-visited sub-tab
76
+ * starts at the top instead of inheriting the outgoing sub-tab's offset
77
+ * (which would land the user at a meaningless row index against a
78
+ * completely different list).
79
+ * - Intentionally NOT captured (left in place on `autoCat` and inherited by
80
+ * the incoming sub-tab):
81
+ * - `uiState.searchString`, `uiState.sortKey`, `uiState.sortOrder`,
82
+ * `uiState.nodeCollapseState`, `uiState.limit` — search and sort are
83
+ * user-applied preferences that persist across sub-tabs by design (a
84
+ * search query stays active when the user toggles between Manual / AI
85
+ * Accountant).
86
+ * - `selectedCheckBoxTransactionIds` — selection state is intentionally
87
+ * cleared at the action source (the fetch epic / save flow), not
88
+ * by the snapshot path. Carrying selection across sub-tabs is safe
89
+ * because every sub-tab change goes through a fetch round-trip that
90
+ * reconciles selection against the new row IDs.
91
+ * - `transactionReviewLocalDataById`, `transactionIdsWithUnsavedData`,
92
+ * `uploadReceiptStatusById`, `saveStatus`, `refreshStatus`,
93
+ * `markAsNotMiscategorizedStatus` — these are per-row / per-flight
94
+ * caches that are write-through global state, not sub-tab-scoped.
95
+ *
96
+ * If the contract changes (e.g. selection should reset on every sub-tab
97
+ * switch), update both this comment and the snapshot return shape together.
98
+ */
99
+ function snapshotAutoCategorizedTab(autoCat) {
100
+ const transactionIdsBySelectedPeriod = {
101
+ ...autoCat.transactionIdsBySelectedPeriod,
102
+ };
103
+ const hasAnyLoadedRows = Object.values(transactionIdsBySelectedPeriod).some((ids) => ids.length > 0);
104
+ let normalisedFetchState = autoCat.fetchState;
105
+ if (normalisedFetchState === 'In-Progress') {
106
+ normalisedFetchState = hasAnyLoadedRows ? 'Completed' : 'Not-Started';
107
+ }
108
+ return {
109
+ error: autoCat.error,
110
+ fetchState: normalisedFetchState,
111
+ pageToken: autoCat.uiState.pageToken,
112
+ scrollPosition: autoCat.uiState.scrollPosition,
113
+ totalCount: { ...autoCat.uiState.totalCount },
114
+ transactionIdsBySelectedPeriod,
115
+ };
116
+ }
50
117
  const expenseAutomationTransactionsView = createSlice({
51
118
  name: 'expenseAutomationTransactionsView',
52
119
  initialState,
@@ -236,6 +303,12 @@ const expenseAutomationTransactionsView = createSlice({
236
303
  uiState.totalCount;
237
304
  }
238
305
  },
306
+ updateTransactionFilters(draft, action) {
307
+ const { selectedTab, filters } = action.payload;
308
+ // `filters` is non-nullable per the payload type, so no runtime
309
+ // null-check is needed — dropping it removes a dead defensive branch.
310
+ draft.transactionCategorizationView[selectedTab].filters = filters;
311
+ },
239
312
  saveTransactionCategorization: {
240
313
  prepare(selectedTab, transactionIds, cotTrackingByTransactionId, isUncategorizedExpenseCategoryEnabled) {
241
314
  return {
@@ -260,6 +333,55 @@ const expenseAutomationTransactionsView = createSlice({
260
333
  return { payload: { newSelectedTab } };
261
334
  },
262
335
  },
336
+ updateTransactionCategorizationCompletedSubTab: {
337
+ reducer(draft, action) {
338
+ const { newSubTab } = action.payload;
339
+ const previousSubTab = draft.selectedTransactionCategorizationCompletedSubTab;
340
+ if (previousSubTab === newSubTab) {
341
+ return;
342
+ }
343
+ const autoCat = draft.transactionCategorizationView.autoCategorized;
344
+ // Snapshot the outgoing sub-tab so a future revisit can short-circuit
345
+ // the fetch epic (mirrors the parent-tab "don't refetch what's
346
+ // already cached" behaviour). Period-scoped data is preserved as-is
347
+ // because the snapshot itself is keyed only by sub-tab; the active
348
+ // tab's per-period maps remain the source of truth while live.
349
+ draft.autoCategorizedSubTabCache[previousSubTab] =
350
+ snapshotAutoCategorizedTab(autoCat);
351
+ const cached = draft.autoCategorizedSubTabCache[newSubTab];
352
+ if (cached != null) {
353
+ autoCat.uiState.pageToken = cached.pageToken;
354
+ autoCat.uiState.totalCount = { ...cached.totalCount };
355
+ autoCat.uiState.scrollPosition = cached.scrollPosition;
356
+ autoCat.transactionIdsBySelectedPeriod = {
357
+ ...cached.transactionIdsBySelectedPeriod,
358
+ };
359
+ autoCat.fetchState = cached.fetchState;
360
+ autoCat.error = cached.error;
361
+ }
362
+ else {
363
+ // Sub-tab has never been visited — clear the active slot so the
364
+ // view-epic's `fetchState === 'Not-Started' || transactionIds.length
365
+ // === 0` cache check falls through and triggers a fresh page-1
366
+ // fetch for the new dataset. `scrollPosition` is reset to the top
367
+ // so the incoming list isn't anchored to the outgoing sub-tab's
368
+ // offset (different rows → previous offset is meaningless).
369
+ autoCat.uiState.pageToken = null;
370
+ autoCat.uiState.totalCount = {};
371
+ autoCat.uiState.scrollPosition = {
372
+ scrollTop: 0,
373
+ scrollLeft: undefined,
374
+ };
375
+ autoCat.transactionIdsBySelectedPeriod = {};
376
+ autoCat.fetchState = 'Not-Started';
377
+ autoCat.error = undefined;
378
+ }
379
+ draft.selectedTransactionCategorizationCompletedSubTab = newSubTab;
380
+ },
381
+ prepare(newSubTab) {
382
+ return { payload: { newSubTab } };
383
+ },
384
+ },
263
385
  updateTransactionCategorization: {
264
386
  reducer(draft, action) {
265
387
  const selectedTab = action.payload.selectedTab;
@@ -697,6 +819,10 @@ const expenseAutomationTransactionsView = createSlice({
697
819
  const { selectedTab, selectedPeriod, totalCount } = action.payload;
698
820
  draft.transactionCategorizationView[selectedTab].uiState.totalCount[toMonthYearPeriodId(selectedPeriod)] = totalCount;
699
821
  },
822
+ updateParentTotalCountForTab(draft, action) {
823
+ const { selectedTab, selectedPeriod, parentTotalCount } = action.payload;
824
+ draft.parentTotalCountByTab[selectedTab][toMonthYearPeriodId(selectedPeriod)] = parentTotalCount;
825
+ },
700
826
  updateSelectedCheckboxTransactionIds(draft, action) {
701
827
  const selectedTab = action.payload.selectedTab;
702
828
  const transactionIds = action.payload.transactionIds;
@@ -707,6 +833,16 @@ const expenseAutomationTransactionsView = createSlice({
707
833
  draft.transactionCategorizationView[selectedTab] = {
708
834
  ...initialTransactionTabViewState,
709
835
  };
836
+ draft.parentTotalCountByTab[selectedTab] = {};
837
+ // The per-sub-tab snapshot cache lives at the slice root and is
838
+ // exclusively populated by the autoCategorized tab's sub-tab switching
839
+ // (see updateTransactionCategorizationCompletedSubTab). When that tab
840
+ // is reset, leaving stale snapshots behind would short-circuit the
841
+ // next sub-tab visit with phantom row IDs / page tokens that no longer
842
+ // correspond to anything in state — drop them in lockstep.
843
+ if (selectedTab === 'autoCategorized') {
844
+ draft.autoCategorizedSubTabCache = {};
845
+ }
710
846
  },
711
847
  clearExpenseAutomationTransactionsView(draft) {
712
848
  Object.assign(draft, initialState);
@@ -803,5 +939,5 @@ const expenseAutomationTransactionsView = createSlice({
803
939
  },
804
940
  },
805
941
  });
806
- export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
942
+ export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
807
943
  export default expenseAutomationTransactionsView.reducer;
@@ -4,14 +4,33 @@ import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
4
4
  import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
5
5
  import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
6
6
  import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
7
+ import { applyTransactionFilters, } from '../transactionFilterHelpers';
7
8
  import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
8
9
  import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
9
10
  import { getProjectList } from '../../projectList/projectListSelector';
10
11
  import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
12
+ export const toTransactionView = (transaction, transactionLocalData) => {
13
+ return {
14
+ id: transaction.id,
15
+ date: transaction.date,
16
+ amount: transaction.amount,
17
+ vendorName: transaction.vendorName,
18
+ customerName: transaction.customerName,
19
+ vendorId: transaction.vendorId,
20
+ customerId: transaction.customerId,
21
+ description: transaction.description,
22
+ memo: transaction.memo,
23
+ type: transaction.type,
24
+ typeName: transaction.typeName,
25
+ createTime: transaction.createTime,
26
+ transaction: transaction,
27
+ transactionLocalData,
28
+ };
29
+ };
11
30
  export function getExpenseAutomationTransactionView(state) {
12
31
  const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
13
- const { selectedTransactionCategorizationTab } = expenseAutomationTransactionsViewState;
14
- const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
32
+ const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
33
+ const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, filters, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
15
34
  const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
16
35
  const accountList = getAccountList(accountState, accountListState, 'accountList');
17
36
  const classList = getClassList(classState, classListState);
@@ -30,20 +49,58 @@ export function getExpenseAutomationTransactionView(state) {
30
49
  ? transactionIdsBySelectedPeriod[monthYearPeriodId]
31
50
  : null;
32
51
  const transactionIds = transactionIdsForSelectedPeriod ?? [];
33
- const transactionLocalData = transactionIds
34
- .map((transactionId) => {
52
+ const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
53
+ const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
54
+ const transactionLocalDataMap = new Map();
55
+ transactionIds.forEach((transactionId) => {
35
56
  const transactionData = transactionReviewLocalDataById[transactionId];
36
57
  if (transactionData != null) {
37
- return {
58
+ transactionLocalDataMap.set(transactionId, {
38
59
  transactionId,
39
60
  transactionReviewLocalData: transactionData.transactionReviewLocalData,
40
- };
61
+ });
41
62
  }
42
- return null;
43
- })
44
- .filter((transaction) => transaction != null);
45
- const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
46
- const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
63
+ });
64
+ // Step 2: Convert to TransactionView and apply advanced filters
65
+ const transactionViews = transactionsWithCOT.map((transaction) => {
66
+ const localData = transactionLocalDataMap.get(transaction.id);
67
+ return toTransactionView(transaction, localData);
68
+ });
69
+ // Apply TC-specific filters (lives under expenseAutomationView, independent
70
+ // of the Spend Management filter pipeline).
71
+ const filteredTransactionViews = applyTransactionFilters(transactionViews, filters);
72
+ // Convert back to TransactionWithCOT for return
73
+ const filteredTransactionsWithCOT = filteredTransactionViews.map((view) => view.transaction);
74
+ // Get transactionLocalData for filtered transactions
75
+ // Also include transactionLocalData for transactions that don't exist in transactionState
76
+ // but have local data (to match original behavior)
77
+ const filteredTransactionIds = new Set(filteredTransactionViews.map((view) => view.id));
78
+ // Pre-compute the membership set once. The previous implementation called
79
+ // `transactions.some(t => t.id === transactionId)` inside the per-id loop,
80
+ // turning the pipeline into O(n × m) — measurable on tenants with large
81
+ // transaction lists since this selector runs on every Redux dispatch.
82
+ const transactionExistsById = new Set(transactions.map((t) => t.id));
83
+ // `.flatMap` returns `X[]` directly when the callback returns `[]` for the
84
+ // skip case, so we don't need the trailing `.filter((d): d is X => …)` type
85
+ // predicate that the previous `map → filter` pipeline required.
86
+ const filteredTransactionLocalData = transactionIds.flatMap((transactionId) => {
87
+ const transactionData = transactionReviewLocalDataById[transactionId];
88
+ if (transactionData == null) {
89
+ return [];
90
+ }
91
+ // Include if transaction is in filtered results, or if transaction doesn't exist in state
92
+ // (to maintain backward compatibility with tests)
93
+ const transactionExists = transactionExistsById.has(transactionId);
94
+ if (filteredTransactionIds.has(transactionId) || !transactionExists) {
95
+ return [
96
+ {
97
+ transactionId,
98
+ transactionReviewLocalData: transactionData.transactionReviewLocalData,
99
+ },
100
+ ];
101
+ }
102
+ return [];
103
+ });
47
104
  const monthEndFetchState = monthYearPeriodId != null
48
105
  ? (monthEndCloseChecksViewState.monthEndCloseChecksViewByTenantId[currentTenant.tenantId]?.[monthYearPeriodId] ?? { fetchState: 'Not-Started', error: undefined })
49
106
  : { fetchState: 'Not-Started', error: undefined };
@@ -63,10 +120,27 @@ export function getExpenseAutomationTransactionView(state) {
63
120
  .uiState.totalCount;
64
121
  const totalCountByAutoCat = expenseAutomationTransactionsViewState.transactionCategorizationView
65
122
  .autoCategorized.uiState.totalCount;
123
+ // Per-(sub-)tab counts powering pagination, in-list "showing X of Y", and
124
+ // any save/refresh round-trips. Sourced from `uiState.totalCount` which
125
+ // mirrors the response's `total_count` for whatever tab/sub-tab the user is
126
+ // viewing (e.g. only the Manual sub-tab rows for Completed → Manual).
66
127
  const totalCountByTab = {
67
128
  review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
68
129
  autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
69
130
  };
131
+ // Parent-tab badge counts. Sourced from `parent_tab_total_count` in the
132
+ // listing response and used exclusively by the navbar / tab strip badges.
133
+ // Independent of which Completed sub-tab is active so the badge stays
134
+ // stable across sub-tab switches.
135
+ const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
136
+ const parentTabTotalCountByTab = {
137
+ review: monthYearPeriodId != null
138
+ ? (parentTotalCountByTab.review[monthYearPeriodId] ?? 0)
139
+ : 0,
140
+ autoCategorized: monthYearPeriodId != null
141
+ ? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
142
+ : 0,
143
+ };
70
144
  const fetchStateByTab = {
71
145
  review: {
72
146
  fetchState: expenseAutomationTransactionsViewState.transactionCategorizationView
@@ -90,7 +164,7 @@ export function getExpenseAutomationTransactionView(state) {
90
164
  version: 0,
91
165
  fetchState: fetchStatus.fetchState,
92
166
  error: fetchStatus.error,
93
- transactions: transactionsWithCOT,
167
+ transactions: filteredTransactionsWithCOT,
94
168
  selectedCheckBoxTransactionIds,
95
169
  transactionIdsWithUnsavedData,
96
170
  uncategorizedAccounts: uncategorizedIncomeExpense,
@@ -101,16 +175,18 @@ export function getExpenseAutomationTransactionView(state) {
101
175
  classHierarchyList,
102
176
  isAccountingProjectsEnabled,
103
177
  projectList,
104
- transactionLocalData,
178
+ transactionLocalData: filteredTransactionLocalData,
105
179
  uiState,
106
180
  refreshStatus,
107
181
  saveStatus,
108
182
  markAsNotMiscategorizedStatus,
109
183
  completionStatus,
110
184
  totalCountByTab,
185
+ parentTabTotalCountByTab,
111
186
  fetchStateByTransactionTabs: fetchStateByTab,
112
187
  uploadReceiptStatusById,
113
188
  selectedTransactionId,
114
189
  selectedTransactionLineId,
190
+ selectedTransactionCategorizationCompletedSubTab,
115
191
  };
116
192
  }