@zeniai/client-epic-state 5.0.71-betaML41 → 5.0.71-betaVR2

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 (52) hide show
  1. package/lib/epic.d.ts +3 -2
  2. package/lib/epic.js +3 -2
  3. package/lib/esm/epic.js +3 -2
  4. package/lib/esm/index.js +8 -8
  5. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +21 -5
  6. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/refreshBatchDetailsForBatchIdEpic.js +52 -0
  7. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/syncTabsAfterAutomatchEpic.js +53 -0
  8. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +13 -9
  9. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -21
  10. package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +78 -3
  11. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +14 -117
  12. package/lib/esm/view/expenseAutomationView/selectors/missingReceiptsSelector.js +2 -0
  13. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +0 -58
  14. package/lib/esm/view/recommendation/recommendationHelper.js +0 -29
  15. package/lib/esm/view/recommendation/recommendationReducer.js +1 -26
  16. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +6 -121
  17. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +0 -22
  18. package/lib/index.d.ts +8 -9
  19. package/lib/index.js +25 -25
  20. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +21 -5
  21. package/lib/view/expenseAutomationView/epics/missingReceipts/refreshBatchDetailsForBatchIdEpic.d.ts +18 -0
  22. package/lib/view/expenseAutomationView/epics/missingReceipts/refreshBatchDetailsForBatchIdEpic.js +56 -0
  23. package/lib/view/expenseAutomationView/epics/missingReceipts/syncTabsAfterAutomatchEpic.d.ts +28 -0
  24. package/lib/view/expenseAutomationView/epics/missingReceipts/syncTabsAfterAutomatchEpic.js +57 -0
  25. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.d.ts +11 -5
  26. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +12 -8
  27. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +0 -1
  28. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +2 -23
  29. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +13 -1
  30. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +80 -4
  31. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +1 -14
  32. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +14 -117
  33. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +12 -0
  34. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +0 -2
  35. package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.js +2 -0
  36. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +0 -3
  37. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +0 -60
  38. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +19 -0
  39. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +0 -1
  40. package/lib/view/recommendation/recommendationHelper.d.ts +0 -9
  41. package/lib/view/recommendation/recommendationHelper.js +1 -31
  42. package/lib/view/recommendation/recommendationReducer.d.ts +1 -20
  43. package/lib/view/recommendation/recommendationReducer.js +2 -27
  44. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +1 -11
  45. package/lib/view/transactionDetail/transactionDetailReducer.js +7 -122
  46. package/lib/view/transactionDetail/transactionDetailSelector.d.ts +0 -2
  47. package/lib/view/transactionDetail/transactionDetailSelector.js +1 -24
  48. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +0 -1
  49. package/package.json +2 -2
  50. package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +0 -87
  51. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +0 -9
  52. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +0 -91
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasTransactionLocalDataChanges = exports.isAnyItemWithUncategorizedExpenseAccount = exports.getSnackbarMessageForTransactionReview = exports.mergeTabSpecificLineItems = exports.getLineItemsByTransactionIdsFromLocalData = exports.getLineItemsByTransactionsIdsFromResponse = exports.checkIfAllLineItemsAreCategoryClassFilled = exports.setEntityRecommendationForLineIdInLocalData = exports.toSetAllLineItemsToCategoryClass = exports.toTransactionDetailLocalData = exports.shouldAutoSelectAndAdd = exports.MAX_SELECTION_LIMIT = exports.filterAutoTabLineItems = exports.removeTransactionFromTabView = exports.getPendingReviewLineIdsFromTransaction = void 0;
3
+ exports.hasTransactionLocalDataChanges = exports.isAnyItemWithUncategorizedExpenseAccount = exports.getSnackbarMessageForTransactionReview = exports.mergeTabSpecificLineItems = exports.getLineItemsByTransactionIdsFromLocalData = exports.getLineItemsByTransactionsIdsFromResponse = exports.checkIfAllLineItemsAreCategoryClassFilled = exports.setEntityRecommendationForLineIdInLocalData = exports.toSetAllLineItemsToCategoryClass = exports.toTransactionDetailLocalData = exports.MAX_SELECTION_LIMIT = exports.filterAutoTabLineItems = exports.removeTransactionFromTabView = exports.getPendingReviewLineIdsFromTransaction = void 0;
4
4
  const vendorTransaction_1 = require("../../../entity/transaction/stateTypes/vendorTransaction");
5
5
  const transactionDetailLocalDataHelper_1 = require("../../transactionDetail/transactionDetailLocalDataHelper");
6
6
  const transactionDetailLocalDataHelper_2 = require("../../transactionDetail/transactionDetailLocalDataHelper");
@@ -65,23 +65,6 @@ const filterAutoTabLineItems = (tabView, transactionId, updatedTransaction) => {
65
65
  };
66
66
  exports.filterAutoTabLineItems = filterAutoTabLineItems;
67
67
  exports.MAX_SELECTION_LIMIT = 5;
