@zeniai/client-epic-state 5.1.17 → 5.1.18-betaRD2

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 (23) hide show
  1. package/lib/entity/customer/customerPayload.d.ts +7 -1
  2. package/lib/entity/customer/customerPayload.js +32 -9
  3. package/lib/entity/reimbursement/reimbursementPayload.js +2 -1
  4. package/lib/entity/transaction/payloadTypes/customerTransactionPayload.js +3 -1
  5. package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +1 -1
  6. package/lib/entity/transaction/payloadTypes/vendorTransactionPayload.d.ts +1 -1
  7. package/lib/entity/transactionActivityLog/transactionActivityLogPayload.js +4 -1
  8. package/lib/esm/entity/customer/customerPayload.js +30 -8
  9. package/lib/esm/entity/reimbursement/reimbursementPayload.js +3 -2
  10. package/lib/esm/entity/transaction/payloadTypes/customerTransactionPayload.js +3 -1
  11. package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +2 -2
  12. package/lib/esm/entity/transactionActivityLog/transactionActivityLogPayload.js +5 -2
  13. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +20 -4
  14. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +48 -4
  15. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +92 -18
  16. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +19 -3
  17. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.d.ts +2 -2
  18. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +47 -3
  19. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +7 -0
  20. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +4 -1
  21. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +93 -19
  22. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +16 -0
  23. package/package.json +1 -1
@@ -1,11 +1,17 @@
1
1
  import { URLPayload } from '../../commonPayloadTypes/urlPayload';
2
2
  import { Customer, CustomerBase } from './customerState';
3
3
  export interface CustomerBasePayload {
4
- customer_id: string;
4
+ customer_id: string | null;
5
5
  customer_name: string;
6
6
  logo?: URLPayload;
7
7
  qbo_id?: string;
8
8
  }
9
+ /**
10
+ * Resolves the effective customer identifier from a payload.
11
+ * For JE/Deposit transactions, customer_id is null at runtime — qbo_id is the
12
+ * actual identity.
13
+ */
14
+ export declare const getCustomerPayloadIdentifier: (customer: CustomerBasePayload) => string | null;
9
15
  export declare const toCustomerBase: (payload: CustomerBasePayload, parseOutParentName: boolean) => CustomerBase;
10
16
  export declare const toCustomerBasePayload: (customer: CustomerBase | undefined, customerUpdates?: CustomerBase) => CustomerBasePayload;
