@zeniai/client-epic-state 5.0.71-betaVR1 → 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 (25) hide show
  1. package/lib/epic.d.ts +3 -1
  2. package/lib/epic.js +3 -1
  3. package/lib/esm/epic.js +3 -1
  4. package/lib/esm/index.js +2 -2
  5. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +17 -7
  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/reducers/missingReceiptsViewReducer.js +78 -12
  10. package/lib/esm/view/expenseAutomationView/selectors/missingReceiptsSelector.js +2 -0
  11. package/lib/index.d.ts +2 -2
  12. package/lib/index.js +37 -34
  13. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchMultipleBatchDetailsEpic.js +17 -7
  14. package/lib/view/expenseAutomationView/epics/missingReceipts/refreshBatchDetailsForBatchIdEpic.d.ts +18 -0
  15. package/lib/view/expenseAutomationView/epics/missingReceipts/refreshBatchDetailsForBatchIdEpic.js +56 -0
  16. package/lib/view/expenseAutomationView/epics/missingReceipts/syncTabsAfterAutomatchEpic.d.ts +28 -0
  17. package/lib/view/expenseAutomationView/epics/missingReceipts/syncTabsAfterAutomatchEpic.js +57 -0
  18. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.d.ts +11 -5
  19. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +12 -8
  20. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +13 -1
  21. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +80 -13
  22. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +12 -0
  23. package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.js +2 -0
  24. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +19 -0
  25. package/package.json +1 -1
@@ -4,13 +4,19 @@ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
4
4
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
5
5
  import { RootState } from '../../../../reducer';
6
6
  import { ZeniAPI } from '../../../../zeniAPI';
7
- import { bulkUploadAutomatchingTimedOut, bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, pusherBatchStatusUpdate, restoreBulkUploadMatchingState } from '../../reducers/missingReceiptsViewReducer';
8
- export type ActionType = ReturnType<typeof bulkUploadReceiptsSuccess> | ReturnType<typeof restoreBulkUploadMatchingState> | ReturnType<typeof pusherBatchStatusUpdate> | ReturnType<typeof fetchBulkUploadBatchDetails> | ReturnType<typeof fetchBulkUploadBatchDetailsSuccess> | ReturnType<typeof fetchBulkUploadBatches> | ReturnType<typeof updateTransactions> | ReturnType<typeof clearBulkUpload> | ReturnType<typeof openSnackbar> | ReturnType<typeof bulkUploadAutomatchingTimedOut>;
7
+ import { bulkUploadAutomatchingTimedOut, bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, pusherBatchStatusUpdate, refreshBatchDetailsForBatchId, restoreBulkUploadMatchingState } from '../../reducers/missingReceiptsViewReducer';
8
+ export type ActionType = ReturnType<typeof bulkUploadReceiptsSuccess> | ReturnType<typeof restoreBulkUploadMatchingState> | ReturnType<typeof pusherBatchStatusUpdate> | ReturnType<typeof fetchBulkUploadBatchDetails> | ReturnType<typeof fetchBulkUploadBatchDetailsSuccess> | ReturnType<typeof fetchBulkUploadBatches> | ReturnType<typeof refreshBatchDetailsForBatchId> | ReturnType<typeof updateTransactions> | ReturnType<typeof clearBulkUpload> | ReturnType<typeof openSnackbar> | ReturnType<typeof bulkUploadAutomatchingTimedOut>;
9
9
  /**
10
- * On Pusher batch completion: refresh batch list (then fetchMultipleBatchDetailsEpic runs).
11
- * Debounced to reduce duplicate refreshes. The post-match tab decision
12
- * (Unmatched vs Completed) is made client-side in `ExpenseAutomationPage` once fresh
10
+ * On Pusher batch completion: refresh the batch list (so summary counts update) and dispatch a
11
+ * targeted single-batch details refresh for the batch that just completed. Stale-while-revalidate:
12
+ * we no longer pass `invalidateBatchDetailsCache: true` existing `batchDetailsById` entries stay
13
+ * on screen and the just-completed row replaces in place via `storeBatchDetails`. The post-match
14
+ * tab decision (Unmatched vs Completed) is made client-side in `ExpenseAutomationPage` once fresh
13
15
  * batch details land -- do not dispatch `requestMissingReceiptsTabNavigation` here.
16
+ *
17
+ * Pusher resend storms (same batch completing twice within the dedupe window) are throttled in
18
+ * `refreshBatchDetailsForBatchIdEpic` via `lastBatchDetailRefreshAtById`, so this epic stays
19
+ * stateless and just forwards the event.
14
20
  */
15
21
  export declare const pusherBatchStatusCompletionEpic: (actions$: ActionsObservable<ActionType>) => Observable<ActionType>;
16
22
  /**
@@ -15,16 +15,20 @@ const FALLBACK_FIRST_DELAY_MS = 30000;
15
15
  const RESTORE_FIRST_DELAY_MS = 5000;
16
16
  const FALLBACK_POLL_INTERVAL_MS = 10000;
17
17
  /**
18
- * On Pusher batch completion: refresh batch list (then fetchMultipleBatchDetailsEpic runs).
19
- * Debounced to reduce duplicate refreshes. The post-match tab decision
20
- * (Unmatched vs Completed) is made client-side in `ExpenseAutomationPage` once fresh
18
+ * On Pusher batch completion: refresh the batch list (so summary counts update) and dispatch a
19
+ * targeted single-batch details refresh for the batch that just completed. Stale-while-revalidate:
20
+ * we no longer pass `invalidateBatchDetailsCache: true` existing `batchDetailsById` entries stay
21
+ * on screen and the just-completed row replaces in place via `storeBatchDetails`. The post-match
22
+ * tab decision (Unmatched vs Completed) is made client-side in `ExpenseAutomationPage` once fresh
21
23
  * batch details land -- do not dispatch `requestMissingReceiptsTabNavigation` here.
24
+ *
25
+ * Pusher resend storms (same batch completing twice within the dedupe window) are throttled in
26
+ * `refreshBatchDetailsForBatchIdEpic` via `lastBatchDetailRefreshAtById`, so this epic stays
27
+ * stateless and just forwards the event.
22
28
  */