68
- // Returns true when the transaction should be auto-added to the
69
- // selectedCheckBoxTransactionIds list after a categorization-related
70
- // state mutation: all line items are filled, the selection limit has
71
- // not been hit, and the id isn't already in the list. Consolidates the
72
- // triplet that was inlined at four sites (updateSelectedCustomer/Vendor,
73
- // setAllItemsToCategoryClassInLocalDataForCategorization, and
74
- // setEntityRecommendationForLineIdInLocalData below).
75
- const shouldAutoSelectAndAdd = (transaction, localData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
76
- if (selectedCheckBoxTransactionIds.includes(transaction.id)) {
77
- return false;
78
- }
79
- if (selectedCheckBoxTransactionIds.length >= exports.MAX_SELECTION_LIMIT) {
80
- return false;
81
- }
82
- return (0, exports.checkIfAllLineItemsAreCategoryClassFilled)(transaction, localData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
83
- };
84
- exports.shouldAutoSelectAndAdd = shouldAutoSelectAndAdd;
85
68
  const toTransactionDetailLocalData = (transaction, selectedTab, lineItemsByTransactionIds, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled) => {
86
69
  const transactionLocal = {
87
70
  ...transactionsViewState_1.initialSupportedTransactionCategorization.transactionReviewLocalData,
@@ -569,14 +552,10 @@ const setEntityRecommendationForLineIdInLocalData = (draft, transaction, transac
569
552
  selectedEntity: transactionDetailLocalData.selectedEntity,
570
553
  tabSpecificLineItems: transactionDetailLocalData.tabSpecificLineItems,
571
554
  };
572
- // Auto-uncheck if not all line items are filled (the row's
573
- // category/class became stale after the entity update). Otherwise
574
- // route through shouldAutoSelectAndAdd so the MAX_SELECTION_LIMIT
575
- // gate is consistently applied across all auto-add sites.
576
555
  if (!(0, exports.checkIfAllLineItemsAreCategoryClassFilled)(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
577
556
  selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds.filter((id) => id !== transaction.id);
578
557
  }
579
- else if ((0, exports.shouldAutoSelectAndAdd)(transaction, newLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
558
+ else if (selectedCheckBoxTransactionIds.includes(transaction.id) === false) {
580
559
  selectedCheckBoxTransactionIds.push(transaction.id);
581
560
  }
582
561
  draft.selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds;
@@ -19,6 +19,15 @@ export declare const fetchBulkUploadBatchDetails: import("@reduxjs/toolkit").Act
19
19
  * after a hard refresh. The epic owns the list + details API calls end-to-end.
20
20
  */
21
21
  export declare const restoreBulkUploadAutomatchingOnMount: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"expenseAutomationMissingReceiptsView/restoreBulkUploadAutomatchingOnMount">;
22
+ /**
23
+ * Epic trigger only. Targeted stale-while-revalidate refresh for a single batch's details — used
24
+ * by `pusherBatchStatusCompletionEpic` so a Pusher 'completed' event re-fetches only that one
25
+ * batch instead of wiping the entire `batchDetailsById` cache. The epic dedupes via
26
+ * `lastBatchDetailRefreshAtById` so resend storms don't trigger waves of API calls.
27
+ */
28
+ export declare const refreshBatchDetailsForBatchId: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
29
+ batchId: ID;
30
+ }, string>;
22
31
  export declare const fetchMissingReceipts: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[period: TimePeriod, minimumTransactionAmount: Amount, cacheOverride?: any, refreshViewInBackground?: any], {
23
32
  period: TimePeriod;
24
33
  minimumTransactionAmount: Amount;
@@ -73,7 +82,10 @@ export declare const fetchMissingReceipts: import("@reduxjs/toolkit").ActionCrea
73
82
  }, "expenseAutomationMissingReceiptsView/fetchBulkUploadBatchesSuccess">, fetchBulkUploadBatchesFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
74
83
  error: ZeniAPIStatus;
75
84
  selectedPeriod: MonthYearPeriod;
76
- }, "expenseAutomationMissingReceiptsView/fetchBulkUploadBatchesFailure">, confirmBulkUploadMatch: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[batchId: string, attachmentId: string, transactionId: string, transactionType: string, fileName: string], {
85
+ }, "expenseAutomationMissingReceiptsView/fetchBulkUploadBatchesFailure">, clearBulkUploadBatchDetailsForScopeChange: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"expenseAutomationMissingReceiptsView/clearBulkUploadBatchDetailsForScopeChange">, markBatchDetailRefreshAttempted: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
86
+ batchId: ID;
87
+ timestamp: number;
88
+ }, "expenseAutomationMissingReceiptsView/markBatchDetailRefreshAttempted">, confirmBulkUploadMatch: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[batchId: string, attachmentId: string, transactionId: string, transactionType: string, fileName: string], {
77
89
  batchId: string;
78
90
  attachmentId: string;
79
91
  transactionId: string;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.fetchCompletedTransactionsFailure = exports.fetchCompletedTransactionsSuccess = exports.fetchCompletedTransactions = exports.acknowledgeBulkUploadConfirmMatchComplete = exports.clearManualSearchResults = exports.searchTransactionsForManualMatchFailure = exports.searchTransactionsForManualMatchSuccess = exports.searchTransactionsForManualMatch = exports.clearBulkUpload = exports.setBulkUploadSortConfig = exports.setBulkUploadCompletedSubTab = exports.setBulkUploadResultsTab = exports.confirmBulkUploadMatchFailure = exports.confirmBulkUploadMatchSuccess = exports.confirmBulkUploadMatch = exports.fetchBulkUploadBatchesFailure = exports.fetchBulkUploadBatchesSuccess = exports.fetchBulkUploadBatches = exports.fetchMoreBatchDetailsFailure = exports.fetchMoreBatchDetailsComplete = exports.fetchMoreBatchDetails = exports.setInitialBatchDetailsLoading = exports.batchDetailFetchFailed = exports.storeBatchDetails = exports.fetchBulkUploadBatchDetailsFailure = exports.fetchBulkUploadBatchDetailsSuccess = exports.clearMissingReceiptsTabNavigation = exports.requestMissingReceiptsTabNavigation = exports.pusherBatchStatusUpdate = exports.bulkUploadAutomatchingTimedOut = exports.restoreBulkUploadMatchingState = exports.bulkUploadReceiptsFailure = exports.bulkUploadReceiptsSuccess = exports.setBulkUploadUploadedFileCount = exports.updateBulkUploadProgress = exports.bulkUploadReceipts = exports.clearExpenseAutomationMissingReceiptsView = exports.markMissingReceiptAsDone = exports.updateMissingReceiptsUIState = exports.updateMissingReceiptUploadState = exports.uploadMissingReceiptSuccess = exports.fetchMissingReceiptsFailure = exports.fetchMissingReceiptsSuccess = exports.fetchMissingReceipts = exports.restoreBulkUploadAutomatchingOnMount = exports.fetchBulkUploadBatchDetails = exports.initialState = exports.initialBulkUploadState = exports.getCompletedTransactionsCacheKey = void 0;
4
+ exports.fetchCompletedTransactions = exports.acknowledgeBulkUploadConfirmMatchComplete = exports.clearManualSearchResults = exports.searchTransactionsForManualMatchFailure = exports.searchTransactionsForManualMatchSuccess = exports.searchTransactionsForManualMatch = exports.clearBulkUpload = exports.setBulkUploadSortConfig = exports.setBulkUploadCompletedSubTab = exports.setBulkUploadResultsTab = exports.confirmBulkUploadMatchFailure = exports.confirmBulkUploadMatchSuccess = exports.confirmBulkUploadMatch = exports.markBatchDetailRefreshAttempted = exports.clearBulkUploadBatchDetailsForScopeChange = exports.fetchBulkUploadBatchesFailure = exports.fetchBulkUploadBatchesSuccess = exports.fetchBulkUploadBatches = exports.fetchMoreBatchDetailsFailure = exports.fetchMoreBatchDetailsComplete = exports.fetchMoreBatchDetails = exports.setInitialBatchDetailsLoading = exports.batchDetailFetchFailed = exports.storeBatchDetails = exports.fetchBulkUploadBatchDetailsFailure = exports.fetchBulkUploadBatchDetailsSuccess = exports.clearMissingReceiptsTabNavigation = exports.requestMissingReceiptsTabNavigation = exports.pusherBatchStatusUpdate = exports.bulkUploadAutomatchingTimedOut = exports.restoreBulkUploadMatchingState = exports.bulkUploadReceiptsFailure = exports.bulkUploadReceiptsSuccess = exports.setBulkUploadUploadedFileCount = exports.updateBulkUploadProgress = exports.bulkUploadReceipts = exports.clearExpenseAutomationMissingReceiptsView = exports.markMissingReceiptAsDone = exports.updateMissingReceiptsUIState = exports.updateMissingReceiptUploadState = exports.uploadMissingReceiptSuccess = exports.fetchMissingReceiptsFailure = exports.fetchMissingReceiptsSuccess = exports.fetchMissingReceipts = exports.refreshBatchDetailsForBatchId = exports.restoreBulkUploadAutomatchingOnMount = exports.fetchBulkUploadBatchDetails = exports.initialState = exports.initialBulkUploadState = exports.getCompletedTransactionsCacheKey = void 0;
5
+ exports.fetchCompletedTransactionsFailure = exports.fetchCompletedTransactionsSuccess = void 0;
5
6
  const toolkit_1 = require("@reduxjs/toolkit");
6
7
  const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
7
8
  const completedSubTab_1 = require("../types/completedSubTab");
@@ -15,6 +16,7 @@ exports.getCompletedTransactionsCacheKey = getCompletedTransactionsCacheKey;
15
16
  exports.initialBulkUploadState = {
16
17
  batchDetailsById: {},
17
18
  batchDetailsPaginationState: { fetchState: 'Not-Started', error: undefined },
19
+ batchDetailsRevalidating: false,
18
20
  batchListByPeriod: {},
19
21
  batchListFetchState: { fetchState: 'Not-Started', error: undefined },
20
22
  batchStatusById: {},
@@ -24,6 +26,8 @@ exports.initialBulkUploadState = {
24
26
  currentBatchId: undefined,
25
27
  currentResultsTab: 'unmatched',
26
28
  failedBatchDetailIds: [],
29
+ initialBatchDetailsLoaded: false,
30
+ lastBatchDetailRefreshAtById: {},
27
31
  manualSearchUiResetKey: 0,
28
32
  manualSearch: {
29
33
  fetchState: { fetchState: 'Not-Started', error: undefined },
@@ -70,6 +74,13 @@ exports.fetchBulkUploadBatchDetails = (0, toolkit_1.createAction)('expenseAutoma
70
74
  * after a hard refresh. The epic owns the list + details API calls end-to-end.
71
75
  */
72
76
  exports.restoreBulkUploadAutomatchingOnMount = (0, toolkit_1.createAction)('expenseAutomationMissingReceiptsView/restoreBulkUploadAutomatchingOnMount');
77
+ /**
78
+ * Epic trigger only. Targeted stale-while-revalidate refresh for a single batch's details — used
79
+ * by `pusherBatchStatusCompletionEpic` so a Pusher 'completed' event re-fetches only that one
80
+ * batch instead of wiping the entire `batchDetailsById` cache. The epic dedupes via
81
+ * `lastBatchDetailRefreshAtById` so resend storms don't trigger waves of API calls.
82
+ */
83
+ exports.refreshBatchDetailsForBatchId = (0, toolkit_1.createAction)('expenseAutomationMissingReceiptsView/refreshBatchDetailsForBatchId');
73
84
  const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
74
85
  name: 'expenseAutomationMissingReceiptsView',
75
86
  initialState: exports.initialState,
@@ -256,6 +267,16 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
256
267
  const details = action.payload;
257
268
  draft.bulkUpload.batchDetailsById[details.batchId] = details;
258
269
  draft.bulkUpload.phase = 'completed';
270
+ draft.bulkUpload.initialBatchDetailsLoaded = true;
271
+ /**
272
+ * A successful detail load supersedes any previous failure for this batch — without this,
273
+ * `getPrimaryBatchIdFromBatchListOrder` (and pagination) would keep skipping the batch
274
+ * across the recovery, hiding now-valid Unmatched-tab work.
275
+ */
276
+ const failedIndex = draft.bulkUpload.failedBatchDetailIds.indexOf(details.batchId);
277
+ if (failedIndex >= 0) {
278
+ draft.bulkUpload.failedBatchDetailIds.splice(failedIndex, 1);
279
+ }
259
280
  },
260
281
  fetchBulkUploadBatchDetailsFailure: {
261
282
  reducer(draft) {
@@ -277,6 +298,19 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
277
298
  draft.bulkUpload.currentBatchId === details.batchId) {
278
299
  draft.bulkUpload.phase = 'completed';
279
300
  }
301
+ /** Stale-while-revalidate: any successful detail load means we have rendered data. */
302
+ draft.bulkUpload.initialBatchDetailsLoaded = true;
303
+ /**
304
+ * Stale-while-revalidate recovery: if this batch had previously failed, a successful
305
+ * targeted refresh (Pusher `completed` → `refreshBatchDetailsForBatchIdEpic`) or
306
+ * multi-batch revalidation (manual refresh) supersedes the failure. Selectors that gate
307
+ * on `failedBatchDetailIds` (primary-batch resolution, `hasMoreBatchDetails`) must see the
308
+ * batch as recovered so it can show fresh Unmatched work.
309
+ */
310
+ const failedIndex = draft.bulkUpload.failedBatchDetailIds.indexOf(details.batchId);
311
+ if (failedIndex >= 0) {
312
+ draft.bulkUpload.failedBatchDetailIds.splice(failedIndex, 1);
313
+ }
280
314
  },
281
315
  batchDetailFetchFailed(draft, action) {
282
316
  const { batchId } = action.payload;
@@ -305,6 +339,14 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
305
339
  fetchState: 'Completed',
306
340
  error: undefined,
307
341
  };
342
+ /**
343
+ * Stale-while-revalidate: a completed pagination round means the initial page (or a
344
+ * subsequent revalidation) has fully resolved. Once `initialBatchDetailsLoaded` flips to
345
+ * `true` it stays `true` for the session — only `clearBulkUpload` /
346
+ * `clearBulkUploadBatchDetailsForScopeChange` can reset it.
347
+ */
348
+ draft.bulkUpload.initialBatchDetailsLoaded = true;
349
+ draft.bulkUpload.batchDetailsRevalidating = false;
308
350
  const currentId = draft.bulkUpload.currentBatchId;
309
351
  const currentDetails = currentId != null
310
352
  ? draft.bulkUpload.batchDetailsById[currentId]
@@ -320,12 +362,21 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
320
362
  fetchState: 'Error',
321
363
  error: action.payload.error,
322
364
  };
365
+ draft.bulkUpload.batchDetailsRevalidating = false;
323
366
  },
324
367
  fetchBulkUploadBatches: {
325
368
  reducer(draft, action) {
369
+ /**
370
+ * Stale-while-revalidate: when caller asks for a full details refresh (manual refresh
371
+ * button), we keep `batchDetailsById` and `failedBatchDetailIds` intact so the existing
372
+ * UI stays rendered, and flip `batchDetailsRevalidating: true`. The post-success epic
373
+ * (`fetchMultipleBatchDetailsEpic`) sees this flag and re-fetches every completed list
374
+ * row, replacing entries via `storeBatchDetails` as each response arrives. This avoids
375
+ * the previous "wipe to empty → render skeleton" flash. To genuinely drop details (e.g.
376
+ * tenant/period scope change), use `clearBulkUploadBatchDetailsForScopeChange` instead.
377
+ */
326
378
  if (action.payload.invalidateBatchDetailsCache === true) {
327
- draft.bulkUpload.batchDetailsById = {};
328
- draft.bulkUpload.failedBatchDetailIds = [];
379
+ draft.bulkUpload.batchDetailsRevalidating = true;
329
380
  }
330
381
  draft.bulkUpload.batchListFetchState = {
331
382
  fetchState: 'In-Progress',
@@ -342,6 +393,31 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
342
393
  };
343
394
  },
344
395
  },
396
+ /**
397
+ * Tenant/period scope change — the new scope's data is genuinely unknown so the existing
398
+ * details cache and `initialBatchDetailsLoaded` flag must be reset (the Unmatched skeleton
399
+ * is acceptable on scope change). Distinct from a stale-while-revalidate refresh.
400
+ */
401
+ clearBulkUploadBatchDetailsForScopeChange(draft) {
402
+ draft.bulkUpload.batchDetailsById = {};
403
+ draft.bulkUpload.failedBatchDetailIds = [];
404
+ draft.bulkUpload.batchDetailsPaginationState = {
405
+ fetchState: 'Not-Started',
406
+ error: undefined,
407
+ };
408
+ draft.bulkUpload.batchDetailsRevalidating = false;
409
+ draft.bulkUpload.initialBatchDetailsLoaded = false;
410
+ draft.bulkUpload.lastBatchDetailRefreshAtById = {};
411
+ },
412
+ /**
413
+ * Records the last time we issued a targeted refresh for `batchId`. Set by
414
+ * `refreshBatchDetailsForBatchIdEpic` before each API call so subsequent Pusher resends in the
415
+ * dedupe window are skipped.
416
+ */
417
+ markBatchDetailRefreshAttempted(draft, action) {
418
+ draft.bulkUpload.lastBatchDetailRefreshAtById[action.payload.batchId] =
419
+ action.payload.timestamp;
420
+ },
345
421
  fetchBulkUploadBatchesSuccess(draft, action) {
346
422
  const periodId = (0, timePeriod_1.toMonthYearPeriodId)(action.payload.selectedPeriod);
347
423
  draft.bulkUpload.batchListByPeriod[periodId] = action.payload.batchList;
@@ -584,7 +660,7 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
584
660
  });
585
661
  _a = expenseAutomationMissingReceiptsView.actions, exports.fetchMissingReceipts = _a.fetchMissingReceipts, exports.fetchMissingReceiptsSuccess = _a.fetchMissingReceiptsSuccess, exports.fetchMissingReceiptsFailure = _a.fetchMissingReceiptsFailure, exports.uploadMissingReceiptSuccess = _a.uploadMissingReceiptSuccess, exports.updateMissingReceiptUploadState = _a.updateMissingReceiptUploadState, exports.updateMissingReceiptsUIState = _a.updateMissingReceiptsUIState, exports.markMissingReceiptAsDone = _a.markMissingReceiptAsDone, exports.clearExpenseAutomationMissingReceiptsView = _a.clearExpenseAutomationMissingReceiptsView,
586
662
  // Bulk Upload
587
- exports.bulkUploadReceipts = _a.bulkUploadReceipts, exports.updateBulkUploadProgress = _a.updateBulkUploadProgress, exports.setBulkUploadUploadedFileCount = _a.setBulkUploadUploadedFileCount, exports.bulkUploadReceiptsSuccess = _a.bulkUploadReceiptsSuccess, exports.bulkUploadReceiptsFailure = _a.bulkUploadReceiptsFailure, exports.restoreBulkUploadMatchingState = _a.restoreBulkUploadMatchingState, exports.bulkUploadAutomatchingTimedOut = _a.bulkUploadAutomatchingTimedOut, exports.pusherBatchStatusUpdate = _a.pusherBatchStatusUpdate, exports.requestMissingReceiptsTabNavigation = _a.requestMissingReceiptsTabNavigation, exports.clearMissingReceiptsTabNavigation = _a.clearMissingReceiptsTabNavigation, exports.fetchBulkUploadBatchDetailsSuccess = _a.fetchBulkUploadBatchDetailsSuccess, exports.fetchBulkUploadBatchDetailsFailure = _a.fetchBulkUploadBatchDetailsFailure, exports.storeBatchDetails = _a.storeBatchDetails, exports.batchDetailFetchFailed = _a.batchDetailFetchFailed, exports.setInitialBatchDetailsLoading = _a.setInitialBatchDetailsLoading, exports.fetchMoreBatchDetails = _a.fetchMoreBatchDetails, exports.fetchMoreBatchDetailsComplete = _a.fetchMoreBatchDetailsComplete, exports.fetchMoreBatchDetailsFailure = _a.fetchMoreBatchDetailsFailure, exports.fetchBulkUploadBatches = _a.fetchBulkUploadBatches, exports.fetchBulkUploadBatchesSuccess = _a.fetchBulkUploadBatchesSuccess, exports.fetchBulkUploadBatchesFailure = _a.fetchBulkUploadBatchesFailure, exports.confirmBulkUploadMatch = _a.confirmBulkUploadMatch, exports.confirmBulkUploadMatchSuccess = _a.confirmBulkUploadMatchSuccess, exports.confirmBulkUploadMatchFailure = _a.confirmBulkUploadMatchFailure, exports.setBulkUploadResultsTab = _a.setBulkUploadResultsTab, exports.setBulkUploadCompletedSubTab = _a.setBulkUploadCompletedSubTab, exports.setBulkUploadSortConfig = _a.setBulkUploadSortConfig, exports.clearBulkUpload = _a.clearBulkUpload, exports.searchTransactionsForManualMatch = _a.searchTransactionsForManualMatch, exports.searchTransactionsForManualMatchSuccess = _a.searchTransactionsForManualMatchSuccess, exports.searchTransactionsForManualMatchFailure = _a.searchTransactionsForManualMatchFailure, exports.clearManualSearchResults = _a.clearManualSearchResults, exports.acknowledgeBulkUploadConfirmMatchComplete = _a.acknowledgeBulkUploadConfirmMatchComplete,
663
+ exports.bulkUploadReceipts = _a.bulkUploadReceipts, exports.updateBulkUploadProgress = _a.updateBulkUploadProgress, exports.setBulkUploadUploadedFileCount = _a.setBulkUploadUploadedFileCount, exports.bulkUploadReceiptsSuccess = _a.bulkUploadReceiptsSuccess, exports.bulkUploadReceiptsFailure = _a.bulkUploadReceiptsFailure, exports.restoreBulkUploadMatchingState = _a.restoreBulkUploadMatchingState, exports.bulkUploadAutomatchingTimedOut = _a.bulkUploadAutomatchingTimedOut, exports.pusherBatchStatusUpdate = _a.pusherBatchStatusUpdate, exports.requestMissingReceiptsTabNavigation = _a.requestMissingReceiptsTabNavigation, exports.clearMissingReceiptsTabNavigation = _a.clearMissingReceiptsTabNavigation, exports.fetchBulkUploadBatchDetailsSuccess = _a.fetchBulkUploadBatchDetailsSuccess, exports.fetchBulkUploadBatchDetailsFailure = _a.fetchBulkUploadBatchDetailsFailure, exports.storeBatchDetails = _a.storeBatchDetails, exports.batchDetailFetchFailed = _a.batchDetailFetchFailed, exports.setInitialBatchDetailsLoading = _a.setInitialBatchDetailsLoading, exports.fetchMoreBatchDetails = _a.fetchMoreBatchDetails, exports.fetchMoreBatchDetailsComplete = _a.fetchMoreBatchDetailsComplete, exports.fetchMoreBatchDetailsFailure = _a.fetchMoreBatchDetailsFailure, exports.fetchBulkUploadBatches = _a.fetchBulkUploadBatches, exports.fetchBulkUploadBatchesSuccess = _a.fetchBulkUploadBatchesSuccess, exports.fetchBulkUploadBatchesFailure = _a.fetchBulkUploadBatchesFailure, exports.clearBulkUploadBatchDetailsForScopeChange = _a.clearBulkUploadBatchDetailsForScopeChange, exports.markBatchDetailRefreshAttempted = _a.markBatchDetailRefreshAttempted, exports.confirmBulkUploadMatch = _a.confirmBulkUploadMatch, exports.confirmBulkUploadMatchSuccess = _a.confirmBulkUploadMatchSuccess, exports.confirmBulkUploadMatchFailure = _a.confirmBulkUploadMatchFailure, exports.setBulkUploadResultsTab = _a.setBulkUploadResultsTab, exports.setBulkUploadCompletedSubTab = _a.setBulkUploadCompletedSubTab, exports.setBulkUploadSortConfig = _a.setBulkUploadSortConfig, exports.clearBulkUpload = _a.clearBulkUpload, exports.searchTransactionsForManualMatch = _a.searchTransactionsForManualMatch, exports.searchTransactionsForManualMatchSuccess = _a.searchTransactionsForManualMatchSuccess, exports.searchTransactionsForManualMatchFailure = _a.searchTransactionsForManualMatchFailure, exports.clearManualSearchResults = _a.clearManualSearchResults, exports.acknowledgeBulkUploadConfirmMatchComplete = _a.acknowledgeBulkUploadConfirmMatchComplete,
588
664
  // Completed Transactions
589
665
  exports.fetchCompletedTransactions = _a.fetchCompletedTransactions, exports.fetchCompletedTransactionsSuccess = _a.fetchCompletedTransactionsSuccess, exports.fetchCompletedTransactionsFailure = _a.fetchCompletedTransactionsFailure;
590
666
  exports.default = expenseAutomationMissingReceiptsView.reducer;
@@ -157,20 +157,7 @@ export declare const fetchTransactionCategorization: import("@reduxjs/toolkit").
157
157
  uncategorizedAccounts: UncategorizedAccounts;
158
158
  isAccountingClassesEnabled: boolean | undefined;
159
159
  isUncategorizedExpenseCategoryEnabled: boolean | undefined;
160
- }, "expenseAutomationTransactionsView/setEntityRecommendationForLineIdsForCategorization", never, never>, markCategoryClassRecommendationsInProgressForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[]], {
161
- selectedTab: "review" | "autoCategorized";
162
- transactionId: TransactionID;
163
- lineIds: string[];
164
- }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsInProgressForCategorization", never, never>, markCategoryClassRecommendationsCompletedForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[]], {
165
- selectedTab: "review" | "autoCategorized";
166
- transactionId: TransactionID;
167
- lineIds: string[];
168
- }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsCompletedForCategorization", never, never>, markCategoryClassRecommendationsFailureForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[], error: ZeniAPIStatus<Record<string, unknown>>], {
169
- selectedTab: "review" | "autoCategorized";
170
- transactionId: TransactionID;
171
- lineIds: string[];
172
- error: ZeniAPIStatus<Record<string, unknown>>;
173
- }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsFailureForCategorization", never, never>, updateSelectedTransactionId: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
160
+ }, "expenseAutomationTransactionsView/setEntityRecommendationForLineIdsForCategorization", never, never>, updateSelectedTransactionId: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
174
161
  lineId: ID;