11
17
  export interface CustomerPayload extends CustomerBasePayload {
@@ -1,16 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toCustomer = exports.toCustomerBasePayload = exports.toCustomerBase = void 0;
3
+ exports.toCustomer = exports.toCustomerBasePayload = exports.toCustomerBase = exports.getCustomerPayloadIdentifier = void 0;
4
4
  const urlPayload_1 = require("../../commonPayloadTypes/urlPayload");
5
5
  const zeniDayJS_1 = require("../../zeniDayJS");
6
- const toCustomerBase = (payload, parseOutParentName) => ({
7
- id: payload.customer_id,
8
- name: parseOutParentName
9
- ? removeParentName(payload.customer_name)
10
- : payload.customer_name,
11
- qboId: payload.qbo_id,
12
- logo: payload.logo != null ? (0, urlPayload_1.toURL)(payload.logo) : undefined,
13
- });
6
+ /**
7
+ * Resolves the effective customer identifier from a payload.
8
+ * For JE/Deposit transactions, customer_id is null at runtime — qbo_id is the
9
+ * actual identity.
10
+ */
11
+ const getCustomerPayloadIdentifier = (customer) => {
12
+ const cid = customer.customer_id;
13
+ if (cid != null && cid !== '') {
14
+ return cid;
15
+ }
16
+ const qid = customer.qbo_id;
17
+ if (qid != null && qid !== '') {
18
+ return qid;
19
+ }
20
+ return null;
21
+ };
22
+ exports.getCustomerPayloadIdentifier = getCustomerPayloadIdentifier;
23
+ const toCustomerBase = (payload, parseOutParentName) => {
24
+ const id = (0, exports.getCustomerPayloadIdentifier)(payload);
25
+ if (id == null) {
26
+ throw new Error(`toCustomerBase: no identifier (customer_id/qbo_id) found for customer "${payload.customer_name}".`);
27
+ }
28
+ return {
29
+ id,
30
+ name: parseOutParentName
31
+ ? removeParentName(payload.customer_name)
32
+ : payload.customer_name,
33
+ qboId: payload.qbo_id,
34
+ logo: payload.logo != null ? (0, urlPayload_1.toURL)(payload.logo) : undefined,
35
+ };
36
+ };
14
37
  exports.toCustomerBase = toCustomerBase;
15
38
  const toCustomerBasePayload = (customer, customerUpdates) => {
16
39
  if (customerUpdates != null && customerUpdates.id != null) {
@@ -184,7 +184,8 @@ const toLineDetail = (payload) => ({
184
184
  account: payload.account?.account_id != null
185
185
  ? (0, accountPayload_1.mapAccountBasePayloadToAccountBase)(payload.account)
186
186
  : undefined,
187
- customer: payload.customer?.customer_id != null
187
+ customer: payload.customer != null &&
188
+ (0, customerPayload_1.getCustomerPayloadIdentifier)(payload.customer) != null
188
189
  ? (0, customerPayload_1.toCustomerBase)(payload.customer, false)
189
190
  : undefined,
190
191
  });
@@ -12,7 +12,9 @@ const toCustomerTransaction = (payload) => {
12
12
  const transaction = (0, transactionPayload_1.toTransaction)(payload);
13
13
  return {
14
14
  ...transaction,
15
- customer: payload.customer_id != null ? (0, customerPayload_1.toCustomerBase)(payload, false) : undefined,
15
+ customer: Boolean(payload.customer_id) === true
16
+ ? (0, customerPayload_1.toCustomerBase)(payload, false)
17
+ : undefined,
16
18
  documentId: payload.document_id,
17
19
  paymentType: (0, paymentType_1.toPaymentTypeStrict)(payload.payment_type),
18
20
  paymentTypeName: payload.payment_type_name,
@@ -39,7 +39,7 @@ const toTransactionWithAccountAndClassLine = (payload, currency, type = 'transac
39
39
  class: (0, classPayload_1.mapClassBasePayloadToClassBase)(payload.line_detail.class ?? {}), // Note: why bother about empty {} object ? because sometimes backend sends empty objects, sometimes it sends undefined or null
40
40
  account: (0, accountPayload_1.mapAccountBasePayloadToAccountBase)(payload.line_detail.account),
41
41
  customer: payload.line_detail.customer != null &&
42
- payload.line_detail.customer.customer_id != null
42
+ (0, customerPayload_1.getCustomerPayloadIdentifier)(payload.line_detail.customer) != null
43
43
  ? (0, customerPayload_1.toCustomerBase)(payload.line_detail.customer, false)
44
44
  : undefined,
45
45
  vendor: payload.line_detail.vendor != null &&
@@ -12,7 +12,7 @@ export interface VendorTransactionPayload extends VendorBasePayload, Omit<Transa
12
12
  payment_type_name?: string;
13
13
  }
14
14
  export interface VendorTransactionWithCustomerPayload extends VendorTransactionPayload {
15
- customer_id: string;
15
+ customer_id: string | null;
16
16
  customer_name: string;
17
17
  }
18
18
  export declare const isVendorTransactionPayload: (payload: TransactionPayload) => boolean;
@@ -18,7 +18,10 @@ const toTransactionActivityLogLine = (line) => {
18
18
  class: line.accounting_class
19
19
  ? (0, classPayload_1.mapClassBasePayloadToClassBase)(line.accounting_class)
20
20
  : undefined,
21
- customer: line.customer ? (0, customerPayload_1.toCustomerBase)(line.customer, false) : undefined,
21
+ customer: line.customer != null &&
22
+ (0, customerPayload_1.getCustomerPayloadIdentifier)(line.customer) != null
23
+ ? (0, customerPayload_1.toCustomerBase)(line.customer, false)
24
+ : undefined,
22
25
  vendor: line.vendor != null && (0, vendorPayload_1.getVendorPayloadIdentifier)(line.vendor) != null
23
26
  ? (0, vendorPayload_1.mapVendorBasePayloadToVendorBase)(line.vendor)
24
27
  : undefined,
@@ -1,13 +1,35 @@
1
1
  import { toURL } from '../../commonPayloadTypes/urlPayload';
2
2
  import { date } from '../../zeniDayJS';
3
- export const toCustomerBase = (payload, parseOutParentName) => ({
4
- id: payload.customer_id,
5
- name: parseOutParentName
6
- ? removeParentName(payload.customer_name)
7
- : payload.customer_name,
8
- qboId: payload.qbo_id,
9
- logo: payload.logo != null ? toURL(payload.logo) : undefined,
10
- });
3
+ /**
4
+ * Resolves the effective customer identifier from a payload.
5
+ * For JE/Deposit transactions, customer_id is null at runtime — qbo_id is the
6
+ * actual identity.
7
+ */
8
+ export const getCustomerPayloadIdentifier = (customer) => {
9
+ const cid = customer.customer_id;
10
+ if (cid != null && cid !== '') {
11
+ return cid;
12
+ }
13
+ const qid = customer.qbo_id;
14
+ if (qid != null && qid !== '') {
15
+ return qid;
16
+ }
17
+ return null;
18
+ };
19
+ export const toCustomerBase = (payload, parseOutParentName) => {
20
+ const id = getCustomerPayloadIdentifier(payload);
21
+ if (id == null) {
22
+ throw new Error(`toCustomerBase: no identifier (customer_id/qbo_id) found for customer "${payload.customer_name}".`);
23
+ }
24
+ return {
25
+ id,
26
+ name: parseOutParentName
27
+ ? removeParentName(payload.customer_name)
28
+ : payload.customer_name,
29
+ qboId: payload.qbo_id,
30
+ logo: payload.logo != null ? toURL(payload.logo) : undefined,
31
+ };
32
+ };
11
33
  export const toCustomerBasePayload = (customer, customerUpdates) => {
12
34
  if (customerUpdates != null && customerUpdates.id != null) {
13
35
  return {
@@ -5,7 +5,7 @@ import { date, dateNow } from '../../zeniDayJS';
5
5
  import { mapAccountBasePayloadToAccountBase, toAccountPayload, } from '../account/accountPayload';
6
6
  import { toBillPaymentMethod, toBillPaymentStatus, toBillStage, toBillStatus, toBulkProcessingDetail, toDeletionInfo, } from '../billPay/billTransaction/billTransactionPayload';
7
7
  import { mapClassBasePayloadToClassBase, toClassBasePayload, } from '../class/classPayload';
8
- import { toCustomerBase, toCustomerBasePayload, } from '../customer/customerPayload';
8
+ import { getCustomerPayloadIdentifier, toCustomerBase, toCustomerBasePayload, } from '../customer/customerPayload';
9
9
  import { mapMerchantBasePayloadToMerchantBase, toMerchantBasePayload, } from '../merchant/merchantPayload';
10
10
  import { toAttachment, } from '../transaction/payloadTypes/attachmentPayload';
11
11
  const ALL_REIMBURSEMENT_TRANSACTION_TYPE = ['reimbursement'];
@@ -178,7 +178,8 @@ const toLineDetail = (payload) => ({
178
178
  account: payload.account?.account_id != null
179
179
  ? mapAccountBasePayloadToAccountBase(payload.account)
180
180
  : undefined,
181
- customer: payload.customer?.customer_id != null
181
+ customer: payload.customer != null &&
182
+ getCustomerPayloadIdentifier(payload.customer) != null
182
183
  ? toCustomerBase(payload.customer, false)
183
184
  : undefined,
184
185
  });
@@ -8,7 +8,9 @@ export const toCustomerTransaction = (payload) => {
8
8
  const transaction = toTransaction(payload);
9
9
  return {
10
10
  ...transaction,
11
- customer: payload.customer_id != null ? toCustomerBase(payload, false) : undefined,
11
+ customer: Boolean(payload.customer_id) === true
12
+ ? toCustomerBase(payload, false)
13
+ : undefined,
12
14
  documentId: payload.document_id,
13
15
  paymentType: toPaymentTypeStrict(payload.payment_type),
14
16
  paymentTypeName: payload.payment_type_name,
@@ -1,7 +1,7 @@
1
1
  import { toAmountWC } from '../../../commonStateTypes/amount';
2
2
  import { mapAccountBasePayloadToAccountBase, toAccountPayload, } from '../../account/accountPayload';
3
3
  import { mapClassBasePayloadToClassBase, toClassBasePayload, } from '../../class/classPayload';
4
- import { toCustomerBase, toCustomerBasePayload, } from '../../customer/customerPayload';
4
+ import { getCustomerPayloadIdentifier, toCustomerBase, toCustomerBasePayload, } from '../../customer/customerPayload';
5
5
  import { mapProjectBasePayloadToProjectBase, toProjectBasePayload, } from '../../project/projectPayload';
6
6
  import { getVendorPayloadIdentifier, mapVendorBasePayloadToVendorBase, toVendorBasePayload, } from '../../vendor/vendorPayload';
7
7
  import { toCategorizationStatusType, toPlatformLineDetailType, } from '../stateTypes/transactionLine';
@@ -35,7 +35,7 @@ export const toTransactionWithAccountAndClassLine = (payload, currency, type = '
35
35
  class: mapClassBasePayloadToClassBase(payload.line_detail.class ?? {}), // Note: why bother about empty {} object ? because sometimes backend sends empty objects, sometimes it sends undefined or null
36
36
  account: mapAccountBasePayloadToAccountBase(payload.line_detail.account),
37
37
  customer: payload.line_detail.customer != null &&
38
- payload.line_detail.customer.customer_id != null
38
+ getCustomerPayloadIdentifier(payload.line_detail.customer) != null
39
39
  ? toCustomerBase(payload.line_detail.customer, false)
40
40
  : undefined,
41
41
  vendor: payload.line_detail.vendor != null &&
@@ -1,7 +1,7 @@
1
1
  import { date } from '../../zeniDayJS';
2
2
  import { mapAccountBasePayloadToAccountBase, } from '../account/accountPayload';
3
3
  import { mapClassBasePayloadToClassBase, } from '../class/classPayload';
4
- import { toCustomerBase } from '../customer/customerPayload';
4
+ import { getCustomerPayloadIdentifier, toCustomerBase } from '../customer/customerPayload';
5
5
  import { toAISummary, } from '../transaction/payloadTypes/transactionLinePayload';
6
6
  import { getVendorPayloadIdentifier, mapVendorBasePayloadToVendorBase, } from '../vendor/vendorPayload';
7
7
  import { toActivityLogEventType, } from './transactionActivityLogState';
@@ -14,7 +14,10 @@ const toTransactionActivityLogLine = (line) => {
14
14
  class: line.accounting_class
15
15
  ? mapClassBasePayloadToClassBase(line.accounting_class)
16
16
  : undefined,
17
- customer: line.customer ? toCustomerBase(line.customer, false) : undefined,
17
+ customer: line.customer != null &&
18
+ getCustomerPayloadIdentifier(line.customer) != null
19
+ ? toCustomerBase(line.customer, false)
20
+ : undefined,
18
21
  vendor: line.vendor != null && getVendorPayloadIdentifier(line.vendor) != null
19
22
  ? mapVendorBasePayloadToVendorBase(line.vendor)
20
23
  : undefined,
@@ -1,5 +1,5 @@
1
1
  import { from, of } from 'rxjs';
2
- import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
2
+ import { catchError, filter, groupBy, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { toString } from '../../../../commonStateTypes/timePeriod';
4
4
  import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
5
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
@@ -8,7 +8,10 @@ import { getLineItemsByTransactionsIdsFromResponse } from '../../helpers/transac
8
8
  import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateParentTotalCountForTab, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
9
9
  import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
10
10
  import { TRANSACTIONS_TABS, toTransactionsSortKey, } from '../../types/transactionsViewState';
11
- export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match), switchMap((action) => {
11
+ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match),
12
+ // Per-tab switchMap: cancels in-flight requests for the same tab on a new
13
+ // request, but allows both tabs to fetch concurrently (e.g. dual search).
14
+ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pipe(switchMap((action) => {
12
15
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationTransactionsViewState, } = state$.value;
13
16
  const { period, keepExistingListItems, selectedTab, refreshViewInBackground, } = action.payload;
14
17
  const uiState = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTab].uiState;
@@ -24,6 +27,10 @@ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) =>
24
27
  page_token: pageToken,
25
28
  page_size: 25,
26
29
  search_text: uiState.searchString,
30
+ // Always scope to the active tab's auto_categorized status;
31
+ // cross-status search (omit or true) is reserved for Receipts
32
+ // manual-match.
33
+ search_all_statuses: false,
27
34
  sub_tab: selectedTab === 'autoCategorized'
28
35
  ? expenseAutomationTransactionsViewState.selectedTransactionCategorizationCompletedSubTab
29
36
  : DEFAULT_COMPLETED_SUB_TAB,
@@ -36,10 +43,19 @@ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) =>
36
43
  updateActions.push(updateTransactions(response.data.transactions, (value) => value.transaction_id, 'merge'));
37
44
  const transactionIds = response.data.transactions.map((transaction) => transaction.transaction_id);
38
45
  const lineItemsByTransactionIds = getLineItemsByTransactionsIdsFromResponse(response.data.transactions);
46
+ // Only propagate the fields the response provides (pageToken).
47
+ // Do NOT spread the captured uiState — that would re-apply
48
+ // searchString/sortKey/etc. from request-start and overwrite
49
+ // any state changes made during the in-flight request (e.g.
50
+ // user clearing search triggers restoreFullDatasetSnapshot
51
+ // before this response lands).
52
+ // pageToken is also set by saveTransactionCategorizationLocalData
53
+ // via initializeTransactionCategorizationViewLocalDataEpic, so
54
+ // null values are handled correctly there regardless of the
55
+ // != null guard in updateTransactionCategorizationUIState.
39
56
  updateActions.push(updateTransactionCategorizationUIState({
40
57
  selectedTab,
41
58
  uiState: {
42
- ...uiState,
43
59
  pageToken: response.data.next_page_token ?? null,
44
60
  },
45
61
  }));
@@ -79,4 +95,4 @@ export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) =>
79
95
  status: createZeniAPIStatus('Unexpected Error', 'fetch Transaction Categorization REST API call errored out' +
80
96
  JSON.stringify(error)),
81
97
  }))));
82
- }));
98
+ }))));
@@ -6,7 +6,8 @@ import { fetchAccountList } from '../../../accountList/accountListReducer';
6
6
  import { fetchClassList } from '../../../classList/classListReducer';
