@zeniai/client-epic-state 5.0.80 → 5.0.81-beta0ND

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 (222) 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/paymentAccount/paymentAccountSelector.d.ts +1 -0
  23. package/lib/entity/paymentAccount/paymentAccountSelector.js +4 -0
  24. package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +1 -1
  25. package/lib/entity/sectionProjectView/sectionProjectViewSelector.js +1 -1
  26. package/lib/entity/tenant/SessionManager.js +6 -3
  27. package/lib/entity/tenant/clearAllEpic.d.ts +2 -2
  28. package/lib/entity/tenant/clearAllEpic.js +2 -2
  29. package/lib/entity/tenant/epic/deleteConnectionEpic.d.ts +1 -1
  30. package/lib/entity/tenant/epic/initEmailConnectOAuthEpic.d.ts +21 -0
  31. package/lib/entity/tenant/epic/initEmailConnectOAuthEpic.js +54 -0
  32. package/lib/entity/tenant/epic/saveAPIKeyConnectionEpic.d.ts +1 -1
  33. package/lib/entity/tenant/epic/saveAPIKeyConnectionEpic.js +6 -2
  34. package/lib/entity/tenant/epic/saveConnectorCredentialsEpic.d.ts +3 -3
  35. package/lib/entity/tenant/epic/saveOAuthConnectionEpic.d.ts +1 -1
  36. package/lib/entity/tenant/epic/saveOAuthConnectionEpic.js +6 -2
  37. package/lib/entity/tenant/tenantPayload.d.ts +1 -0
  38. package/lib/entity/tenant/tenantReducer.d.ts +38 -25
  39. package/lib/entity/tenant/tenantReducer.js +71 -6
  40. package/lib/entity/tenant/tenantState.d.ts +1 -0
  41. package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +3 -1
  42. package/lib/epic.d.ts +13 -11
  43. package/lib/epic.js +15 -12
  44. package/lib/esm/commonStateTypes/cacheOverrideFetchReducer.js +16 -0
  45. package/lib/esm/commonStateTypes/filterPrimitives.js +20 -0
  46. package/lib/esm/commonStateTypes/pusherActions.js +3 -0
  47. package/lib/esm/coreEpics.js +1 -1
  48. package/lib/esm/entity/account/accountSelector.js +46 -1
  49. package/lib/esm/entity/account/accountState.js +7 -1
  50. package/lib/esm/entity/aiAccountantCustomer/aiAccountantCustomerPayload.js +1 -3
  51. package/lib/esm/entity/aiAccountantCustomer/aiAccountantCustomerReducer.js +1 -2
  52. package/lib/esm/entity/chargeCard/chargeCardReducer.js +34 -1
  53. package/lib/esm/entity/chargeCardTransaction/chargeCardTransactionReducer.js +35 -1
  54. package/lib/esm/entity/class/classSelector.js +48 -0
  55. package/lib/esm/entity/paymentAccount/paymentAccountSelector.js +3 -0
  56. package/lib/esm/entity/sectionProjectView/sectionProjectViewSelector.js +1 -1
  57. package/lib/esm/entity/tenant/SessionManager.js +4 -1
  58. package/lib/esm/entity/tenant/clearAllEpic.js +2 -2
  59. package/lib/esm/entity/tenant/epic/initEmailConnectOAuthEpic.js +50 -0
  60. package/lib/esm/entity/tenant/epic/saveAPIKeyConnectionEpic.js +6 -2
  61. package/lib/esm/entity/tenant/epic/saveOAuthConnectionEpic.js +6 -2
  62. package/lib/esm/entity/tenant/tenantReducer.js +69 -4
  63. package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +3 -1
  64. package/lib/esm/epic.js +15 -12
  65. package/lib/esm/index.js +38 -25
  66. package/lib/esm/reducer.js +2 -2
  67. package/lib/esm/view/aiAccountantView/epics/fetchAiAccountantCustomersEpic.js +2 -5
  68. package/lib/esm/view/aiAccountantView/epics/fetchAiAccountantJobsEpic.js +2 -5
  69. package/lib/esm/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +4 -1
  70. package/lib/esm/view/companyHealthMetricView/companyHealthMetricViewSelector.js +1 -1
  71. package/lib/esm/view/companyTaskManagerView/companyTaskManagerViewSelector.js +1 -1
  72. package/lib/esm/view/companyTaskManagerView/epics/fetchTaskManagerMetricsEpic.js +1 -1
  73. package/lib/esm/view/companyView/helpers/cockpitHelpers.js +1 -1
  74. package/lib/esm/view/companyView/selector/companyManagementViewSelector.js +1 -1
  75. package/lib/esm/view/companyView/selector/companyPortfolioViewSelector.js +1 -1
  76. package/lib/esm/view/creditAgentView/creditAgentViewReducer.js +1 -16
  77. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +2 -2
  78. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +2 -1
  79. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/restoreBulkUploadAutomatchingOnMountEpic.js +8 -6
  80. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +7 -1
  81. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +11 -1
  82. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +21 -1
  83. package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +2 -1
  84. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +253 -14
  85. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +147 -13
  86. package/lib/esm/view/expenseAutomationView/transactionFilterHelpers.js +258 -0
  87. package/lib/esm/view/expenseAutomationView/types/completedSubTab.js +17 -0
  88. package/lib/esm/view/expenseAutomationView/types/missingReceiptsViewState.js +0 -2
  89. package/lib/esm/view/financeStatement/financeStatementSelector.js +1 -1
  90. package/lib/esm/view/onboardingView/customerView/onboardingCustomerViewReducer.js +61 -50
  91. package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +87 -0
  92. package/lib/esm/view/recommendation/recommendationHelper.js +29 -0
  93. package/lib/esm/view/recommendation/recommendationReducer.js +26 -1
  94. package/lib/esm/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.js +3 -2
  95. package/lib/esm/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.js +3 -2
  96. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/chargeCardDetailReducer.js +55 -18
  97. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailEpic.js +16 -2
  98. package/lib/esm/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailPageEpic.js +5 -4
  99. package/lib/esm/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.js +26 -9
  100. package/lib/esm/view/spendManagement/chargeCards/chargeCardList/fetchChargeCardListPageEpic.js +9 -6
  101. package/lib/esm/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.js +3 -2
  102. package/lib/esm/view/spendManagement/chargeCards/chargeCardPaymentHistory/fetchChargeCardPaymentPageEpic.js +1 -1
  103. package/lib/esm/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.js +7 -3
  104. package/lib/esm/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.js +49 -0
  105. package/lib/esm/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.js +58 -0
  106. package/lib/esm/view/spendManagement/commonSetup/kycKybAutofillActions.js +12 -0
  107. package/lib/esm/view/spendManagement/commonSetup/kycKybParseMapper.js +205 -0
  108. package/lib/esm/view/spendManagement/commonSetup/setupViewReducer.js +71 -52
  109. package/lib/esm/view/spendManagement/commonSetup/setupViewSelector.js +2 -0
  110. package/lib/esm/view/spendManagement/commonSetup/types/kycKybAutofill.js +28 -0
  111. package/lib/esm/view/spendManagement/spendManagementFilterHelpers.js +4 -2
  112. package/lib/esm/view/subscriptionView/subscriptionViewSelector.js +1 -1
  113. package/lib/esm/view/taskManager/cannedResponsesView/epics/fetchCannedResponsesEpic.js +1 -1
  114. package/lib/esm/view/taskManager/cannedResponsesView/epics/saveCannedResponseEpic.js +1 -1
  115. package/lib/esm/view/taskManager/taskDetailView/epics/unsnoozeTaskEpic.js +1 -1
  116. package/lib/esm/view/taskManager/taskListView/taskListReducer.js +1 -1
  117. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +121 -6
  118. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +29 -1
  119. package/lib/index.d.ts +39 -28
  120. package/lib/index.js +97 -60
  121. package/lib/reducer.d.ts +2 -2
  122. package/lib/reducer.js +2 -2
  123. package/lib/view/aiAccountantView/aiAccountantViewPayload.d.ts +2 -2
  124. package/lib/view/aiAccountantView/aiAccountantViewReducer.d.ts +1 -1
  125. package/lib/view/aiAccountantView/epics/fetchAiAccountantCustomersEpic.js +2 -5
  126. package/lib/view/aiAccountantView/epics/fetchAiAccountantJobsEpic.js +2 -5
  127. package/lib/view/aiAccountantView/epics/triggerAiAccountantJobEpic.js +4 -1
  128. package/lib/view/companyHealthMetricView/companyHealthMetricViewSelector.js +1 -1
  129. package/lib/view/companyTaskManagerView/companyTaskManagerViewSelector.d.ts +1 -1
  130. package/lib/view/companyTaskManagerView/companyTaskManagerViewSelector.js +1 -1
  131. package/lib/view/companyView/helpers/cockpitHelpers.js +1 -1
  132. package/lib/view/companyView/selector/companyManagementViewSelector.js +1 -1
  133. package/lib/view/companyView/selector/companyPortfolioViewSelector.js +1 -1
  134. package/lib/view/companyView/types/cockpitTypes.d.ts +1 -1
  135. package/lib/view/creditAgentView/creditAgentViewPayload.d.ts +0 -3
  136. package/lib/view/creditAgentView/creditAgentViewReducer.d.ts +1 -1
  137. package/lib/view/creditAgentView/creditAgentViewReducer.js +2 -17
  138. package/lib/view/creditAgentView/creditAgentViewState.d.ts +0 -5
  139. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +2 -2
  140. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +2 -1
  141. package/lib/view/expenseAutomationView/epics/missingReceipts/restoreBulkUploadAutomatchingOnMountEpic.js +8 -6
  142. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +7 -1
  143. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.d.ts +2 -2
  144. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +10 -0
  145. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +2 -1
  146. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +23 -2
  147. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +21 -0
  148. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +2 -1
  149. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +2 -1
  150. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +28 -5
  151. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +253 -14
  152. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +2 -1
  153. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +16 -0
  154. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +7 -1
  155. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +150 -13
  156. package/lib/view/expenseAutomationView/transactionFilterHelpers.d.ts +66 -0
  157. package/lib/view/expenseAutomationView/transactionFilterHelpers.js +262 -0
  158. package/lib/view/expenseAutomationView/types/completedSubTab.d.ts +10 -0
  159. package/lib/view/expenseAutomationView/types/completedSubTab.js +21 -0
  160. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +1 -2
  161. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.js +1 -4
  162. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +44 -2
  163. package/lib/view/financeStatement/financeStatementSelector.js +1 -1
  164. package/lib/view/onboardingView/customerView/onboardingCustomerViewReducer.d.ts +25 -2
  165. package/lib/view/onboardingView/customerView/onboardingCustomerViewReducer.js +62 -51
  166. package/lib/view/onboardingView/customerView/onboardingCustomerViewState.d.ts +10 -0
  167. package/lib/view/profitAndLossProjectView/profitAndLossProjectViewSelectorTypes.d.ts +1 -1
  168. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +9 -0
  169. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +91 -0
  170. package/lib/view/recommendation/recommendationHelper.d.ts +9 -0
  171. package/lib/view/recommendation/recommendationHelper.js +31 -1
  172. package/lib/view/recommendation/recommendationReducer.d.ts +20 -1
  173. package/lib/view/recommendation/recommendationReducer.js +27 -2
  174. package/lib/view/spendManagement/billPay/billList/billListState.d.ts +1 -1
  175. package/lib/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.d.ts +3 -1
  176. package/lib/view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer.js +3 -2
  177. package/lib/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.d.ts +3 -1
  178. package/lib/view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer.js +3 -2
  179. package/lib/view/spendManagement/chargeCards/chargeCardDetail/chargeCardDetailReducer.js +55 -18
  180. package/lib/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailEpic.js +16 -2
  181. package/lib/view/spendManagement/chargeCards/chargeCardDetail/fetchChargeCardDetailPageEpic.js +5 -4
  182. package/lib/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.d.ts +18 -3
  183. package/lib/view/spendManagement/chargeCards/chargeCardList/chargeCardListReducer.js +27 -10
  184. package/lib/view/spendManagement/chargeCards/chargeCardList/fetchChargeCardListPageEpic.js +9 -6
  185. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistory.d.ts +1 -1
  186. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.d.ts +3 -1
  187. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/chargeCardPaymentHistoryReducer.js +3 -2
  188. package/lib/view/spendManagement/chargeCards/chargeCardPaymentHistory/fetchChargeCardPaymentPageEpic.js +1 -1
  189. package/lib/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.d.ts +3 -1
  190. package/lib/view/spendManagement/chargeCards/chargeCardRepaymentDetail/chargeCardRepaymentDetailReducer.js +7 -3
  191. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.d.ts +8 -0
  192. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.js +53 -0
  193. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.d.ts +8 -0
  194. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.js +62 -0
  195. package/lib/view/spendManagement/commonSetup/kycKybAutofillActions.d.ts +27 -0
  196. package/lib/view/spendManagement/commonSetup/kycKybAutofillActions.js +15 -0
  197. package/lib/view/spendManagement/commonSetup/kycKybParseMapper.d.ts +24 -0
  198. package/lib/view/spendManagement/commonSetup/kycKybParseMapper.js +213 -0
  199. package/lib/view/spendManagement/commonSetup/setupViewReducer.d.ts +24 -2
  200. package/lib/view/spendManagement/commonSetup/setupViewReducer.js +72 -53
  201. package/lib/view/spendManagement/commonSetup/setupViewSelector.d.ts +4 -0
  202. package/lib/view/spendManagement/commonSetup/setupViewSelector.js +2 -0
  203. package/lib/view/spendManagement/commonSetup/setupViewState.d.ts +12 -0
  204. package/lib/view/spendManagement/commonSetup/types/kycKybAutofill.d.ts +125 -0
  205. package/lib/view/spendManagement/commonSetup/types/kycKybAutofill.js +32 -0
  206. package/lib/view/spendManagement/reimbursement/remiListView/remiListState.d.ts +1 -1
  207. package/lib/view/spendManagement/spendManagementFilterHelpers.d.ts +12 -3
  208. package/lib/view/spendManagement/spendManagementFilterHelpers.js +4 -2
  209. package/lib/view/subscriptionView/subscriptionViewSelector.js +1 -1
  210. package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
  211. package/lib/view/taskManager/taskListView/taskListReducer.js +1 -1
  212. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -1
  213. package/lib/view/transactionDetail/transactionDetailReducer.js +122 -7
  214. package/lib/view/transactionDetail/transactionDetailSelector.d.ts +2 -0
  215. package/lib/view/transactionDetail/transactionDetailSelector.js +31 -2
  216. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +1 -0
  217. package/lib/view/zeniOAuthView/zeniOAuthSelector.d.ts +1 -1
  218. package/package.json +10 -4
  219. package/lib/esm/view/creditAgentView/epics/fetchCreditAgentAccessEpic.js +0 -13
  220. package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
  221. package/lib/view/creditAgentView/epics/fetchCreditAgentAccessEpic.d.ts +0 -11
  222. package/lib/view/creditAgentView/epics/fetchCreditAgentAccessEpic.js +0 -17