175
162
  selectedTab: TransactionsTab;
176
163
  transactionId: ID;
@@ -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.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.updateTransactionCategorizationUIState = exports.fetchTransactionCategorization = exports.initialState = exports.initialTransactionTabViewState = void 0;
7
+ exports.uploadTransactionCategorizationReceiptSuccess = exports.updateTransactionCategorizationUploadReceiptState = exports.backgroundRefetchReviewTab = exports.syncTransactionCategorizationFromDetailSave = exports.updateSelectedTransactionId = 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.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"));
@@ -427,9 +427,12 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
427
427
  const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
428
428
  .selectedCheckBoxTransactionIds;
429
429
  if (selectedTransaction != null &&
430
- (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
430
+ (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
431
+ selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
432
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
431
433
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
432
- ...selectedCheckBoxTransactionIds,
434
+ ...draft.transactionCategorizationView[selectedTab]
435
+ .selectedCheckBoxTransactionIds,
433
436
  transactionId,
434
437
  ];
435
438
  }
@@ -512,26 +515,9 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
512
515
  const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
513
516
  .transactionReviewLocalDataById, transactionId.id, undefined);
514
517
  if (transactionLocalData != null) {
515
- const newLocalData = (0, transactionCategorizationLocalDataHelper_1.setEntityRecommendationForLineIdInLocalData)(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
516
- // Release the per-line fetch flag for every line this fetch
517
- // covered. Done in-place on the freshly returned local data
518
- // (the helper produces a new mutable object), before we write
519
- // it back to the draft.
520
- lineIds.forEach((lineId) => {
521
- const lineItem = newLocalData.lineItemById[lineId];
522
- if (lineItem != null) {
523
- newLocalData.lineItemById[lineId] = {
524
- ...lineItem,
525
- categoryClassRecommendationsFetchState: {
526
- fetchState: 'Completed',
527
- error: undefined,
528
- },
529
- };
530
- }
531
- });
532
518
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
533
519
  transactionId: transaction.id,
534
- transactionReviewLocalData: newLocalData,
520
+ transactionReviewLocalData: (0, transactionCategorizationLocalDataHelper_1.setEntityRecommendationForLineIdInLocalData)(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
535
521
  };
536
522
  // Mark transaction as dirty (autofill happened)
537
523
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
@@ -544,99 +530,6 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
544
530
  }
545
531
  },