7
7
  import { fetchOwnerList } from '../../../ownerList/ownerListReducer';
8
8
  import { fetchProjectList } from '../../../projectList/projectListReducer';
9
- import { fetchTransactionCategorization, fetchTransactionCategorizationView, } from '../../reducers/transactionsViewReducer';
9
+ import { fetchTransactionCategorization, fetchTransactionCategorizationView, restoreFullDatasetSnapshot, } from '../../reducers/transactionsViewReducer';
10
+ import { TRANSACTIONS_TABS, } from '../../types/transactionsViewState';
10
11
  export const fetchTransactionCategorizationViewEpic = (actions$, state$) => actions$.pipe(filter(fetchTransactionCategorizationView.match), mergeMap((action) => {
11
12
  const { selectedTab, cacheOverride, keepExistingListItems, pageToken, period, refreshViewInBackground, searchString, resetListItems, isUncategorizedExpenseCategoryEnabled, } = action.payload;
12
13
  const updateActions = [];
@@ -16,8 +17,14 @@ export const fetchTransactionCategorizationViewEpic = (actions$, state$) => acti
16
17
  month: period.start.month,
17
18
  year: period.start.year,
18
19
  };
19
- const transactionIds = transactionIdsBySelectedPeriod[toMonthYearPeriodId(monthYearPeriod)] ??
20
- [];
20
+ const periodId = toMonthYearPeriodId(monthYearPeriod);
21
+ const transactionIds = transactionIdsBySelectedPeriod[periodId] ?? [];
22
+ const hasUsableSnapshot = (tabKey) => {
23
+ const snapshot = expenseAutomationTransactionsViewState.transactionCategorizationView[tabKey].fullDatasetSnapshot;
24
+ return (snapshot != null &&
25
+ snapshot.periodId === periodId &&
26
+ snapshot.transactionIds.length > 0);
27
+ };
21
28
  const accountList = state$.value.accountListState.byReportId['accountList'];
22
29
  if (accountList.hasValidState() === false &&
23
30
  accountList.fetchState === 'Not-Started') {
@@ -39,10 +46,47 @@ export const fetchTransactionCategorizationViewEpic = (actions$, state$) => acti
39
46
  vendorOwnerList.fetchState === 'Not-Started') {
40
47
  updateActions.push(fetchOwnerList());
41
48
  }
42
- if (cacheOverride === true ||
49
+ // When clearing search (searchString === ''), restore from the pre-search
50
+ // snapshot if one exists — avoids a round-trip since all data is already
51
+ // in the entity store. Fall back to a normal fetch if no snapshot (the
52
+ // tab was never loaded before the search was applied).
53
+ if (searchString === '' && hasUsableSnapshot(selectedTab)) {
54
+ updateActions.push(restoreFullDatasetSnapshot({ selectedTab, period }));
55
+ }
56
+ else if (cacheOverride === true ||
43
57
  fetchState === 'Not-Started' ||
44
58
  transactionIds.length === 0) {
45
59
  updateActions.push(fetchTransactionCategorization(selectedTab, period, cacheOverride, keepExistingListItems, searchString, pageToken, refreshViewInBackground, resetListItems, isUncategorizedExpenseCategoryEnabled));
46
60
  }
61
+ // On the initial load of a tab (searchString undefined = non-search fetch),
62
+ // preload every other tab that hasn't been fetched yet for this period.
63
+ // This makes switching to the other tab instant instead of triggering a
64
+ // fresh fetch on click. refreshViewInBackground=true keeps it silent.
65
+ if (searchString === undefined) {
66
+ TRANSACTIONS_TABS.filter((tab) => tab !== selectedTab).forEach((otherTab) => {
67
+ const otherTabFetchState = expenseAutomationTransactionsViewState.transactionCategorizationView[otherTab].fetchState;
68
+ if (otherTabFetchState === 'Not-Started') {
69
+ updateActions.push(fetchTransactionCategorization(otherTab, period, false, false, undefined, undefined, true, undefined, isUncategorizedExpenseCategoryEnabled));
70
+ }
71
+ });
72
+ }
73
+ // When search changes (searchString defined, including empty string to
74
+ // clear), also update every other tab so all tabs always reflect the same
75
+ // search query. Skip if the other tab already has this search string to
76
+ // avoid redundant work on noop updates. On clear, restore from snapshot
77
+ // if available; otherwise issue a background fetch.
78
+ if (searchString !== undefined) {
79
+ TRANSACTIONS_TABS.filter((tab) => tab !== selectedTab).forEach((otherTab) => {
80
+ const otherTabSearchString = expenseAutomationTransactionsViewState.transactionCategorizationView[otherTab].uiState.searchString;
81
+ if (searchString !== otherTabSearchString) {
82
+ if (searchString === '' && hasUsableSnapshot(otherTab)) {
83
+ updateActions.push(restoreFullDatasetSnapshot({ selectedTab: otherTab, period }));
84
+ }
85
+ else {
86
+ updateActions.push(fetchTransactionCategorization(otherTab, period, true, keepExistingListItems, searchString, undefined, true, true, isUncategorizedExpenseCategoryEnabled));
87
+ }
88
+ }
89
+ });
90
+ }
47
91
  return from(updateActions);
48
92
  }));
