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

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 +84 -12
  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 +85 -13
  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,8 +157,12 @@ 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;
162
167
  // Reset list when we change the sort order and fetch list again
163
168
  if (resetListItems === true) {
@@ -165,8 +170,30 @@ const expenseAutomationTransactionsView = createSlice({
165
170
  month: period.start.month,
166
171
  year: period.start.year,
167
172
  };
173
+ const periodId = toMonthYearPeriodId(monthYearPeriod);
168
174
  const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
169
- .transactionIdsBySelectedPeriod[toMonthYearPeriodId(monthYearPeriod)];
175
+ .transactionIdsBySelectedPeriod[periodId] ?? [];
176
+ // When starting a non-empty search, snapshot the current full dataset
177
+ // so clearing the search can restore instantly without an API round-trip.
178
+ // Only save the snapshot when there are rows to restore; skip for tabs
179
+ // that have never been loaded (empty list → no useful snapshot).
180
+ if (searchString !== undefined &&
181
+ searchString !== '' &&
182
+ transactionIdsBySelectedPeriod.length > 0) {
183
+ const snapshot = {
184
+ parentTotalCount: draft.parentTotalCountByTab[selectedTab][periodId],
185
+ periodId,
186
+ totalCount: draft.transactionCategorizationView[selectedTab].uiState
187
+ .totalCount[periodId],
188
+ transactionIds: [...transactionIdsBySelectedPeriod],
189
+ transactionReviewLocalDataById: {
190
+ ...draft.transactionCategorizationView[selectedTab]
191
+ .transactionReviewLocalDataById,
192
+ },
193
+ };
194
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
195
+ snapshot;
196
+ }
170
197
  transactionIdsBySelectedPeriod.forEach((transactionId) => {
171
198
  delete draft.transactionCategorizationView[selectedTab]
172
199
  .transactionReviewLocalDataById[transactionId];
@@ -305,10 +332,12 @@ const expenseAutomationTransactionsView = createSlice({
305
332
  }
306
333
  },
307
334
  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;
335
+ const { filters } = action.payload;
336
+ // Filters are tab-agnostic (payee/category/class/amount), so apply to
337
+ // all tabs so switching tabs after filtering shows consistent results.
338
+ TRANSACTIONS_TABS.forEach((tab) => {
339
+ draft.transactionCategorizationView[tab].filters = filters;
340
+ });
312
341
  },
313
342
  saveTransactionCategorization: {
314
343
  prepare(selectedTab, transactionIds, cotTrackingByTransactionId, isUncategorizedExpenseCategoryEnabled) {
@@ -660,20 +689,63 @@ const expenseAutomationTransactionsView = createSlice({
660
689
  resetOtherTabsFetchState(draft, action) {
661
690
  const { tabs, refreshViewInBackground } = action.payload;
662
691
  tabs.forEach((tab) => {
663
- if (draft.transactionCategorizationView[tab].fetchState ===
664
- 'In-Progress' ||
665
- refreshViewInBackground !== true) {
692
+ const tabFetchState = draft.transactionCategorizationView[tab].fetchState;
693
+ if (refreshViewInBackground !== true) {
694
+ // Foreground fetch completed: mark other tabs as Completed.
666
695
  draft.transactionCategorizationView[tab].fetchState = 'Completed';
667
696
  draft.transactionCategorizationView[tab].error = undefined;
668
697
  }
669
- else {
698
+ else if (tabFetchState !== 'In-Progress') {
699
+ // Background fetch completed: reset the other tab's refreshStatus,
700
+ // but only when it is not mid-load. During a dual-search the active
701
+ // tab has its own foreground fetch in flight — clobbering its
702
+ // fetchState here would drop the skeleton and show an empty list.
670
703
  draft.transactionCategorizationView[tab].refreshStatus = {
671
704
  fetchState: 'Not-Started',
672
705
  error: undefined,
673
706
  };
674
707
  }
708
+ // If tabFetchState === 'In-Progress' and refreshViewInBackground === true,
709
+ // leave the other tab's state untouched — it has its own active fetch.
675
710
  });
676
711
  },
712
+ restoreFullDatasetSnapshot(draft, action) {
713
+ const { selectedTab, period } = action.payload;
714
+ const periodId = toMonthYearPeriodId({
715
+ month: period.start.month,
716
+ year: period.start.year,
717
+ });
718
+ const snapshot = draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot;
719
+ if (snapshot == null || snapshot.periodId !== periodId) {
720
+ return;
721
+ }
722
+ draft.transactionCategorizationView[selectedTab]
723
+ .transactionIdsBySelectedPeriod[periodId] = snapshot.transactionIds;
724
+ // Merge back per-transaction local data; skip entries that were updated
725
+ // during the search (e.g. a save that happened while filtered results
726
+ // were showing) so those saves are not overwritten.
727
+ Object.entries(snapshot.transactionReviewLocalDataById).forEach(([transactionId, localData]) => {
728
+ if (draft.transactionCategorizationView[selectedTab]
729
+ .transactionReviewLocalDataById[transactionId] == null) {
730
+ draft.transactionCategorizationView[selectedTab]
731
+ .transactionReviewLocalDataById[transactionId] = localData;
732
+ }
733
+ });
734
+ if (snapshot.totalCount != null) {
735
+ draft.transactionCategorizationView[selectedTab].uiState.totalCount[periodId] = snapshot.totalCount;
736
+ }
737
+ if (snapshot.parentTotalCount != null) {
738
+ draft.parentTotalCountByTab[selectedTab][periodId] =
739
+ snapshot.parentTotalCount;
740
+ }
741
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
742
+ '';
743
+ draft.transactionCategorizationView[selectedTab].uiState.pageToken = null;
744
+ draft.transactionCategorizationView[selectedTab].fetchState = 'Completed';
745
+ draft.transactionCategorizationView[selectedTab].error = undefined;
746
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
747
+ undefined;
748
+ },
677
749
  fetchTransactionCategorizationFailure(draft, action) {
678
750
  const { selectedTab, selectedPeriod } = action.payload;
679
751
  draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[toMonthYearPeriodId(selectedPeriod)] =
@@ -1054,5 +1126,5 @@ const expenseAutomationTransactionsView = createSlice({
1054
1126
  },
1055
1127
  },
1056
1128
  });
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;
1129
+ 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
1130
  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,8 +164,12 @@ 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;
169
174
  // Reset list when we change the sort order and fetch list again
170
175
  if (resetListItems === true) {
@@ -172,8 +177,30 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
172
177
  month: period.start.month,
173
178
  year: period.start.year,
174
179
  };
180
+ const periodId = (0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod);
175
181
  const transactionIdsBySelectedPeriod = draft.transactionCategorizationView[selectedTab]
176
- .transactionIdsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(monthYearPeriod)];
182
+ .transactionIdsBySelectedPeriod[periodId] ?? [];
183
+ // When starting a non-empty search, snapshot the current full dataset
184
+ // so clearing the search can restore instantly without an API round-trip.
185
+ // Only save the snapshot when there are rows to restore; skip for tabs
186
+ // that have never been loaded (empty list → no useful snapshot).
187
+ if (searchString !== undefined &&
188
+ searchString !== '' &&
189
+ transactionIdsBySelectedPeriod.length > 0) {
190
+ const snapshot = {
191
+ parentTotalCount: draft.parentTotalCountByTab[selectedTab][periodId],
192
+ periodId,
193
+ totalCount: draft.transactionCategorizationView[selectedTab].uiState
194
+ .totalCount[periodId],
195
+ transactionIds: [...transactionIdsBySelectedPeriod],
196
+ transactionReviewLocalDataById: {
197
+ ...draft.transactionCategorizationView[selectedTab]
198
+ .transactionReviewLocalDataById,
199
+ },
200
+ };
201
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
202
+ snapshot;
203
+ }
177
204
  transactionIdsBySelectedPeriod.forEach((transactionId) => {
178
205
  delete draft.transactionCategorizationView[selectedTab]
179
206
  .transactionReviewLocalDataById[transactionId];
@@ -312,10 +339,12 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
312
339
  }
313
340
  },
314
341
  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;
342
+ const { filters } = action.payload;
343
+ // Filters are tab-agnostic (payee/category/class/amount), so apply to
344
+ // all tabs so switching tabs after filtering shows consistent results.
345
+ transactionsViewState_1.TRANSACTIONS_TABS.forEach((tab) => {
346
+ draft.transactionCategorizationView[tab].filters = filters;
347
+ });
319
348
  },
320
349
  saveTransactionCategorization: {
321
350
  prepare(selectedTab, transactionIds, cotTrackingByTransactionId, isUncategorizedExpenseCategoryEnabled) {
@@ -667,20 +696,63 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
667
696
  resetOtherTabsFetchState(draft, action) {
668
697
  const { tabs, refreshViewInBackground } = action.payload;
669
698
  tabs.forEach((tab) => {
670
- if (draft.transactionCategorizationView[tab].fetchState ===
671
- 'In-Progress' ||
672
- refreshViewInBackground !== true) {
699
+ const tabFetchState = draft.transactionCategorizationView[tab].fetchState;
700
+ if (refreshViewInBackground !== true) {
701
+ // Foreground fetch completed: mark other tabs as Completed.
673
702
  draft.transactionCategorizationView[tab].fetchState = 'Completed';
674
703
  draft.transactionCategorizationView[tab].error = undefined;
675
704
  }
676
- else {
705
+ else if (tabFetchState !== 'In-Progress') {
706
+ // Background fetch completed: reset the other tab's refreshStatus,
707
+ // but only when it is not mid-load. During a dual-search the active
708
+ // tab has its own foreground fetch in flight — clobbering its
709
+ // fetchState here would drop the skeleton and show an empty list.
677
710
  draft.transactionCategorizationView[tab].refreshStatus = {
678
711
  fetchState: 'Not-Started',
679
712
  error: undefined,
680
713
  };
681
714
  }
715
+ // If tabFetchState === 'In-Progress' and refreshViewInBackground === true,
716
+ // leave the other tab's state untouched — it has its own active fetch.
682
717
  });
683
718
  },
719
+ restoreFullDatasetSnapshot(draft, action) {
720
+ const { selectedTab, period } = action.payload;
721
+ const periodId = (0, timePeriod_1.toMonthYearPeriodId)({
722
+ month: period.start.month,
723
+ year: period.start.year,
724
+ });
725
+ const snapshot = draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot;
726
+ if (snapshot == null || snapshot.periodId !== periodId) {
727
+ return;
728
+ }
729
+ draft.transactionCategorizationView[selectedTab]
730
+ .transactionIdsBySelectedPeriod[periodId] = snapshot.transactionIds;
731
+ // Merge back per-transaction local data; skip entries that were updated
732
+ // during the search (e.g. a save that happened while filtered results
733
+ // were showing) so those saves are not overwritten.
734
+ Object.entries(snapshot.transactionReviewLocalDataById).forEach(([transactionId, localData]) => {
735
+ if (draft.transactionCategorizationView[selectedTab]
736
+ .transactionReviewLocalDataById[transactionId] == null) {
737
+ draft.transactionCategorizationView[selectedTab]
738
+ .transactionReviewLocalDataById[transactionId] = localData;
739
+ }
740
+ });
741
+ if (snapshot.totalCount != null) {
742
+ draft.transactionCategorizationView[selectedTab].uiState.totalCount[periodId] = snapshot.totalCount;
743
+ }
744
+ if (snapshot.parentTotalCount != null) {
745
+ draft.parentTotalCountByTab[selectedTab][periodId] =
746
+ snapshot.parentTotalCount;
747
+ }
748
+ draft.transactionCategorizationView[selectedTab].uiState.searchString =
749
+ '';
750
+ draft.transactionCategorizationView[selectedTab].uiState.pageToken = null;
751
+ draft.transactionCategorizationView[selectedTab].fetchState = 'Completed';
752
+ draft.transactionCategorizationView[selectedTab].error = undefined;
753
+ draft.transactionCategorizationView[selectedTab].fullDatasetSnapshot =
754
+ undefined;
755
+ },
684
756
  fetchTransactionCategorizationFailure(draft, action) {
685
757
  const { selectedTab, selectedPeriod } = action.payload;
686
758
  draft.transactionCategorizationView[selectedTab].transactionIdsBySelectedPeriod[(0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod)] =
@@ -1061,5 +1133,5 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
1061
1133
  },
1062
1134
  },
1063
1135
  });
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;
1136
+ _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
1137
  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-betaRD1",
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",