23
- const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.pusherBatchStatusUpdate.match), (0, operators_1.filter)((action) => action.payload.status === 'completed'), (0, operators_1.debounceTime)(300), (0, operators_1.mergeMap)(() => (0, rxjs_1.from)([
24
- (0, missingReceiptsViewReducer_1.fetchBulkUploadBatches)({
25
- cacheOverride: true,
26
- invalidateBatchDetailsCache: true,
27
- }),
29
+ const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.pusherBatchStatusUpdate.match), (0, operators_1.filter)((action) => action.payload.status === 'completed'), (0, operators_1.debounceTime)(300), (0, operators_1.mergeMap)((action) => (0, rxjs_1.from)([
30
+ (0, missingReceiptsViewReducer_1.fetchBulkUploadBatches)({ cacheOverride: true }),
31
+ (0, missingReceiptsViewReducer_1.refreshBatchDetailsForBatchId)({ batchId: action.payload.batchId }),
28
32
  ])));
29
33
  exports.pusherBatchStatusCompletionEpic = pusherBatchStatusCompletionEpic;
30
34
  /**
@@ -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,21 +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 = [];
329
- /**
330
- * Also reset pagination so the post-success epic re-runs the details chain from a
331
- * clean slate. Otherwise a previously `Completed` pagination state survives the cache
332
- * wipe and the Unmatched skeleton can stay up while no new details actually load.
333
- */
334
- draft.bulkUpload.batchDetailsPaginationState = {
335
- fetchState: 'Not-Started',
336
- error: undefined,
337
- };
379
+ draft.bulkUpload.batchDetailsRevalidating = true;
338
380
  }
339
381
  draft.bulkUpload.batchListFetchState = {
340
382
  fetchState: 'In-Progress',
@@ -351,6 +393,31 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
351
393
  };
352
394
  },
353
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
+ },
354
421
  fetchBulkUploadBatchesSuccess(draft, action) {
355
422
  const periodId = (0, timePeriod_1.toMonthYearPeriodId)(action.payload.selectedPeriod);
356
423
  draft.bulkUpload.batchListByPeriod[periodId] = action.payload.batchList;
@@ -593,7 +660,7 @@ const expenseAutomationMissingReceiptsView = (0, toolkit_1.createSlice)({
593
660
  });
594
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,
595
662
  // Bulk Upload
596
- 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,
597
664
  // Completed Transactions
598
665
  exports.fetchCompletedTransactions = _a.fetchCompletedTransactions, exports.fetchCompletedTransactionsSuccess = _a.fetchCompletedTransactionsSuccess, exports.fetchCompletedTransactionsFailure = _a.fetchCompletedTransactionsFailure;
599
666
  exports.default = expenseAutomationMissingReceiptsView.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;
@@ -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,
@@ -129,6 +129,13 @@ export interface CompletedTransactionsState {
129
129
  export interface BulkUploadState {
130
130
  batchDetailsById: Record<ID, BatchDetails>;
131
131
  batchDetailsPaginationState: FetchStateAndError;
132
+ /**
133
+ * True while a stale-while-revalidate refresh of `batchDetailsById` is in flight (manual refresh
134
+ * or scope-change replacement). Existing entries are kept on screen during this window; the UI
135
+ * can surface a lightweight refresh indicator. Cleared on `fetchMoreBatchDetailsComplete` /
136
+ * `fetchMoreBatchDetailsFailure`.
137
+ */
138
+ batchDetailsRevalidating: boolean;
132
139
  batchListByPeriod: Record<MonthYearPeriodId, BatchListItem[]>;
133
140
  batchListFetchState: FetchStateAndError;
134
141
  batchStatusById: Record<ID, BatchStatus>;
@@ -137,6 +144,18 @@ export interface BulkUploadState {
137
144
  confirmMatchStatus: FetchStateAndError;
138
145
  currentResultsTab: BulkUploadResultsTab;
139
146
  failedBatchDetailIds: ID[];
147
+ /**
148
+ * Set to `true` once the first batch-details page has resolved (success or empty completion).
149
+ * Drives stale-while-revalidate: the Unmatched skeleton is shown only while this is `false`.
150
+ * Reset only by `clearBulkUpload` or by an explicit scope-change (period/tenant) clear.
151
+ */
152
+ initialBatchDetailsLoaded: boolean;
153
+ /**
154
+ * Last time `GET /1.0/batches/:id` was issued for a given batch via the targeted refresh epic.
155
+ * Used to throttle Pusher resend storms — repeat completion events for the same batchId within
156
+ * the dedupe window are dropped.
157
+ */
158
+ lastBatchDetailRefreshAtById: Record<ID, number>;
140
159
  /**
141
160
  * Incremented when manual search Redux state is cleared so Unmatched `ManualSearchInput`
142
161
  * instances remount and drop local query text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.0.71-betaVR1",
3
+ "version": "5.0.71-betaVR2",
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",