@@ -38,6 +38,7 @@ export const initialTransactionTabViewState = {
38
38
  hasValidState() {
39
39
  return this.fetchState == 'Completed';
40
40
  },
41
+ fullDatasetSnapshot: undefined,
41
42
  transactionReviewLocalDataById: {},
42
43
  transactionIdsBySelectedPeriod: {},
43
44
  selectedCheckBoxTransactionIds: [],
@@ -156,24 +157,52 @@ const expenseAutomationTransactionsView = createSlice({
156
157
  }
157
158
  draft.transactionCategorizationView[selectedTab].uiState.pageToken =
158
159
  pageToken ?? null;
159
- draft.transactionCategorizationView[selectedTab].uiState.searchString =
160
- searchString ?? '';
160
+ // Only overwrite when explicitly set; undefined means a non-search fetch
161
+ // (tab switch, period change) should preserve the existing search string.
162
+ if (searchString !== undefined) {
163
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
164
+ searchString;
165
+ }
161
166
  draft.transactionCategorizationView[selectedTab].error = undefined;
167
+ const monthYearPeriod = {
168
+ month: period.start.month,
169
+ year: period.start.year,
170
+ };
171
+ const periodId = toMonthYearPeriodId(monthYearPeriod);
172
+ const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
173
+ .transactionIdsBySelectedPeriod[periodId] ?? [];
174
+ // Snapshot before any list mutation so clearing the search restores
175
+ // instantly without an API round-trip — for BOTH active and synced tabs.
176
+ // Active-tab search dispatches resetListItems=false, so this must live
177
+ // outside the resetListItems block. fullDatasetSnapshot == null prevents
178
+ // overwriting the original snapshot on search refinement ('ama'→'amazon').
179
+ if (searchString !== undefined &&
180
+ searchString !== '' &&
181
+ transactionIdsBySelectedPeriod.length > 0 &&
182
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot ==
183
+ null) {
184
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
185
+ {
186
+ parentTotalCount: draft.parentTotalCountByTab[selectedTab][periodId],
187
+ periodId,
188
+ totalCount: draft.transactionCategorizationView[selectedTab].uiState
189
+ .totalCount[periodId],
190
+ transactionIds: [...transactionIdsBySelectedPeriod],
191
+ transactionReviewLocalDataById: {
192
+ ...draft.transactionCategorizationView[selectedTab]
193
+ .transactionReviewLocalDataById,
194
+ },
195
+ };
196
+ }
162
197
  // Reset list when we change the sort order and fetch list again
163
198
  if (resetListItems === true) {
164
- const monthYearPeriod = {
165
- month: period.start.month,
166
- year: period.start.year,
167
- };
168
- const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
169
- .transactionIdsBySelectedPeriod[toMonthYearPeriodId(monthYearPeriod)];
170
199
  transactionIdsBySelectedPeriod.forEach((transactionId) => {
171
200
  delete draft.transactionCategorizationView[selectedTab]
172
201
  .transactionReviewLocalDataById[transactionId];
173
202
  });
174
203
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds =
175
204
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds.filter((selectedCheckBoxTransactionId) => transactionIdsBySelectedPeriod.includes(selectedCheckBoxTransactionId));
176
- draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[toMonthYearPeriodId(monthYearPeriod)] = [];
205
+ draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[periodId] = [];
177
206
  draft.transactionCategorizationView[selectedTab].refreshStatus = {
178
207
  fetchState: 'Not-Started',
179
208
  error: undefined,
@@ -305,10 +334,12 @@ const expenseAutomationTransactionsView = createSlice({
305
334
  }
306
335
  },
307
336
  updateTransactionFilters(draft, action) {
308
- const { selectedTab, filters } = action.payload;
309
- // `filters` is non-nullable per the payload type, so no runtime
310
- // null-check is needed dropping it removes a dead defensive branch.
311
- draft.transactionCategorizationView[selectedTab].filters = filters;
337
+ const { filters } = action.payload;
338
+ // Filters are tab-agnostic (payee/category/class/amount), so apply to
339
+ // all tabs so switching tabs after filtering shows consistent results.
340
+ TRANSACTIONS_TABS.forEach((tab) => {
341
+ draft.transactionCategorizationView[tab].filters = filters;
342
+ });
312
343
  },
313
344
  saveTransactionCategorization: {
314
345
  prepare(selectedTab, transactionIds, cotTrackingByTransactionId, isUncategorizedExpenseCategoryEnabled) {
@@ -660,19 +691,62 @@ const expenseAutomationTransactionsView = createSlice({
660
691
  resetOtherTabsFetchState(draft, action) {
661
692
  const { tabs, refreshViewInBackground } = action.payload;
662
693
  tabs.forEach((tab) => {
663
- if (draft.transactionCategorizationView[tab].fetchState ===
664
- 'In-Progress' ||
665
- refreshViewInBackground !== true) {
694
+ const tabFetchState = draft.transactionCategorizationView[tab].fetchState;
695
+ if (refreshViewInBackground !== true) {
696
+ // Foreground fetch completed: mark other tabs as Completed.
666
697
  draft.transactionCategorizationView[tab].fetchState = 'Completed';
667
698
  draft.transactionCategorizationView[tab].error = undefined;
668
699
  }
669
- else {
700
+ else if (tabFetchState !== 'In-Progress') {
701
+ // Background fetch completed: reset the other tab's refreshStatus,
702
+ // but only when it is not mid-load. During a dual-search the active
703
+ // tab has its own foreground fetch in flight — clobbering its
704
+ // fetchState here would drop the skeleton and show an empty list.
670
705
  draft.transactionCategorizationView[tab].refreshStatus = {
671
706
  fetchState: 'Not-Started',
672
707
  error: undefined,
673
708
  };
674
709
  }
710
+ // If tabFetchState === 'In-Progress' and refreshViewInBackground === true,
711
+ // leave the other tab's state untouched — it has its own active fetch.
712
+ });
713
+ },
714
+ restoreFullDatasetSnapshot(draft, action) {
715
+ const { selectedTab, period } = action.payload;
716
+ const periodId = toMonthYearPeriodId({
717
+ month: period.start.month,
718
+ year: period.start.year,
719
+ });
720
+ const snapshot = draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot;
721
+ if (snapshot == null || snapshot.periodId !== periodId) {
722
+ return;
723
+ }
724
+ draft.transactionCategorizationView[selectedTab]
725
+ .transactionIdsBySelectedPeriod[periodId] = snapshot.transactionIds;
726
+ // Merge back per-transaction local data; skip entries that were updated
727
+ // during the search (e.g. a save that happened while filtered results
728
+ // were showing) so those saves are not overwritten.
729
+ Object.entries(snapshot.transactionReviewLocalDataById).forEach(([transactionId, localData]) => {
730
+ if (draft.transactionCategorizationView[selectedTab]
731
+ .transactionReviewLocalDataById[transactionId] == null) {
732
+ draft.transactionCategorizationView[selectedTab]
733
+ .transactionReviewLocalDataById[transactionId] = localData;
734
+ }
675
735
  });
736
+ if (snapshot.totalCount != null) {
737
+ draft.transactionCategorizationView[selectedTab].uiState.totalCount[periodId] = snapshot.totalCount;
738
+ }
739
+ if (snapshot.parentTotalCount != null) {
740
+ draft.parentTotalCountByTab[selectedTab][periodId] =
741
+ snapshot.parentTotalCount;
742
+ }
743
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
744
+ '';
745
+ draft.transactionCategorizationView[selectedTab].uiState.pageToken = null;
746
+ draft.transactionCategorizationView[selectedTab].fetchState = 'Completed';
747
+ draft.transactionCategorizationView[selectedTab].error = undefined;
748
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
749
+ undefined;
676
750
  },
677
751
  fetchTransactionCategorizationFailure(draft, action) {
678
752
  const { selectedTab, selectedPeriod } = action.payload;
@@ -1054,5 +1128,5 @@ const expenseAutomationTransactionsView = createSlice({
1054
1128
  },
1055
1129
  },
1056
1130
  });
1057
- 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, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
1131
+ export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, restoreFullDatasetSnapshot, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, markCategoryClassRecommendationsInProgressForCategorization, markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
1058
1132
  export default expenseAutomationTransactionsView.reducer;
@@ -11,7 +11,10 @@ const transactionCategorizationLocalDataHelper_1 = require("../../helpers/transa
11
11
  const transactionsViewReducer_1 = require("../../reducers/transactionsViewReducer");
12
12
  const completedSubTab_1 = require("../../types/completedSubTab");
13
13
  const transactionsViewState_1 = require("../../types/transactionsViewState");
14
- const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(transactionsViewReducer_1.fetchTransactionCategorization.match), (0, operators_1.switchMap)((action) => {
14
+ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(transactionsViewReducer_1.fetchTransactionCategorization.match),
15
+ // Per-tab switchMap: cancels in-flight requests for the same tab on a new
16
+ // request, but allows both tabs to fetch concurrently (e.g. dual search).
17
+ (0, operators_1.groupBy)((action) => action.payload.selectedTab), (0, operators_1.mergeMap)((group$) => group$.pipe((0, operators_1.switchMap)((action) => {
15
18
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationTransactionsViewState, } = state$.value;
16
19
  const { period, keepExistingListItems, selectedTab, refreshViewInBackground, } = action.payload;
17
20
  const uiState = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTab].uiState;
@@ -27,6 +30,10 @@ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => action
27
30
  page_token: pageToken,
28
31
  page_size: 25,
29
32
  search_text: uiState.searchString,
33
+ // Always scope to the active tab's auto_categorized status;
34
+ // cross-status search (omit or true) is reserved for Receipts
35
+ // manual-match.
36
+ search_all_statuses: false,
30
37
  sub_tab: selectedTab === 'autoCategorized'
31
38
  ? expenseAutomationTransactionsViewState.selectedTransactionCategorizationCompletedSubTab
32
39
  : completedSubTab_1.DEFAULT_COMPLETED_SUB_TAB,
@@ -39,10 +46,19 @@ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => action
39
46
  updateActions.push((0, transactionReducer_1.updateTransactions)(response.data.transactions, (value) => value.transaction_id, 'merge'));
40
47
  const transactionIds = response.data.transactions.map((transaction) => transaction.transaction_id);
41
48
  const lineItemsByTransactionIds = (0, transactionCategorizationLocalDataHelper_1.getLineItemsByTransactionsIdsFromResponse)(response.data.transactions);
49
+ // Only propagate the fields the response provides (pageToken).
50
+ // Do NOT spread the captured uiState — that would re-apply
51
+ // searchString/sortKey/etc. from request-start and overwrite
52
+ // any state changes made during the in-flight request (e.g.
53
+ // user clearing search triggers restoreFullDatasetSnapshot
54
+ // before this response lands).
55
+ // pageToken is also set by saveTransactionCategorizationLocalData
56
+ // via initializeTransactionCategorizationViewLocalDataEpic, so
57
+ // null values are handled correctly there regardless of the
58
+ // != null guard in updateTransactionCategorizationUIState.
42
59
  updateActions.push((0, transactionsViewReducer_1.updateTransactionCategorizationUIState)({
43
60
  selectedTab,
44
61
  uiState: {
45
- ...uiState,
46
62
  pageToken: response.data.next_page_token ?? null,
47
63
  },
48
64
  }));
@@ -82,5 +98,5 @@ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => action
82
98
  status: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'fetch Transaction Categorization REST API call errored out' +
83
99
  JSON.stringify(error)),
84
100
  }))));
85
- }));
101
+ }))));
86
102
  exports.fetchTransactionCategorizationEpic = fetchTransactionCategorizationEpic;