@@ -3,7 +3,8 @@ import recordGet from 'lodash/get';
3
3
  import uniq from 'lodash/uniq';
4
4
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
5
  import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
6
- import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
6
+ import { filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, shouldAutoSelectAndAdd, 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;
@@ -308,12 +430,9 @@ const expenseAutomationTransactionsView = createSlice({
308
430
  const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
309
431
  .selectedCheckBoxTransactionIds;
310
432
  if (selectedTransaction != null &&
311
- checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
312
- selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
313
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
433
+ shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
314
434
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
315
- ...draft.transactionCategorizationView[selectedTab]
316
- .selectedCheckBoxTransactionIds,
435
+ ...selectedCheckBoxTransactionIds,
317
436
  transactionId,
318
437
  ];
319
438
  }
@@ -396,9 +515,26 @@ const expenseAutomationTransactionsView = createSlice({
396
515
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
397
516
  .transactionReviewLocalDataById, transactionId.id, undefined);
398
517
  if (transactionLocalData != null) {
518
+ const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
519
+ // Release the per-line fetch flag for every line this fetch
520
+ // covered. Done in-place on the freshly returned local data
521
+ // (the helper produces a new mutable object), before we write
522
+ // it back to the draft.
523
+ lineIds.forEach((lineId) => {
524
+ const lineItem = newLocalData.lineItemById[lineId];
525
+ if (lineItem != null) {
526
+ newLocalData.lineItemById[lineId] = {
527
+ ...lineItem,
528
+ categoryClassRecommendationsFetchState: {
529
+ fetchState: 'Completed',
530
+ error: undefined,
531
+ },
532
+ };
533
+ }
534
+ });
399
535
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
400
536
  transactionId: transaction.id,
401
- transactionReviewLocalData: setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
537
+ transactionReviewLocalData: newLocalData,
402
538
  };
403
539
  // Mark transaction as dirty (autofill happened)
404
540
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
@@ -411,6 +547,99 @@ const expenseAutomationTransactionsView = createSlice({
411
547
  }
412
548
  },
413
549
  },
550
+ // Start arm of the line-update recommendations fetch. Flips covered
551
+ // lines to In-Progress AND removes the transaction from
552
+ // selectedCheckBoxTransactionIds — the row is unchecked + disabled
553
+ // until either the success arm (recheck if limit allows) or the
554
+ // failure arm releases the per-line flag.
555
+ markCategoryClassRecommendationsInProgressForCategorization: {
556
+ reducer(draft, action) {
557
+ const { selectedTab, transactionId, lineIds } = action.payload;
558
+ const tab = draft.transactionCategorizationView[selectedTab];
559
+ const transactionLocalData = recordGet(tab.transactionReviewLocalDataById, transactionId.id, undefined);
560
+ if (transactionLocalData != null) {
561
+ lineIds.forEach((lineId) => {
562
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
563
+ if (lineItem != null) {
564
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
565
+ ...lineItem,
566
+ categoryClassRecommendationsFetchState: {
567
+ fetchState: 'In-Progress',
568
+ error: undefined,
569
+ },
570
+ };
571
+ }
572
+ });
573
+ }
574
+ // Uncheck on fetch start: the row's existing category/class are
575
+ // stale after a payee change, so we drop the auto-selection until
576
+ // fresh recommendations land.
577
+ if (tab.selectedCheckBoxTransactionIds.includes(transactionId.id)) {
578
+ tab.selectedCheckBoxTransactionIds =
579
+ tab.selectedCheckBoxTransactionIds.filter((id) => id !== transactionId.id);
580
+ }
581
+ },
582
+ prepare(selectedTab, transactionId, lineIds) {
583
+ return { payload: { selectedTab, transactionId, lineIds } };
584
+ },
585
+ },
586
+ // Completed arm. Used by the epic when the HTTP call succeeds but the
587
+ // server returned no per-line recommendations — we still need to
588
+ // release the In-Progress flag so the skeleton resolves. Successful
589
+ // responses with recommendations go through
590
+ // setEntityRecommendationForLineIdsForCategorization instead (which
591
+ // also flips the lines to Completed).
592
+ markCategoryClassRecommendationsCompletedForCategorization: {
593
+ reducer(draft, action) {
594
+ const { selectedTab, transactionId, lineIds } = action.payload;
595
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
596
+ .transactionReviewLocalDataById, transactionId.id, undefined);
597
+ if (transactionLocalData != null) {
598
+ lineIds.forEach((lineId) => {
599
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
600
+ if (lineItem != null) {
601
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
602
+ ...lineItem,
603
+ categoryClassRecommendationsFetchState: {
604
+ fetchState: 'Completed',
605
+ error: undefined,
606
+ },
607
+ };
608
+ }
609
+ });
610
+ }
611
+ },
612
+ prepare(selectedTab, transactionId, lineIds) {
613
+ return { payload: { selectedTab, transactionId, lineIds } };
614
+ },
615
+ },
616
+ // Failure arm. Flips covered lines to Error so the skeleton resolves
617
+ // and downstream UI can react to the failure. The transaction stays
618
+ // unchecked from the start-arm action.
619
+ markCategoryClassRecommendationsFailureForCategorization: {
620
+ reducer(draft, action) {
621
+ const { selectedTab, transactionId, lineIds, error } = action.payload;
622
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
623
+ .transactionReviewLocalDataById, transactionId.id, undefined);
624
+ if (transactionLocalData != null) {
625
+ lineIds.forEach((lineId) => {
626
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
627
+ if (lineItem != null) {
628
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
629
+ ...lineItem,
630
+ categoryClassRecommendationsFetchState: {
631
+ fetchState: 'Error',
632
+ error,
633
+ },
634
+ };
635
+ }
636
+ });
637
+ }
638
+ },
639
+ prepare(selectedTab, transactionId, lineIds, error) {
640
+ return { payload: { selectedTab, transactionId, lineIds, error } };
641
+ },
642
+ },
414
643
  fetchTransactionCategorizationSuccess(draft, action) {
415
644
  const { selectedTab, refreshViewInBackground } = action.payload;
416
645
  if (draft.transactionCategorizationView[selectedTab].fetchState ===
@@ -506,9 +735,7 @@ const expenseAutomationTransactionsView = createSlice({
506
735
  .selectedCheckBoxTransactionIds;
507
736
  if (newTransactionLocalData.customer != null &&
508
737
  newTransactionLocalData.customer.id != null &&
509
- checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
510
- selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
511
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
738
+ shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
512
739
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
513
740
  ...selectedCheckBoxTransactionIds,
514
741
  transactionId,
@@ -582,9 +809,7 @@ const expenseAutomationTransactionsView = createSlice({
582
809
  .selectedCheckBoxTransactionIds;
583
810
  if (newTransactionLocalData.vendor != null &&
584
811
  newTransactionLocalData.vendor.id != null &&
585
- checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
586
- selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
587
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
812
+ shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
588
813
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
589
814
  ...selectedCheckBoxTransactionIds,
590
815
  transactionId,
@@ -697,6 +922,10 @@ const expenseAutomationTransactionsView = createSlice({
697
922
  const { selectedTab, selectedPeriod, totalCount } = action.payload;
698
923
  draft.transactionCategorizationView[selectedTab].uiState.totalCount[toMonthYearPeriodId(selectedPeriod)] = totalCount;
699
924
  },
925
+ updateParentTotalCountForTab(draft, action) {
926
+ const { selectedTab, selectedPeriod, parentTotalCount } = action.payload;
927
+ draft.parentTotalCountByTab[selectedTab][toMonthYearPeriodId(selectedPeriod)] = parentTotalCount;
928
+ },
700
929
  updateSelectedCheckboxTransactionIds(draft, action) {
701
930
  const selectedTab = action.payload.selectedTab;
702
931
  const transactionIds = action.payload.transactionIds;
@@ -707,6 +936,16 @@ const expenseAutomationTransactionsView = createSlice({
707
936
  draft.transactionCategorizationView[selectedTab] = {
708
937
  ...initialTransactionTabViewState,
709
938
  };
939
+ draft.parentTotalCountByTab[selectedTab] = {};
940
+ // The per-sub-tab snapshot cache lives at the slice root and is
941
+ // exclusively populated by the autoCategorized tab's sub-tab switching
942
+ // (see updateTransactionCategorizationCompletedSubTab). When that tab
943
+ // is reset, leaving stale snapshots behind would short-circuit the
944
+ // next sub-tab visit with phantom row IDs / page tokens that no longer
945
+ // correspond to anything in state — drop them in lockstep.
946
+ if (selectedTab === 'autoCategorized') {
947
+ draft.autoCategorizedSubTabCache = {};
948
+ }
710
949
  },
711
950
  clearExpenseAutomationTransactionsView(draft) {
712
951
  Object.assign(draft, initialState);
@@ -803,5 +1042,5 @@ const expenseAutomationTransactionsView = createSlice({
803
1042
  },
804
1043
  },
805
1044
  });
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;
1045
+ 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, markCategoryClassRecommendationsInProgressForCategorization, markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
807
1046
  export default expenseAutomationTransactionsView.reducer;
@@ -1,17 +1,90 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
1
2
  import omit from 'lodash/omit';
2
3
  import { reduceAllFetchState, reduceAnyFetchState, } from '../../../commonStateTypes/reduceFetchState';
3
4
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
4
5
  import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
5
6
  import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
6
7
  import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
8
+ import { applyTransactionFilters, } from '../transactionFilterHelpers';
7
9
  import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
8
10
  import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
9
11
  import { getProjectList } from '../../projectList/projectListSelector';
10
12
  import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
13
+ // Walks the tab's transactionReviewLocalDataById map and returns
14
+ // `{ [transactionId]: true }` for every transaction that has at least
15
+ // one line whose targeted-fetch is In-Progress. Pulled out as a plain
16
+ // function so each tab's createSelector can share it.
17
+ const computeCategorizationInFlightRecommendations = (transactionReviewLocalDataById) => {
18
+ const result = {};
19
+ if (transactionReviewLocalDataById == null) {
20
+ return result;
21
+ }
22
+ for (const [transactionId, data] of Object.entries(transactionReviewLocalDataById)) {
23
+ const lineItems = Object.values(data.transactionReviewLocalData.lineItemById);
24
+ if (lineItems.some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
25
+ 'In-Progress')) {
26
+ result[transactionId] = true;
27
+ }
28
+ }
29
+ return result;
30
+ };
31
+ // Per-tab createSelector instances. Each one is memoized on its OWN tab's
32
+ // transactionReviewLocalDataById reference. Switching tabs A->B->A no
33
+ // longer busts the memo for tab A — the dispatcher below just returns
34
+ // whichever per-tab selector matches the active tab, and reselect keeps
35
+ // the prior result alive as long as the underlying slice ref is stable.
36
+ const getReviewInFlightRecommendationsByTransactionId = createSelector((state) => state.expenseAutomationTransactionsViewState.transactionCategorizationView
37
+ .review.transactionReviewLocalDataById, computeCategorizationInFlightRecommendations);
38
+ const getAutoCategorizedInFlightRecommendationsByTransactionId = createSelector((state) => state.expenseAutomationTransactionsViewState.transactionCategorizationView
39
+ .autoCategorized.transactionReviewLocalDataById, computeCategorizationInFlightRecommendations);
40
+ // Per-transaction "any line is fetching category/class recommendations?"
41
+ // map for the active categorization tab. Dispatches to the per-tab
42
+ // memoized selector above so steady state (no fetches in flight)
43
+ // recomputes only when the active tab's local data changes; tab switches
44
+ // are O(1) cache lookups.
45
+ //
46
+ // `default` guards against the union "lying" at runtime: `TransactionsTab`
47
+ // is produced via `stringToUnion`, which falls back to `s as StringUnion`
48
+ // for unrecognized strings, so an unexpected value can land in state. The
49
+ // downstream `getCategorizationHasInFlightRecommendations` calls
50
+ // `Object.keys` on this result, so we must always return an object.
51
+ export const getCategorizationInFlightRecommendationsByTransactionId = (state) => {
52
+ const tab = state.expenseAutomationTransactionsViewState
53
+ .selectedTransactionCategorizationTab;
54
+ switch (tab) {
55
+ case 'review':
56
+ return getReviewInFlightRecommendationsByTransactionId(state);
57
+ case 'autoCategorized':
58
+ return getAutoCategorizedInFlightRecommendationsByTransactionId(state);
59
+ default:
60
+ return {};
61
+ }
62
+ };
63
+ // Page-level "any line on the active tab is in flight?" flag. Drives the
64
+ // list-page Save disable.
65
+ export const getCategorizationHasInFlightRecommendations = createSelector(getCategorizationInFlightRecommendationsByTransactionId, (inFlightByTransactionId) => Object.keys(inFlightByTransactionId).length > 0);
66
+ export const toTransactionView = (transaction, transactionLocalData) => {
67
+ return {
68
+ id: transaction.id,
69
+ date: transaction.date,
70
+ amount: transaction.amount,
71
+ vendorName: transaction.vendorName,
72
+ customerName: transaction.customerName,
73
+ vendorId: transaction.vendorId,
74
+ customerId: transaction.customerId,
75
+ description: transaction.description,
76
+ memo: transaction.memo,
77
+ type: transaction.type,
78
+ typeName: transaction.typeName,
79
+ createTime: transaction.createTime,
80
+ transaction: transaction,
81
+ transactionLocalData,
82
+ };
83
+ };
11
84
  export function getExpenseAutomationTransactionView(state) {
12
85
  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];
86
+ const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
87
+ const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, filters, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
15
88
  const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
16
89
  const accountList = getAccountList(accountState, accountListState, 'accountList');
17
90
  const classList = getClassList(classState, classListState);
@@ -30,20 +103,58 @@ export function getExpenseAutomationTransactionView(state) {
30
103
  ? transactionIdsBySelectedPeriod[monthYearPeriodId]
31
104
  : null;
32
105
  const transactionIds = transactionIdsForSelectedPeriod ?? [];
33
- const transactionLocalData = transactionIds
34
- .map((transactionId) => {
106
+ const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
107
+ const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
108
+ const transactionLocalDataMap = new Map();
109
+ transactionIds.forEach((transactionId) => {
35
110
  const transactionData = transactionReviewLocalDataById[transactionId];
36
111
  if (transactionData != null) {
37
- return {
112
+ transactionLocalDataMap.set(transactionId, {
38
113
  transactionId,
39
114
  transactionReviewLocalData: transactionData.transactionReviewLocalData,
40
- };
115
+ });
41
116
  }
42
- return null;
43
- })
44
- .filter((transaction) => transaction != null);
45
- const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
46
- const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
117
+ });
118
+ // Step 2: Convert to TransactionView and apply advanced filters
119
+ const transactionViews = transactionsWithCOT.map((transaction) => {
120
+ const localData = transactionLocalDataMap.get(transaction.id);
121
+ return toTransactionView(transaction, localData);
122
+ });
123
+ // Apply TC-specific filters (lives under expenseAutomationView, independent
124
+ // of the Spend Management filter pipeline).
125
+ const filteredTransactionViews = applyTransactionFilters(transactionViews, filters);
126
+ // Convert back to TransactionWithCOT for return
127
+ const filteredTransactionsWithCOT = filteredTransactionViews.map((view) => view.transaction);
128
+ // Get transactionLocalData for filtered transactions
129
+ // Also include transactionLocalData for transactions that don't exist in transactionState
130
+ // but have local data (to match original behavior)
131
+ const filteredTransactionIds = new Set(filteredTransactionViews.map((view) => view.id));
132
+ // Pre-compute the membership set once. The previous implementation called
133
+ // `transactions.some(t => t.id === transactionId)` inside the per-id loop,
134
+ // turning the pipeline into O(n × m) — measurable on tenants with large
135
+ // transaction lists since this selector runs on every Redux dispatch.
136
+ const transactionExistsById = new Set(transactions.map((t) => t.id));
137
+ // `.flatMap` returns `X[]` directly when the callback returns `[]` for the
138
+ // skip case, so we don't need the trailing `.filter((d): d is X => …)` type
139
+ // predicate that the previous `map → filter` pipeline required.
140
+ const filteredTransactionLocalData = transactionIds.flatMap((transactionId) => {
141
+ const transactionData = transactionReviewLocalDataById[transactionId];
142
+ if (transactionData == null) {
143
+ return [];
144
+ }
145
+ // Include if transaction is in filtered results, or if transaction doesn't exist in state
146
+ // (to maintain backward compatibility with tests)
147
+ const transactionExists = transactionExistsById.has(transactionId);
148
+ if (filteredTransactionIds.has(transactionId) || !transactionExists) {
149
+ return [
150
+ {
151
+ transactionId,
152
+ transactionReviewLocalData: transactionData.transactionReviewLocalData,
153
+ },
154
+ ];
155
+ }
156
+ return [];
157
+ });
47
158
  const monthEndFetchState = monthYearPeriodId != null
48
159
  ? (monthEndCloseChecksViewState.monthEndCloseChecksViewByTenantId[currentTenant.tenantId]?.[monthYearPeriodId] ?? { fetchState: 'Not-Started', error: undefined })
49
160
  : { fetchState: 'Not-Started', error: undefined };
@@ -63,10 +174,27 @@ export function getExpenseAutomationTransactionView(state) {
63
174
  .uiState.totalCount;
64
175
  const totalCountByAutoCat = expenseAutomationTransactionsViewState.transactionCategorizationView
65
176
  .autoCategorized.uiState.totalCount;
177
+ // Per-(sub-)tab counts powering pagination, in-list "showing X of Y", and
178
+ // any save/refresh round-trips. Sourced from `uiState.totalCount` which
179
+ // mirrors the response's `total_count` for whatever tab/sub-tab the user is
180
+ // viewing (e.g. only the Manual sub-tab rows for Completed → Manual).
66
181
  const totalCountByTab = {
67
182
  review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
68
183
  autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
69
184
  };
185
+ // Parent-tab badge counts. Sourced from `parent_tab_total_count` in the
186
+ // listing response and used exclusively by the navbar / tab strip badges.
187
+ // Independent of which Completed sub-tab is active so the badge stays
188
+ // stable across sub-tab switches.
189
+ const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
190
+ const parentTabTotalCountByTab = {
191
+ review: monthYearPeriodId != null
192
+ ? (parentTotalCountByTab.review[monthYearPeriodId] ?? 0)
193
+ : 0,
194
+ autoCategorized: monthYearPeriodId != null
195
+ ? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
196
+ : 0,
197
+ };
70
198
  const fetchStateByTab = {
71
199
  review: {
72
200
  fetchState: expenseAutomationTransactionsViewState.transactionCategorizationView
@@ -86,11 +214,13 @@ export function getExpenseAutomationTransactionView(state) {
86
214
  : [];
87
215
  const transactionCompletionStatus = allSteps.find((step) => step.step === 'transaction_categorization')?.completionStatus;
88
216
  const completionStatus = transactionCompletionStatus ?? 'incomplete';
217
+ const inFlightCategoryClassRecommendationsByTransactionId = getCategorizationInFlightRecommendationsByTransactionId(state);
218
+ const hasInFlightCategoryClassRecommendations = getCategorizationHasInFlightRecommendations(state);
89
219
  return {
90
220
  version: 0,
91
221
  fetchState: fetchStatus.fetchState,
92
222
  error: fetchStatus.error,
93
- transactions: transactionsWithCOT,
223
+ transactions: filteredTransactionsWithCOT,
94
224
  selectedCheckBoxTransactionIds,
95
225
  transactionIdsWithUnsavedData,
96
226
  uncategorizedAccounts: uncategorizedIncomeExpense,
@@ -99,18 +229,22 @@ export function getExpenseAutomationTransactionView(state) {
99
229
  classList: allClasses,
100
230
  accountsHierarchyList,
101
231
  classHierarchyList,
232
+ hasInFlightCategoryClassRecommendations,
233
+ inFlightCategoryClassRecommendationsByTransactionId,
102
234
  isAccountingProjectsEnabled,
103
235
  projectList,
104
- transactionLocalData,
236
+ transactionLocalData: filteredTransactionLocalData,
105
237
  uiState,
106
238
  refreshStatus,
107
239
  saveStatus,
108
240
  markAsNotMiscategorizedStatus,
109
241
  completionStatus,
110
242
  totalCountByTab,
243
+ parentTabTotalCountByTab,
111
244
  fetchStateByTransactionTabs: fetchStateByTab,
112
245
  uploadReceiptStatusById,
113
246
  selectedTransactionId,
114
247
  selectedTransactionLineId,
248
+ selectedTransactionCategorizationCompletedSubTab,
115
249
  };
116
250
  }