@zeniai/client-epic-state 4.19.37-betaVR16 → 4.19.37-betaVR18

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.
@@ -1,5 +1,6 @@
1
+ import orderBy from 'lodash/orderBy';
1
2
  import { concat, of } from 'rxjs';
2
- import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { catchError, exhaustMap, filter } from 'rxjs/operators';
3
4
  import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
4
5
  import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
6
  import { createZeniAPIStatus } from '../../../../responsePayload';
@@ -7,7 +8,9 @@ import { isBatchListStatusCompleted } from '../../payload/missingReceiptsPayload
7
8
  import { fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, } from '../../reducers/missingReceiptsViewReducer';
8
9
  import { fetchBatchDetailsByIds } from './fetchMultipleBatchDetailsEpic';
9
10
  const MORE_BATCH_PAGE_SIZE = 4;
10
- export const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchMoreBatchDetails.match), mergeMap(() => {
11
+ export const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchMoreBatchDetails.match),
12
+ /** One page at a time — avoids duplicate loads if IO + scroll fallback both fire. */
13
+ exhaustMap(() => {
11
14
  const state = state$.value;
12
15
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationMissingReceiptsViewState: { bulkUpload }, } = state;
13
16
  const currentTenant = getCurrentTenant(state);
@@ -18,11 +21,10 @@ export const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$
18
21
  const periodId = toMonthYearPeriodId(selectedPeriod);
19
22
  const batchList = bulkUpload.batchListByPeriod[periodId] ?? [];
20
23
  const failedIds = new Set(bulkUpload.failedBatchDetailIds);
21
- const unfetchedBatchIds = batchList
22
- .filter((batch) => isBatchListStatusCompleted(batch.status) &&
24
+ const unfetchedBatches = batchList.filter((batch) => isBatchListStatusCompleted(batch.status) &&
23
25
  bulkUpload.batchDetailsById[batch.batchId] == null &&
24
- !failedIds.has(batch.batchId))
25
- .map((batch) => batch.batchId);
26
+ !failedIds.has(batch.batchId));
27
+ const unfetchedBatchIds = orderBy(unfetchedBatches, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
26
28
  if (unfetchedBatchIds.length === 0) {
27
29
  return of(fetchMoreBatchDetailsComplete());
28
30
  }
@@ -1,3 +1,4 @@
1
+ import orderBy from 'lodash/orderBy';
1
2
  import { EMPTY, concat, from, of } from 'rxjs';
2
3
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
4
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
@@ -31,10 +32,10 @@ export function fetchBatchDetailsByIds(batchIds, zeniAPI) {
31
32
  export const fetchMultipleBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchBulkUploadBatchesSuccess.match), mergeMap((action) => {
32
33
  const batchList = action.payload.batchList;
33
34
  const { batchDetailsById } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
34
- const completedBatchIds = batchList
35
- .filter((batch) => isBatchListStatusCompleted(batch.status) &&
36
- batchDetailsById[batch.batchId] == null)
37
- .map((batch) => batch.batchId);
35
+ /** Oldest first — matches primary-batch selection in missingReceiptsSelector (chronological). */
36
+ const completedBatchesMissingDetails = batchList.filter((batch) => isBatchListStatusCompleted(batch.status) &&
37
+ batchDetailsById[batch.batchId] == null);
38
+ const completedBatchIds = orderBy(completedBatchesMissingDetails, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
38
39
  const batchIdsToFetch = completedBatchIds.slice(0, INITIAL_BATCH_PAGE_SIZE);
39
40
  if (batchIdsToFetch.length === 0) {
40
41
  return EMPTY;
@@ -56,12 +56,15 @@ export function getExpenseAutomationMissingReceiptsView(state) {
56
56
  const batchList = monthYearPeriodId != null
57
57
  ? bulkUpload.batchListByPeriod[monthYearPeriodId] ?? []
58
58
  : [];
59
+ /** API returns newest-first (`sort_order: desc`); primary batch uses oldest-first order. */
60
+ const batchListChronological = orderBy(batchList, (b) => new Date(b.createdAt).valueOf(), 'asc');
59
61
  /**
60
- * Primary "Unmatched" batch = first batch in batch list order whose stored batchDetails
61
- * has status `completed`. If an earlier batch has no details yet (still loading), we return
62
- * undefined so we do not assign a later batch until ordering is known (avoids async reorder bugs).
62
+ * Primary "Unmatched" batch = first completed batch in chronological order, preferring the
63
+ * first batch that still has un_matched/no_match work when any completed batch has such work.
64
+ * Batches without details yet: wait if the list row still reports unmatched work; otherwise skip
65
+ * so we do not block the whole tab while older rows are not fetched yet.
63
66
  */
64
- const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchList, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
67
+ const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchListChronological, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
65
68
  /** During upload/matching, the in-flight batch may not appear in batchList yet — prefer currentBatchId. */
66
69
  const unmatchedSectionBatchId = bulkUpload.phase === 'matching' || bulkUpload.phase === 'uploading'
67
70
  ? bulkUpload.currentBatchId ?? primaryBatchIdFromDetailsOrder
@@ -112,9 +115,13 @@ export function getExpenseAutomationMissingReceiptsView(state) {
112
115
  }
113
116
  : { total: 0, processed: 0, percentage: 0 };
114
117
  const batchListFetchState = bulkUpload.batchListFetchState;
118
+ /**
119
+ * When primary batch is not resolved yet, still aggregate unmatched files from every batch
120
+ * that has details loaded (otherwise past is empty and receipts vanish from the UI).
121
+ */
115
122
  const pastBatches = unmatchedSectionBatchId != null
116
123
  ? batchList.filter((b) => b.batchId !== unmatchedSectionBatchId)
117
- : [];
124
+ : batchList;
118
125
  const pastUnmatchedFiles = pastBatches.flatMap((b) => {
119
126
  const details = bulkUpload.batchDetailsById[b.batchId];
120
127
  if (details == null) {
@@ -183,24 +190,57 @@ export function getExpenseAutomationMissingReceiptsView(state) {
183
190
  },
184
191
  };
185
192
  }
193
+ function batchDetailsHasUnmatchedWork(details) {
194
+ return details.files.some((f) => f.status === 'un_matched' || f.status === 'no_match');
195
+ }
196
+ /** List row summary before batch details load — aligns with unmatched / possible-match work. */
197
+ function batchListItemHasUnmatchedWork(batch) {
198
+ return batch.noMatchCount > 0 || batch.possibleMatchesCount > 0;
199
+ }
186
200
  /**
187
- * First batch in `batchList` order with `batchDetailsById[id].status === 'completed'`.
188
- * Skips failed detail fetches. Stops at the first batch with no details yet (still loading)
189
- * so a later batch is never chosen as primary until earlier list rows are resolved.
201
+ * `batchList` must be in chronological order (oldest first). Picks the primary batch for the
202
+ * top "Unmatched" section. Skips failed detail fetches.
203
+ *
204
+ * If details are not loaded for an older batch but the list row reports no unmatched work, we
205
+ * skip that batch so newer batches with loaded details can be primary (avoids an empty tab when
206
+ * the first fetch page only loaded newer batches). If the list reports unmatched work but details
207
+ * are still loading, we wait (undefined) so primary order stays correct once details arrive.
190
208
  */
191
- function getPrimaryBatchIdFromBatchListOrder(batchList, batchDetailsById, failedBatchDetailIds) {
209
+ function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetailsById, failedBatchDetailIds) {
192
210
  const failedIds = new Set(failedBatchDetailIds);
193
- for (const batch of batchList) {
211
+ const anyCompletedBatchHasUnmatchedWork = batchListChronological.some((b) => {
212
+ if (failedIds.has(b.batchId)) {
213
+ return false;
214
+ }
215
+ const d = batchDetailsById[b.batchId];
216
+ if (d != null &&
217
+ d.status === 'completed' &&
218
+ batchDetailsHasUnmatchedWork(d)) {
219
+ return true;
220
+ }
221
+ return d == null && batchListItemHasUnmatchedWork(b);
222
+ });
223
+ for (const batch of batchListChronological) {
194
224
  if (failedIds.has(batch.batchId)) {
195
225
  continue;
196
226
  }
197
227
  const details = batchDetailsById[batch.batchId];
198
228
  if (details == null) {
199
- return undefined;
229
+ if (batchListItemHasUnmatchedWork(batch)) {
230
+ return undefined;
231
+ }
232
+ continue;
200
233
  }
201
- if (details.status === 'completed') {
202
- return batch.batchId;
234
+ if (details.status !== 'completed') {
235
+ continue;
236
+ }
237
+ if (anyCompletedBatchHasUnmatchedWork) {
238
+ if (batchDetailsHasUnmatchedWork(details)) {
239
+ return batch.batchId;
240
+ }
241
+ continue;
203
242
  }
243
+ return batch.batchId;
204
244
  }
205
245
  return undefined;
206
246
  }
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.fetchMoreBatchDetailsEpic = void 0;
7
+ const orderBy_1 = __importDefault(require("lodash/orderBy"));
4
8
  const rxjs_1 = require("rxjs");
5
9
  const operators_1 = require("rxjs/operators");
6
10
  const timePeriod_1 = require("../../../../commonStateTypes/timePeriod");
@@ -10,7 +14,9 @@ const missingReceiptsPayload_1 = require("../../payload/missingReceiptsPayload")
10
14
  const missingReceiptsViewReducer_1 = require("../../reducers/missingReceiptsViewReducer");
11
15
  const fetchMultipleBatchDetailsEpic_1 = require("./fetchMultipleBatchDetailsEpic");
12
16
  const MORE_BATCH_PAGE_SIZE = 4;
13
- const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchMoreBatchDetails.match), (0, operators_1.mergeMap)(() => {
17
+ const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchMoreBatchDetails.match),
18
+ /** One page at a time — avoids duplicate loads if IO + scroll fallback both fire. */
19
+ (0, operators_1.exhaustMap)(() => {
14
20
  const state = state$.value;
15
21
  const { expenseAutomationViewState: { selectedPeriodByTenantId }, expenseAutomationMissingReceiptsViewState: { bulkUpload }, } = state;
16
22
  const currentTenant = (0, tenantSelector_1.getCurrentTenant)(state);
@@ -21,11 +27,10 @@ const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((
21
27
  const periodId = (0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod);
22
28
  const batchList = bulkUpload.batchListByPeriod[periodId] ?? [];
23
29
  const failedIds = new Set(bulkUpload.failedBatchDetailIds);
24
- const unfetchedBatchIds = batchList
25
- .filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
30
+ const unfetchedBatches = batchList.filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
26
31
  bulkUpload.batchDetailsById[batch.batchId] == null &&
27
- !failedIds.has(batch.batchId))
28
- .map((batch) => batch.batchId);
32
+ !failedIds.has(batch.batchId));
33
+ const unfetchedBatchIds = (0, orderBy_1.default)(unfetchedBatches, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
29
34
  if (unfetchedBatchIds.length === 0) {
30
35
  return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchMoreBatchDetailsComplete)());
31
36
  }
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.fetchMultipleBatchDetailsEpic = void 0;
4
7
  exports.fetchBatchDetailsByIds = fetchBatchDetailsByIds;
8
+ const orderBy_1 = __importDefault(require("lodash/orderBy"));
5
9
  const rxjs_1 = require("rxjs");
6
10
  const operators_1 = require("rxjs/operators");
7
11
  const transactionReducer_1 = require("../../../../entity/transaction/transactionReducer");
@@ -35,10 +39,10 @@ function fetchBatchDetailsByIds(batchIds, zeniAPI) {
35
39
  const fetchMultipleBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchBulkUploadBatchesSuccess.match), (0, operators_1.mergeMap)((action) => {
36
40
  const batchList = action.payload.batchList;
37
41
  const { batchDetailsById } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
38
- const completedBatchIds = batchList
39
- .filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
40
- batchDetailsById[batch.batchId] == null)
41
- .map((batch) => batch.batchId);
42
+ /** Oldest first — matches primary-batch selection in missingReceiptsSelector (chronological). */
43
+ const completedBatchesMissingDetails = batchList.filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
44
+ batchDetailsById[batch.batchId] == null);
45
+ const completedBatchIds = (0, orderBy_1.default)(completedBatchesMissingDetails, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
42
46
  const batchIdsToFetch = completedBatchIds.slice(0, INITIAL_BATCH_PAGE_SIZE);
43
47
  if (batchIdsToFetch.length === 0) {
44
48
  return rxjs_1.EMPTY;
@@ -62,12 +62,15 @@ function getExpenseAutomationMissingReceiptsView(state) {
62
62
  const batchList = monthYearPeriodId != null
63
63
  ? bulkUpload.batchListByPeriod[monthYearPeriodId] ?? []
64
64
  : [];
65
+ /** API returns newest-first (`sort_order: desc`); primary batch uses oldest-first order. */
66
+ const batchListChronological = (0, orderBy_1.default)(batchList, (b) => new Date(b.createdAt).valueOf(), 'asc');
65
67
  /**
66
- * Primary "Unmatched" batch = first batch in batch list order whose stored batchDetails
67
- * has status `completed`. If an earlier batch has no details yet (still loading), we return
68
- * undefined so we do not assign a later batch until ordering is known (avoids async reorder bugs).
68
+ * Primary "Unmatched" batch = first completed batch in chronological order, preferring the
69
+ * first batch that still has un_matched/no_match work when any completed batch has such work.
70
+ * Batches without details yet: wait if the list row still reports unmatched work; otherwise skip
71
+ * so we do not block the whole tab while older rows are not fetched yet.
69
72
  */
70
- const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchList, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
73
+ const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchListChronological, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
71
74
  /** During upload/matching, the in-flight batch may not appear in batchList yet — prefer currentBatchId. */
72
75
  const unmatchedSectionBatchId = bulkUpload.phase === 'matching' || bulkUpload.phase === 'uploading'
73
76
  ? bulkUpload.currentBatchId ?? primaryBatchIdFromDetailsOrder
@@ -118,9 +121,13 @@ function getExpenseAutomationMissingReceiptsView(state) {
118
121
  }
119
122
  : { total: 0, processed: 0, percentage: 0 };
120
123
  const batchListFetchState = bulkUpload.batchListFetchState;
124
+ /**
125
+ * When primary batch is not resolved yet, still aggregate unmatched files from every batch
126
+ * that has details loaded (otherwise past is empty and receipts vanish from the UI).
127
+ */
121
128
  const pastBatches = unmatchedSectionBatchId != null
122
129
  ? batchList.filter((b) => b.batchId !== unmatchedSectionBatchId)
123
- : [];
130
+ : batchList;
124
131
  const pastUnmatchedFiles = pastBatches.flatMap((b) => {
125
132
  const details = bulkUpload.batchDetailsById[b.batchId];
126
133
  if (details == null) {
@@ -189,24 +196,57 @@ function getExpenseAutomationMissingReceiptsView(state) {
189
196
  },
190
197
  };
191
198
  }
199
+ function batchDetailsHasUnmatchedWork(details) {
200
+ return details.files.some((f) => f.status === 'un_matched' || f.status === 'no_match');
201
+ }
202
+ /** List row summary before batch details load — aligns with unmatched / possible-match work. */
203
+ function batchListItemHasUnmatchedWork(batch) {
204
+ return batch.noMatchCount > 0 || batch.possibleMatchesCount > 0;
205
+ }
192
206
  /**
193
- * First batch in `batchList` order with `batchDetailsById[id].status === 'completed'`.
194
- * Skips failed detail fetches. Stops at the first batch with no details yet (still loading)
195
- * so a later batch is never chosen as primary until earlier list rows are resolved.
207
+ * `batchList` must be in chronological order (oldest first). Picks the primary batch for the
208
+ * top "Unmatched" section. Skips failed detail fetches.
209
+ *
210
+ * If details are not loaded for an older batch but the list row reports no unmatched work, we
211
+ * skip that batch so newer batches with loaded details can be primary (avoids an empty tab when
212
+ * the first fetch page only loaded newer batches). If the list reports unmatched work but details
213
+ * are still loading, we wait (undefined) so primary order stays correct once details arrive.
196
214
  */
197
- function getPrimaryBatchIdFromBatchListOrder(batchList, batchDetailsById, failedBatchDetailIds) {
215
+ function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetailsById, failedBatchDetailIds) {
198
216
  const failedIds = new Set(failedBatchDetailIds);
199
- for (const batch of batchList) {
217
+ const anyCompletedBatchHasUnmatchedWork = batchListChronological.some((b) => {
218
+ if (failedIds.has(b.batchId)) {
219
+ return false;
220
+ }
221
+ const d = batchDetailsById[b.batchId];
222
+ if (d != null &&
223
+ d.status === 'completed' &&
224
+ batchDetailsHasUnmatchedWork(d)) {
225
+ return true;
226
+ }
227
+ return d == null && batchListItemHasUnmatchedWork(b);
228
+ });
229
+ for (const batch of batchListChronological) {
200
230
  if (failedIds.has(batch.batchId)) {
201
231
  continue;
202
232
  }
203
233
  const details = batchDetailsById[batch.batchId];
204
234
  if (details == null) {
205
- return undefined;
235
+ if (batchListItemHasUnmatchedWork(batch)) {
236
+ return undefined;
237
+ }
238
+ continue;
206
239
  }
207
- if (details.status === 'completed') {
208
- return batch.batchId;
240
+ if (details.status !== 'completed') {
241
+ continue;
242
+ }
243
+ if (anyCompletedBatchHasUnmatchedWork) {
244
+ if (batchDetailsHasUnmatchedWork(details)) {
245
+ return batch.batchId;
246
+ }
247
+ continue;
209
248
  }
249
+ return batch.batchId;
210
250
  }
211
251
  return undefined;
212
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "4.19.37-betaVR16",
3
+ "version": "4.19.37-betaVR18",
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",