@@ -5,6 +5,6 @@ import { fetchAccountList } from '../../../accountList/accountListReducer';
5
5
  import { fetchClassList } from '../../../classList/classListReducer';
6
6
  import { fetchOwnerList } from '../../../ownerList/ownerListReducer';
7
7
  import { fetchProjectList } from '../../../projectList/projectListReducer';
8
- import { fetchTransactionCategorization, fetchTransactionCategorizationView } from '../../reducers/transactionsViewReducer';
9
- export type ActionType = ReturnType<typeof fetchTransactionCategorizationView> | ReturnType<typeof fetchTransactionCategorization> | ReturnType<typeof fetchAccountList> | ReturnType<typeof fetchClassList> | ReturnType<typeof fetchOwnerList> | ReturnType<typeof fetchProjectList>;
8
+ import { fetchTransactionCategorization, fetchTransactionCategorizationView, restoreFullDatasetSnapshot } from '../../reducers/transactionsViewReducer';
9
+ export type ActionType = ReturnType<typeof fetchTransactionCategorizationView> | ReturnType<typeof fetchTransactionCategorization> | ReturnType<typeof restoreFullDatasetSnapshot> | ReturnType<typeof fetchAccountList> | ReturnType<typeof fetchClassList> | ReturnType<typeof fetchOwnerList> | ReturnType<typeof fetchProjectList>;
10
10
  export declare const fetchTransactionCategorizationViewEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>) => Observable<ActionType>;
@@ -10,6 +10,7 @@ const classListReducer_1 = require("../../../classList/classListReducer");
10
10
  const ownerListReducer_1 = require("../../../ownerList/ownerListReducer");
11
11
  const projectListReducer_1 = require("../../../projectList/projectListReducer");
12
12
  const transactionsViewReducer_1 = require("../../reducers/transactionsViewReducer");
13
+ const transactionsViewState_1 = require("../../types/transactionsViewState");
13
14
  const fetchTransactionCategorizationViewEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(transactionsViewReducer_1.fetchTransactionCategorizationView.match), (0, operators_1.mergeMap)((action) => {
14
15
  const { selectedTab, cacheOverride, keepExistingListItems, pageToken, period, refreshViewInBackground, searchString, resetListItems, isUncategorizedExpenseCategoryEnabled, } = action.payload;
15
16
  const updateActions = [];
@@ -19,8 +20,14 @@ const fetchTransactionCategorizationViewEpic = (actions$, state$) => actions$.pi
19
20
  month: period.start.month,
20
21
  year: period.start.year,
21
22
  };
22
- const transactionIds = transactionIdsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod)] ??
23
- [];
23
+ const periodId = (0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod);
24
+ const transactionIds = transactionIdsBySelectedPeriod[periodId] ?? [];
25
+ const hasUsableSnapshot = (tabKey) => {
26
+ const snapshot = expenseAutomationTransactionsViewState.transactionCategorizationView[tabKey].fullDatasetSnapshot;
27
+ return (snapshot != null &&
28
+ snapshot.periodId === periodId &&
29
+ snapshot.transactionIds.length > 0);
30
+ };
24
31
  const accountList = state$.value.accountListState.byReportId['accountList'];
25
32
  if (accountList.hasValidState() === false &&
26
33
  accountList.fetchState === 'Not-Started') {
@@ -42,11 +49,48 @@ const fetchTransactionCategorizationViewEpic = (actions$, state$) => actions$.pi
42
49
  vendorOwnerList.fetchState === 'Not-Started') {
43
50
  updateActions.push((0, ownerListReducer_1.fetchOwnerList)());
44
51
  }