546
532
  },
547
- // Start arm of the line-update recommendations fetch. Flips covered
548
- // lines to In-Progress AND removes the transaction from
549
- // selectedCheckBoxTransactionIds — the row is unchecked + disabled
550
- // until either the success arm (recheck if limit allows) or the
551
- // failure arm releases the per-line flag.
552
- markCategoryClassRecommendationsInProgressForCategorization: {
553
- reducer(draft, action) {
554
- const { selectedTab, transactionId, lineIds } = action.payload;
555
- const tab = draft.transactionCategorizationView[selectedTab];
556
- const transactionLocalData = (0, get_1.default)(tab.transactionReviewLocalDataById, transactionId.id, undefined);
557
- if (transactionLocalData != null) {
558
- lineIds.forEach((lineId) => {
559
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
560
- if (lineItem != null) {
561
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
562
- ...lineItem,
563
- categoryClassRecommendationsFetchState: {
564
- fetchState: 'In-Progress',
565
- error: undefined,
566
- },
567
- };
568
- }
569
- });
570
- }
571
- // Uncheck on fetch start: the row's existing category/class are
572
- // stale after a payee change, so we drop the auto-selection until
573
- // fresh recommendations land.
574
- if (tab.selectedCheckBoxTransactionIds.includes(transactionId.id)) {
575
- tab.selectedCheckBoxTransactionIds =
576
- tab.selectedCheckBoxTransactionIds.filter((id) => id !== transactionId.id);
577
- }
578
- },
579
- prepare(selectedTab, transactionId, lineIds) {
580
- return { payload: { selectedTab, transactionId, lineIds } };
581
- },
582
- },
583
- // Completed arm. Used by the epic when the HTTP call succeeds but the
584
- // server returned no per-line recommendations — we still need to
585
- // release the In-Progress flag so the skeleton resolves. Successful
586
- // responses with recommendations go through
587
- // setEntityRecommendationForLineIdsForCategorization instead (which
588
- // also flips the lines to Completed).
589
- markCategoryClassRecommendationsCompletedForCategorization: {
590
- reducer(draft, action) {
591
- const { selectedTab, transactionId, lineIds } = action.payload;
592
- const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
593
- .transactionReviewLocalDataById, transactionId.id, undefined);
594
- if (transactionLocalData != null) {
595
- lineIds.forEach((lineId) => {
596
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
597
- if (lineItem != null) {
598
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
599
- ...lineItem,
600
- categoryClassRecommendationsFetchState: {
601
- fetchState: 'Completed',
602
- error: undefined,
603
- },
604
- };
605
- }
606
- });
607
- }
608
- },
609
- prepare(selectedTab, transactionId, lineIds) {
610
- return { payload: { selectedTab, transactionId, lineIds } };
611
- },
612
- },
613
- // Failure arm. Flips covered lines to Error so the skeleton resolves
614
- // and downstream UI can react to the failure. The transaction stays
615
- // unchecked from the start-arm action.
616
- markCategoryClassRecommendationsFailureForCategorization: {
617
- reducer(draft, action) {
618
- const { selectedTab, transactionId, lineIds, error } = action.payload;
619
- const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
620
- .transactionReviewLocalDataById, transactionId.id, undefined);
621
- if (transactionLocalData != null) {
622
- lineIds.forEach((lineId) => {
623
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
624
- if (lineItem != null) {
625
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
626
- ...lineItem,
627
- categoryClassRecommendationsFetchState: {
628
- fetchState: 'Error',
629
- error,
630
- },
631
- };
632
- }
633
- });
634
- }
635
- },
636
- prepare(selectedTab, transactionId, lineIds, error) {
637
- return { payload: { selectedTab, transactionId, lineIds, error } };
638
- },
639
- },
640
533
  fetchTransactionCategorizationSuccess(draft, action) {
641
534
  const { selectedTab, refreshViewInBackground } = action.payload;
642
535
  if (draft.transactionCategorizationView[selectedTab].fetchState ===
@@ -732,7 +625,9 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
732
625
  .selectedCheckBoxTransactionIds;
733
626
  if (newTransactionLocalData.customer != null &&
734
627
  newTransactionLocalData.customer.id != null &&
735
- (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
628
+ (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
629
+ selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
630
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
736
631
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
737
632
  ...selectedCheckBoxTransactionIds,
738
633
  transactionId,
@@ -806,7 +701,9 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
806
701
  .selectedCheckBoxTransactionIds;
807
702
  if (newTransactionLocalData.vendor != null &&
808
703
  newTransactionLocalData.vendor.id != null &&
809
- (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
704
+ (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
705
+ selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
706
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
810
707
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
811
708
  ...selectedCheckBoxTransactionIds,
812
709
  transactionId,
@@ -1039,5 +936,5 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
1039
936
  },
1040
937
  },
1041
938
  });
1042
- _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, 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.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
939
+ _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, 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.updateSelectedTransactionId = _a.updateSelectedTransactionId, exports.syncTransactionCategorizationFromDetailSave = _a.syncTransactionCategorizationFromDetailSave, exports.backgroundRefetchReviewTab = _a.backgroundRefetchReviewTab, exports.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
1043
940
  exports.default = expenseAutomationTransactionsView.reducer;
@@ -29,6 +29,12 @@ export interface ResolvedBatchFile {
29
29
  export interface BulkUploadSelectorData {
30
30
  batchDetails: ResolvedBatchDetails | undefined;
31
31
  batchDetailsPaginationState: FetchStateAndError;
32
+ /**
33
+ * True while a stale-while-revalidate refresh of `batchDetailsById` is in flight (e.g. manual
34
+ * refresh button). UI should keep rendering existing data and show only a lightweight refresh
35
+ * indicator — never the full skeleton.
36
+ */
37
+ batchDetailsRevalidating: boolean;
32
38
  batchList: BatchListItem[];
33
39
  batchListFetchState: FetchStateAndError;
34
40
  batchStatus: BatchStatus | undefined;
@@ -37,6 +43,12 @@ export interface BulkUploadSelectorData {
37
43
  confirmMatchStatus: FetchStateAndError;
38
44
  currentResultsTab: BulkUploadResultsTab;
39
45
  hasMoreBatchDetails: boolean;
46
+ /**
47
+ * `true` once the first batch-details page has loaded for the current scope (period/tenant).
48
+ * Stays `true` across refreshes — flips back to `false` only on `clearBulkUpload` or on an
49
+ * explicit scope change. Used by the Unmatched tab to gate the initial-load skeleton.
50
+ */
51
+ initialBatchDetailsLoaded: boolean;
40
52
  manualSearch: ManualSearchState;
41
53
  /** Bumps when manual search state is cleared — remount manual search inputs. */
42
54
  manualSearchUiResetKey: number;
@@ -20,8 +20,6 @@ export interface ExpenseAutomationTransactionViewSelector extends SelectorView {
20
20
  classList: ClassBase[];
21
21
  completionStatus: CompletionStatusType;
22
22
  fetchStateByTransactionTabs: Record<TransactionsTab, FetchStateAndError>;
23
- hasInFlightCategoryClassRecommendations: boolean;
24
- inFlightCategoryClassRecommendationsByTransactionId: Record<ID, boolean>;
25
23
  isAccountingProjectsEnabled: boolean;
26
24
  markAsNotMiscategorizedStatus: FetchStateAndError;
27
25
  /**
@@ -230,6 +230,8 @@ function getExpenseAutomationMissingReceiptsView(state) {
230
230
  manualSearchUiResetKey: bulkUpload.manualSearchUiResetKey,
231
231
  hasMoreBatchDetails,
232
232
  batchDetailsPaginationState: bulkUpload.batchDetailsPaginationState,
233
+ batchDetailsRevalidating: bulkUpload.batchDetailsRevalidating,
234
+ initialBatchDetailsLoaded: bulkUpload.initialBatchDetailsLoaded,
233
235
  manualSearch: {
234
236
  ...bulkUpload.manualSearch,
235
237
  results: manualSearchResults,
@@ -1,6 +1,3 @@
1
- import { ID } from '../../../commonStateTypes/common';
2
1
  import { RootState } from '../../../reducer';
3
2
  import { ExpenseAutomationTransactionViewSelector } from '../selectorTypes/transactionsViewSelectorTypes';
4
- export declare const getCategorizationInFlightRecommendationsByTransactionId: (state: RootState) => Record<ID, boolean>;
5
- export declare const getCategorizationHasInFlightRecommendations: (state: RootState) => boolean;
6
3
  export declare function getExpenseAutomationTransactionView(state: RootState): ExpenseAutomationTransactionViewSelector;
@@ -3,9 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getCategorizationHasInFlightRecommendations = exports.getCategorizationInFlightRecommendationsByTransactionId = void 0;
7
6
  exports.getExpenseAutomationTransactionView = getExpenseAutomationTransactionView;
8
- const toolkit_1 = require("@reduxjs/toolkit");
9
7
  const omit_1 = __importDefault(require("lodash/omit"));
10
8
  const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
11
9
  const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
@@ -16,60 +14,6 @@ const accountListSelector_1 = require("../../accountList/accountListSelector");
16
14
  const classListSelector_1 = require("../../classList/classListSelector");
17
15
  const projectListSelector_1 = require("../../projectList/projectListSelector");
18
16
  const expenseAutomationViewSelectorTypes_1 = require("../selectorTypes/expenseAutomationViewSelectorTypes");
19
- // Walks the tab's transactionReviewLocalDataById map and returns
20
- // `{ [transactionId]: true }` for every transaction that has at least
21
- // one line whose targeted-fetch is In-Progress. Pulled out as a plain
22
- // function so each tab's createSelector can share it.
23
- const computeCategorizationInFlightRecommendations = (transactionReviewLocalDataById) => {
24
- const result = {};
25
- if (transactionReviewLocalDataById == null) {
26
- return result;
27
- }
28
- for (const [transactionId, data] of Object.entries(transactionReviewLocalDataById)) {
29
- const lineItems = Object.values(data.transactionReviewLocalData.lineItemById);
30
- if (lineItems.some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
31
- 'In-Progress')) {
32
- result[transactionId] = true;
33
- }
34
- }
35
- return result;
36
- };
37
- // Per-tab createSelector instances. Each one is memoized on its OWN tab's
38
- // transactionReviewLocalDataById reference. Switching tabs A->B->A no
39
- // longer busts the memo for tab A — the dispatcher below just returns
40
- // whichever per-tab selector matches the active tab, and reselect keeps
41
- // the prior result alive as long as the underlying slice ref is stable.
42
- const getReviewInFlightRecommendationsByTransactionId = (0, toolkit_1.createSelector)((state) => state.expenseAutomationTransactionsViewState.transactionCategorizationView
43
- .review.transactionReviewLocalDataById, computeCategorizationInFlightRecommendations);
44
- const getAutoCategorizedInFlightRecommendationsByTransactionId = (0, toolkit_1.createSelector)((state) => state.expenseAutomationTransactionsViewState.transactionCategorizationView
45
- .autoCategorized.transactionReviewLocalDataById, computeCategorizationInFlightRecommendations);
46
- // Per-transaction "any line is fetching category/class recommendations?"
47
- // map for the active categorization tab. Dispatches to the per-tab
48
- // memoized selector above so steady state (no fetches in flight)
49
- // recomputes only when the active tab's local data changes; tab switches
50
- // are O(1) cache lookups.
51
- //
52
- // `default` guards against the union "lying" at runtime: `TransactionsTab`
53
- // is produced via `stringToUnion`, which falls back to `s as StringUnion`
54
- // for unrecognized strings, so an unexpected value can land in state. The
55
- // downstream `getCategorizationHasInFlightRecommendations` calls
56
- // `Object.keys` on this result, so we must always return an object.
57
- const getCategorizationInFlightRecommendationsByTransactionId = (state) => {
58
- const tab = state.expenseAutomationTransactionsViewState
59
- .selectedTransactionCategorizationTab;
60
- switch (tab) {
61
- case 'review':
62
- return getReviewInFlightRecommendationsByTransactionId(state);
63
- case 'autoCategorized':
64
- return getAutoCategorizedInFlightRecommendationsByTransactionId(state);
65
- default:
66
- return {};
67
- }
68
- };
69
- exports.getCategorizationInFlightRecommendationsByTransactionId = getCategorizationInFlightRecommendationsByTransactionId;
70
- // Page-level "any line on the active tab is in flight?" flag. Drives the
71
- // list-page Save disable.
72
- exports.getCategorizationHasInFlightRecommendations = (0, toolkit_1.createSelector)(exports.getCategorizationInFlightRecommendationsByTransactionId, (inFlightByTransactionId) => Object.keys(inFlightByTransactionId).length > 0);
73
17
  function getExpenseAutomationTransactionView(state) {
74
18
  const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
75
19
  const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
@@ -165,8 +109,6 @@ function getExpenseAutomationTransactionView(state) {
165
109
  : [];
166
110
  const transactionCompletionStatus = allSteps.find((step) => step.step === 'transaction_categorization')?.completionStatus;
167
111
  const completionStatus = transactionCompletionStatus ?? 'incomplete';
168
- const inFlightCategoryClassRecommendationsByTransactionId = (0, exports.getCategorizationInFlightRecommendationsByTransactionId)(state);
169
- const hasInFlightCategoryClassRecommendations = (0, exports.getCategorizationHasInFlightRecommendations)(state);
170
112
  return {
171
113
  version: 0,
172
114
  fetchState: fetchStatus.fetchState,
@@ -180,8 +122,6 @@ function getExpenseAutomationTransactionView(state) {
180
122
  classList: allClasses,
181
123
  accountsHierarchyList,
182
124
  classHierarchyList,
183
- hasInFlightCategoryClassRecommendations,
184
- inFlightCategoryClassRecommendationsByTransactionId,
185
125
  isAccountingProjectsEnabled,
186
126
  projectList,
187
127
  transactionLocalData,