45
- if (cacheOverride === true ||
52
+ // When clearing search (searchString === ''), restore from the pre-search
53
+ // snapshot if one exists — avoids a round-trip since all data is already
54
+ // in the entity store. Fall back to a normal fetch if no snapshot (the
55
+ // tab was never loaded before the search was applied).
56
+ if (searchString === '' && hasUsableSnapshot(selectedTab)) {
57
+ updateActions.push((0, transactionsViewReducer_1.restoreFullDatasetSnapshot)({ selectedTab, period }));
58
+ }
59
+ else if (cacheOverride === true ||
46
60
  fetchState === 'Not-Started' ||
47
61
  transactionIds.length === 0) {
48
62
  updateActions.push((0, transactionsViewReducer_1.fetchTransactionCategorization)(selectedTab, period, cacheOverride, keepExistingListItems, searchString, pageToken, refreshViewInBackground, resetListItems, isUncategorizedExpenseCategoryEnabled));
49
63
  }
64
+ // On the initial load of a tab (searchString undefined = non-search fetch),
65
+ // preload every other tab that hasn't been fetched yet for this period.
66
+ // This makes switching to the other tab instant instead of triggering a
67
+ // fresh fetch on click. refreshViewInBackground=true keeps it silent.
68
+ if (searchString === undefined) {
69
+ transactionsViewState_1.TRANSACTIONS_TABS.filter((tab) => tab !== selectedTab).forEach((otherTab) => {
70
+ const otherTabFetchState = expenseAutomationTransactionsViewState.transactionCategorizationView[otherTab].fetchState;
71
+ if (otherTabFetchState === 'Not-Started') {
72
+ updateActions.push((0, transactionsViewReducer_1.fetchTransactionCategorization)(otherTab, period, false, false, undefined, undefined, true, undefined, isUncategorizedExpenseCategoryEnabled));
73
+ }
74
+ });
75
+ }
76
+ // When search changes (searchString defined, including empty string to
77
+ // clear), also update every other tab so all tabs always reflect the same
78
+ // search query. Skip if the other tab already has this search string to
79
+ // avoid redundant work on noop updates. On clear, restore from snapshot
80
+ // if available; otherwise issue a background fetch.
81
+ if (searchString !== undefined) {
82
+ transactionsViewState_1.TRANSACTIONS_TABS.filter((tab) => tab !== selectedTab).forEach((otherTab) => {
83
+ const otherTabSearchString = expenseAutomationTransactionsViewState.transactionCategorizationView[otherTab].uiState.searchString;
84
+ if (searchString !== otherTabSearchString) {
85
+ if (searchString === '' && hasUsableSnapshot(otherTab)) {
86
+ updateActions.push((0, transactionsViewReducer_1.restoreFullDatasetSnapshot)({ selectedTab: otherTab, period }));
87
+ }
88
+ else {
89
+ updateActions.push((0, transactionsViewReducer_1.fetchTransactionCategorization)(otherTab, period, true, keepExistingListItems, searchString, undefined, true, true, isUncategorizedExpenseCategoryEnabled));
90
+ }
91
+ }
92
+ });
93
+ }
50
94
  return (0, rxjs_1.from)(updateActions);
51
95
  }));
52
96
  exports.fetchTransactionCategorizationViewEpic = fetchTransactionCategorizationViewEpic;
@@ -19,6 +19,13 @@ export interface TransactionCategorizationQueryPayload {
19
19
  * on the backend.
20
20
  */
21
21
  sub_tab: CompletedSubTab;
22
+ /**
23
+ * When `false`, the backend scopes the search to the active tab's
24
+ * categorization status (respects `auto_categorized`). When `true` or
25
+ * omitted, the backend searches across all statuses — the legacy behaviour
26
+ * that the Receipts manual-match flow relies on.
27
+ */
28
+ search_all_statuses?: boolean;
22
29
  }
23
30
  export interface TransactionCategorizationPayload {
24
31
  next_page_token: string | null;
@@ -137,7 +137,10 @@ export declare const fetchTransactionCategorization: import("@reduxjs/toolkit").
137
137
  }, "expenseAutomationTransactionsView/fetchTransactionCategorizationSuccess">, resetOtherTabsFetchState: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
138
138
  tabs: TransactionsTab[];
139
139
  refreshViewInBackground?: boolean;
140
- }, "expenseAutomationTransactionsView/resetOtherTabsFetchState">, fetchTransactionCategorizationView: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", period: TimePeriod, cacheOverride?: any, keepExistingListItems?: any, searchString?: string | undefined, pageToken?: PageToken | undefined, refreshViewInBackground?: any, resetListItems?: any, isUncategorizedExpenseCategoryEnabled?: boolean | undefined], {
140
+ }, "expenseAutomationTransactionsView/resetOtherTabsFetchState">, restoreFullDatasetSnapshot: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
141
+ period: TimePeriod;
142
+ selectedTab: TransactionsTab;
143
+ }, "expenseAutomationTransactionsView/restoreFullDatasetSnapshot">, fetchTransactionCategorizationView: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", period: TimePeriod, cacheOverride?: any, keepExistingListItems?: any, searchString?: string | undefined, pageToken?: PageToken | undefined, refreshViewInBackground?: any, resetListItems?: any, isUncategorizedExpenseCategoryEnabled?: boolean | undefined], {
141
144
  selectedTab: "review" | "autoCategorized";
142
145
  period: TimePeriod;
143
146
  searchString: string | undefined;
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  var _a;
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.uploadTransactionCategorizationReceiptSuccess = exports.updateTransactionCategorizationUploadReceiptState = exports.removeTransactionFromAllTabs = exports.backgroundRefetchReviewTab = exports.syncTransactionCategorizationFromDetailSave = exports.updateSelectedTransactionId = exports.markCategoryClassRecommendationsFailureForCategorization = exports.markCategoryClassRecommendationsCompletedForCategorization = exports.markCategoryClassRecommendationsInProgressForCategorization = exports.setEntityRecommendationForLineIdsForCategorization = exports.clearExpenseAutomationTransactionsView = exports.clearExpenseAutomationTransactionsViewPerTabView = exports.updateSelectedCheckboxTransactionIds = exports.fetchTransactionCategorizationView = exports.resetOtherTabsFetchState = exports.fetchTransactionCategorizationSuccess = exports.updateParentTotalCountForTab = exports.updateTotalCountForTransactionCategorization = exports.setAllItemsToCategoryClassInLocalDataForCategorization = exports.updateSelectedCustomerForTransaction = exports.updateSelectedVendorForTransaction = exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = exports.markTransactionAsNotMiscategorized = exports.updateTransactionCategorizationSaveStatus = exports.updateTransactionCategorizationCompletedSubTab = exports.updateCurrentSelectedTransactionCategorizationTab = exports.updateTransactionCategorization = exports.saveTransactionCategorization = exports.fetchTransactionCategorizationFailure = exports.saveTransactionCategorizationLocalData = exports.initializeTransactionCategorizationViewLocalData = exports.updateTransactionFilters = exports.updateTransactionCategorizationUIState = exports.fetchTransactionCategorization = exports.initialState = exports.initialTransactionTabViewState = void 0;
7
+ exports.uploadTransactionCategorizationReceiptSuccess = exports.updateTransactionCategorizationUploadReceiptState = exports.removeTransactionFromAllTabs = exports.backgroundRefetchReviewTab = exports.syncTransactionCategorizationFromDetailSave = exports.updateSelectedTransactionId = exports.markCategoryClassRecommendationsFailureForCategorization = exports.markCategoryClassRecommendationsCompletedForCategorization = exports.markCategoryClassRecommendationsInProgressForCategorization = exports.setEntityRecommendationForLineIdsForCategorization = exports.clearExpenseAutomationTransactionsView = exports.clearExpenseAutomationTransactionsViewPerTabView = exports.updateSelectedCheckboxTransactionIds = exports.fetchTransactionCategorizationView = exports.restoreFullDatasetSnapshot = exports.resetOtherTabsFetchState = exports.fetchTransactionCategorizationSuccess = exports.updateParentTotalCountForTab = exports.updateTotalCountForTransactionCategorization = exports.setAllItemsToCategoryClassInLocalDataForCategorization = exports.updateSelectedCustomerForTransaction = exports.updateSelectedVendorForTransaction = exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = exports.markTransactionAsNotMiscategorized = exports.updateTransactionCategorizationSaveStatus = exports.updateTransactionCategorizationCompletedSubTab = exports.updateCurrentSelectedTransactionCategorizationTab = exports.updateTransactionCategorization = exports.saveTransactionCategorization = exports.fetchTransactionCategorizationFailure = exports.saveTransactionCategorizationLocalData = exports.initializeTransactionCategorizationViewLocalData = exports.updateTransactionFilters = exports.updateTransactionCategorizationUIState = exports.fetchTransactionCategorization = exports.initialState = exports.initialTransactionTabViewState = void 0;
8
8
  const toolkit_1 = require("@reduxjs/toolkit");
9
9
  const get_1 = __importDefault(require("lodash/get"));
10
10
  const uniq_1 = __importDefault(require("lodash/uniq"));
@@ -45,6 +45,7 @@ exports.initialTransactionTabViewState = {
45
45
  hasValidState() {
46
46
  return this.fetchState == 'Completed';
47
47
  },
48
+ fullDatasetSnapshot: undefined,
48
49
  transactionReviewLocalDataById: {},
49
50
  transactionIdsBySelectedPeriod: {},
50
51
  selectedCheckBoxTransactionIds: [],
@@ -163,24 +164,52 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
163
164
  }
164
165
  draft.transactionCategorizationView[selectedTab].uiState.pageToken =
165
166
  pageToken ?? null;
166
- draft.transactionCategorizationView[selectedTab].uiState.searchString =
167
- searchString ?? '';
167
+ // Only overwrite when explicitly set; undefined means a non-search fetch
168
+ // (tab switch, period change) should preserve the existing search string.
169
+ if (searchString !== undefined) {
170
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
171
+ searchString;
172
+ }
168
173
  draft.transactionCategorizationView[selectedTab].error = undefined;
174
+ const monthYearPeriod = {
175
+ month: period.start.month,
176
+ year: period.start.year,
177
+ };
178
+ const periodId = (0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod);
179
+ const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
180
+ .transactionIdsBySelectedPeriod[periodId] ?? [];
181
+ // Snapshot before any list mutation so clearing the search restores
182
+ // instantly without an API round-trip — for BOTH active and synced tabs.
183
+ // Active-tab search dispatches resetListItems=false, so this must live
184
+ // outside the resetListItems block. fullDatasetSnapshot == null prevents
185
+ // overwriting the original snapshot on search refinement ('ama'→'amazon').
186
+ if (searchString !== undefined &&
187
+ searchString !== '' &&
188
+ transactionIdsBySelectedPeriod.length > 0 &&
189
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot ==
190
+ null) {
191
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
192
+ {
193
+ parentTotalCount: draft.parentTotalCountByTab[selectedTab][periodId],
194
+ periodId,
195
+ totalCount: draft.transactionCategorizationView[selectedTab].uiState
196
+ .totalCount[periodId],
197
+ transactionIds: [...transactionIdsBySelectedPeriod],
198
+ transactionReviewLocalDataById: {
199
+ ...draft.transactionCategorizationView[selectedTab]
200
+ .transactionReviewLocalDataById,
201
+ },
202
+ };
203
+ }
169
204
  // Reset list when we change the sort order and fetch list again
170
205
  if (resetListItems === true) {
171
- const monthYearPeriod = {
172
- month: period.start.month,
173
- year: period.start.year,
174
- };
175
- const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
176
- .transactionIdsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod)];
177
206
  transactionIdsBySelectedPeriod.forEach((transactionId) => {
178
207
  delete draft.transactionCategorizationView[selectedTab]
179
208
  .transactionReviewLocalDataById[transactionId];
180
209
  });
181
210
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds =
182
211
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds.filter((selectedCheckBoxTransactionId) => transactionIdsBySelectedPeriod.includes(selectedCheckBoxTransactionId));
183
- draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod)] = [];
212
+ draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[periodId] = [];
184
213
  draft.transactionCategorizationView[selectedTab].refreshStatus = {
185
214
  fetchState: 'Not-Started',
186
215
  error: undefined,
@@ -312,10 +341,12 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
312
341
  }
313
342
  },
314
343
  updateTransactionFilters(draft, action) {
315
- const { selectedTab, filters } = action.payload;
316
- // `filters` is non-nullable per the payload type, so no runtime
317
- // null-check is needed dropping it removes a dead defensive branch.
318
- draft.transactionCategorizationView[selectedTab].filters = filters;
344
+ const { filters } = action.payload;
345
+ // Filters are tab-agnostic (payee/category/class/amount), so apply to
346
+ // all tabs so switching tabs after filtering shows consistent results.
347
+ transactionsViewState_1.TRANSACTIONS_TABS.forEach((tab) => {
348
+ draft.transactionCategorizationView[tab].filters = filters;
349
+ });
319
350
  },
320
351
  saveTransactionCategorization: {
321
352
  prepare(selectedTab, transactionIds, cotTrackingByTransactionId, isUncategorizedExpenseCategoryEnabled) {
@@ -667,19 +698,62 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
667
698
  resetOtherTabsFetchState(draft, action) {
668
699
  const { tabs, refreshViewInBackground } = action.payload;
669
700
  tabs.forEach((tab) => {
670
- if (draft.transactionCategorizationView[tab].fetchState ===
671
- 'In-Progress' ||
672
- refreshViewInBackground !== true) {
701
+ const tabFetchState = draft.transactionCategorizationView[tab].fetchState;
702
+ if (refreshViewInBackground !== true) {
703
+ // Foreground fetch completed: mark other tabs as Completed.
673
704
  draft.transactionCategorizationView[tab].fetchState = 'Completed';
674
705
  draft.transactionCategorizationView[tab].error = undefined;
675
706
  }
676
- else {
707
+ else if (tabFetchState !== 'In-Progress') {
708
+ // Background fetch completed: reset the other tab's refreshStatus,
709
+ // but only when it is not mid-load. During a dual-search the active
710
+ // tab has its own foreground fetch in flight — clobbering its
711
+ // fetchState here would drop the skeleton and show an empty list.
677
712
  draft.transactionCategorizationView[tab].refreshStatus = {
678
713
  fetchState: 'Not-Started',
679
714
  error: undefined,
680
715
  };
681
716
  }
717
+ // If tabFetchState === 'In-Progress' and refreshViewInBackground === true,
718
+ // leave the other tab's state untouched — it has its own active fetch.
719
+ });
720
+ },
721
+ restoreFullDatasetSnapshot(draft, action) {
722
+ const { selectedTab, period } = action.payload;
723
+ const periodId = (0, timePeriod_1.toMonthYearPeriodId)({
724
+ month: period.start.month,
725
+ year: period.start.year,
726
+ });
727
+ const snapshot = draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot;
728
+ if (snapshot == null || snapshot.periodId !== periodId) {
729
+ return;
730
+ }
731
+ draft.transactionCategorizationView[selectedTab]
732
+ .transactionIdsBySelectedPeriod[periodId] = snapshot.transactionIds;
733
+ // Merge back per-transaction local data; skip entries that were updated
734
+ // during the search (e.g. a save that happened while filtered results
735
+ // were showing) so those saves are not overwritten.
736
+ Object.entries(snapshot.transactionReviewLocalDataById).forEach(([transactionId, localData]) => {
737
+ if (draft.transactionCategorizationView[selectedTab]
738
+ .transactionReviewLocalDataById[transactionId] == null) {
739
+ draft.transactionCategorizationView[selectedTab]
740
+ .transactionReviewLocalDataById[transactionId] = localData;
741
+ }
682
742
  });
743
+ if (snapshot.totalCount != null) {
744
+ draft.transactionCategorizationView[selectedTab].uiState.totalCount[periodId] = snapshot.totalCount;
745
+ }
746
+ if (snapshot.parentTotalCount != null) {
747
+ draft.parentTotalCountByTab[selectedTab][periodId] =
748
+ snapshot.parentTotalCount;
749
+ }
750
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
751
+ '';
752
+ draft.transactionCategorizationView[selectedTab].uiState.pageToken = null;
753
+ draft.transactionCategorizationView[selectedTab].fetchState = 'Completed';
754
+ draft.transactionCategorizationView[selectedTab].error = undefined;
755
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
756
+ undefined;
683
757
  },
684
758
  fetchTransactionCategorizationFailure(draft, action) {
685
759
  const { selectedTab, selectedPeriod } = action.payload;
@@ -1061,5 +1135,5 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
1061
1135
  },
1062
1136
  },
1063
1137
  });
1064
- _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, exports.updateTransactionFilters = _a.updateTransactionFilters, exports.initializeTransactionCategorizationViewLocalData = _a.initializeTransactionCategorizationViewLocalData, exports.saveTransactionCategorizationLocalData = _a.saveTransactionCategorizationLocalData, exports.fetchTransactionCategorizationFailure = _a.fetchTransactionCategorizationFailure, exports.saveTransactionCategorization = _a.saveTransactionCategorization, exports.updateTransactionCategorization = _a.updateTransactionCategorization, exports.updateCurrentSelectedTransactionCategorizationTab = _a.updateCurrentSelectedTransactionCategorizationTab, exports.updateTransactionCategorizationCompletedSubTab = _a.updateTransactionCategorizationCompletedSubTab, exports.updateTransactionCategorizationSaveStatus = _a.updateTransactionCategorizationSaveStatus, exports.markTransactionAsNotMiscategorized = _a.markTransactionAsNotMiscategorized, exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = _a.updateStatusForTransactionNotMiscategorizedUpdateForCategorization, exports.updateSelectedVendorForTransaction = _a.updateSelectedVendorForTransaction, exports.updateSelectedCustomerForTransaction = _a.updateSelectedCustomerForTransaction, exports.setAllItemsToCategoryClassInLocalDataForCategorization = _a.setAllItemsToCategoryClassInLocalDataForCategorization, exports.updateTotalCountForTransactionCategorization = _a.updateTotalCountForTransactionCategorization, exports.updateParentTotalCountForTab = _a.updateParentTotalCountForTab, exports.fetchTransactionCategorizationSuccess = _a.fetchTransactionCategorizationSuccess, exports.resetOtherTabsFetchState = _a.resetOtherTabsFetchState, exports.fetchTransactionCategorizationView = _a.fetchTransactionCategorizationView, exports.updateSelectedCheckboxTransactionIds = _a.updateSelectedCheckboxTransactionIds, exports.clearExpenseAutomationTransactionsViewPerTabView = _a.clearExpenseAutomationTransactionsViewPerTabView, exports.clearExpenseAutomationTransactionsView = _a.clearExpenseAutomationTransactionsView, exports.setEntityRecommendationForLineIdsForCategorization = _a.setEntityRecommendationForLineIdsForCategorization, exports.markCategoryClassRecommendationsInProgressForCategorization = _a.markCategoryClassRecommendationsInProgressForCategorization, exports.markCategoryClassRecommendationsCompletedForCategorization = _a.markCategoryClassRecommendationsCompletedForCategorization, exports.markCategoryClassRecommendationsFailureForCategorization = _a.markCategoryClassRecommendationsFailureForCategorization, exports.updateSelectedTransactionId = _a.updateSelectedTransactionId, exports.syncTransactionCategorizationFromDetailSave = _a.syncTransactionCategorizationFromDetailSave, exports.backgroundRefetchReviewTab = _a.backgroundRefetchReviewTab, exports.removeTransactionFromAllTabs = _a.removeTransactionFromAllTabs, exports.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
1138
+ _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, exports.updateTransactionFilters = _a.updateTransactionFilters, exports.initializeTransactionCategorizationViewLocalData = _a.initializeTransactionCategorizationViewLocalData, exports.saveTransactionCategorizationLocalData = _a.saveTransactionCategorizationLocalData, exports.fetchTransactionCategorizationFailure = _a.fetchTransactionCategorizationFailure, exports.saveTransactionCategorization = _a.saveTransactionCategorization, exports.updateTransactionCategorization = _a.updateTransactionCategorization, exports.updateCurrentSelectedTransactionCategorizationTab = _a.updateCurrentSelectedTransactionCategorizationTab, exports.updateTransactionCategorizationCompletedSubTab = _a.updateTransactionCategorizationCompletedSubTab, exports.updateTransactionCategorizationSaveStatus = _a.updateTransactionCategorizationSaveStatus, exports.markTransactionAsNotMiscategorized = _a.markTransactionAsNotMiscategorized, exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = _a.updateStatusForTransactionNotMiscategorizedUpdateForCategorization, exports.updateSelectedVendorForTransaction = _a.updateSelectedVendorForTransaction, exports.updateSelectedCustomerForTransaction = _a.updateSelectedCustomerForTransaction, exports.setAllItemsToCategoryClassInLocalDataForCategorization = _a.setAllItemsToCategoryClassInLocalDataForCategorization, exports.updateTotalCountForTransactionCategorization = _a.updateTotalCountForTransactionCategorization, exports.updateParentTotalCountForTab = _a.updateParentTotalCountForTab, exports.fetchTransactionCategorizationSuccess = _a.fetchTransactionCategorizationSuccess, exports.resetOtherTabsFetchState = _a.resetOtherTabsFetchState, exports.restoreFullDatasetSnapshot = _a.restoreFullDatasetSnapshot, exports.fetchTransactionCategorizationView = _a.fetchTransactionCategorizationView, exports.updateSelectedCheckboxTransactionIds = _a.updateSelectedCheckboxTransactionIds, exports.clearExpenseAutomationTransactionsViewPerTabView = _a.clearExpenseAutomationTransactionsViewPerTabView, exports.clearExpenseAutomationTransactionsView = _a.clearExpenseAutomationTransactionsView, exports.setEntityRecommendationForLineIdsForCategorization = _a.setEntityRecommendationForLineIdsForCategorization, exports.markCategoryClassRecommendationsInProgressForCategorization = _a.markCategoryClassRecommendationsInProgressForCategorization, exports.markCategoryClassRecommendationsCompletedForCategorization = _a.markCategoryClassRecommendationsCompletedForCategorization, exports.markCategoryClassRecommendationsFailureForCategorization = _a.markCategoryClassRecommendationsFailureForCategorization, exports.updateSelectedTransactionId = _a.updateSelectedTransactionId, exports.syncTransactionCategorizationFromDetailSave = _a.syncTransactionCategorizationFromDetailSave, exports.backgroundRefetchReviewTab = _a.backgroundRefetchReviewTab, exports.removeTransactionFromAllTabs = _a.removeTransactionFromAllTabs, exports.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
1065
1139
  exports.default = expenseAutomationTransactionsView.reducer;
@@ -94,8 +94,24 @@ export interface SupportedTransactionCategorization {
94
94
  transactionId?: ID;
95
95
  }
96
96
  export declare const initialSupportedTransactionCategorization: SupportedTransactionCategorization;
97
+ /**
98
+ * Snapshot of a tab's full (pre-search) dataset taken the moment the user
99
+ * first applies a non-empty search string. When the search is cleared the
100
+ * view epic restores from here instead of issuing a new API request.
101
+ *
102
+ * Only saved when there are actual rows to restore (`transactionIds` is
103
+ * non-empty); cleared once consumed or replaced by a fresh search.
104
+ */
105
+ export interface FullDatasetSnapshot {
106
+ parentTotalCount: number | undefined;
107
+ periodId: MonthYearPeriodId;
108
+ totalCount: number | undefined;
109
+ transactionIds: ID[];
110
+ transactionReviewLocalDataById: Record<ID | TransactionDetailKey, SupportedTransactionCategorization>;
111
+ }
97
112
  export interface TransactionsTabViewState extends FetchedState {
98
113
  filters: TransactionFilters;
114
+ fullDatasetSnapshot: FullDatasetSnapshot | undefined;
99
115
  markAsNotMiscategorizedStatus: FetchStateAndError;
100
116
  refreshStatus: FetchStateAndError;
101
117
  saveStatus: FetchStateAndError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.1.17",
3
+ "version": "5.1.18-betaRD2",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",