@zeniai/client-epic-state 5.0.64-betaSS1 → 5.0.65-betaML1
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.
- package/lib/epic.d.ts +2 -1
- package/lib/epic.js +2 -1
- package/lib/esm/epic.js +2 -1
- package/lib/esm/index.js +6 -6
- package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +21 -1
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +137 -21
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -0
- package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +78 -0
- package/lib/esm/view/recommendation/recommendationHelper.js +29 -0
- package/lib/esm/view/recommendation/recommendationReducer.js +36 -1
- package/lib/esm/view/transactionDetail/transactionDetailReducer.js +94 -1
- package/lib/esm/view/transactionDetail/transactionDetailSelector.js +22 -0
- package/lib/index.d.ts +7 -6
- package/lib/index.js +35 -32
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +23 -2
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +14 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +137 -21
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +2 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +3 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +29 -0
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +6 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +9 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +82 -0
- package/lib/view/recommendation/recommendationHelper.d.ts +9 -0
- package/lib/view/recommendation/recommendationHelper.js +31 -1
- package/lib/view/recommendation/recommendationReducer.d.ts +18 -1
- package/lib/view/recommendation/recommendationReducer.js +37 -2
- package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -1
- package/lib/view/transactionDetail/transactionDetailReducer.js +95 -2
- package/lib/view/transactionDetail/transactionDetailSelector.d.ts +2 -0
- package/lib/view/transactionDetail/transactionDetailSelector.js +24 -1
- package/lib/view/transactionDetail/transactionDetailTypes.d.ts +1 -0
- package/package.json +1 -1
package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare const removeTransactionFromTabView: (tabView: TransactionsTabView
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const filterAutoTabLineItems: (tabView: TransactionsTabViewState, transactionId: ID, updatedTransaction: SupportedTransactionWithCOT) => void;
|
|
29
29
|
export declare const MAX_SELECTION_LIMIT = 5;
|
|
30
|
+
export declare const shouldAutoSelectAndAdd: (transaction: SupportedTransactionWithCOT, localData: TransactionReviewLocalData, selectedCheckBoxTransactionIds: ID[], uncategorizedAccounts: UncategorizedAccounts, isUncategorizedExpenseCategoryEnabled?: boolean, isAccountingClassesEnabled?: boolean) => boolean;
|
|
30
31
|
export declare const toTransactionDetailLocalData: (transaction: SupportedTransactionWithCOT, selectedTab: TransactionsTab, lineItemsByTransactionIds: string[], uncategorizedIncomeExpense: UncategorizedAccounts, isUncategorizedExpenseCategoryEnabled?: boolean) => TransactionReviewLocalData;
|
|
31
32
|
export declare const toSetAllLineItemsToCategoryClass: (draft: TransactionsTabViewState, transaction: SupportedTransactionWithCOT, transactionId: ID, transactionLineId: ID, transactionDetailLocalData: TransactionReviewLocalData, updatedItem: "category" | "class" | "project", uncategorizedAccounts: UncategorizedAccounts, selectedTab: TransactionsTab, vendorId?: ID, customerId?: ID, selectedAccount?: AccountBase, selectedClassBase?: ClassBase, selectedProjectBase?: ProjectBase, isUncategorizedExpenseCategoryEnabled?: boolean, isAccountingClassesEnabled?: boolean) => {
|
|
32
33
|
localData: TransactionReviewLocalData;
|
|
@@ -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.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.shouldAutoSelectAndAdd = 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,6 +65,23 @@ 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;
|
|
68
85
|
const toTransactionDetailLocalData = (transaction, selectedTab, lineItemsByTransactionIds, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled) => {
|
|
69
86
|
const transactionLocal = {
|
|
70
87
|
...transactionsViewState_1.initialSupportedTransactionCategorization.transactionReviewLocalData,
|
|
@@ -552,10 +569,14 @@ const setEntityRecommendationForLineIdInLocalData = (draft, transaction, transac
|
|
|
552
569
|
selectedEntity: transactionDetailLocalData.selectedEntity,
|
|
553
570
|
tabSpecificLineItems: transactionDetailLocalData.tabSpecificLineItems,
|
|
554
571
|
};
|
|
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.
|
|
555
576
|
if (!(0, exports.checkIfAllLineItemsAreCategoryClassFilled)(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
|
|
556
577
|
selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds.filter((id) => id !== transaction.id);
|
|
557
578
|
}
|
|
558
|
-
else if (
|
|
579
|
+
else if ((0, exports.shouldAutoSelectAndAdd)(transaction, newLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
|
|
559
580
|
selectedCheckBoxTransactionIds.push(transaction.id);
|
|
560
581
|
}
|
|
561
582
|
draft.selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds;
|
|
@@ -157,7 +157,20 @@ 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>,
|
|
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<{
|
|
161
174
|
lineId: ID;
|
|
162
175
|
selectedTab: TransactionsTab;
|
|
163
176
|
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.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.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;
|
|
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"));
|
|
@@ -72,15 +72,20 @@ exports.initialState = {
|
|
|
72
72
|
*
|
|
73
73
|
* Snapshot scope (deliberately narrow):
|
|
74
74
|
* - Captured: `pageToken`, `totalCount`, `transactionIdsBySelectedPeriod`,
|
|
75
|
-
* `fetchState`, `error`.
|
|
76
|
-
* reads to decide whether to short-circuit a
|
|
75
|
+
* `fetchState`, `error`, `scrollPosition`. The first five are the keys the
|
|
76
|
+
* fetch epic's cache check reads to decide whether to short-circuit a
|
|
77
|
+
* sub-tab revisit; `scrollPosition` is captured per sub-tab so revisiting
|
|
78
|
+
* restores the user's last offset and switching to a never-visited sub-tab
|
|
79
|
+
* starts at the top instead of inheriting the outgoing sub-tab's offset
|
|
80
|
+
* (which would land the user at a meaningless row index against a
|
|
81
|
+
* completely different list).
|
|
77
82
|
* - Intentionally NOT captured (left in place on `autoCat` and inherited by
|
|
78
83
|
* the incoming sub-tab):
|
|
79
84
|
* - `uiState.searchString`, `uiState.sortKey`, `uiState.sortOrder`,
|
|
80
|
-
* `uiState.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
85
|
+
* `uiState.nodeCollapseState`, `uiState.limit` — search and sort are
|
|
86
|
+
* user-applied preferences that persist across sub-tabs by design (a
|
|
87
|
+
* search query stays active when the user toggles between Manual / AI
|
|
88
|
+
* Accountant).
|
|
84
89
|
* - `selectedCheckBoxTransactionIds` — selection state is intentionally
|
|
85
90
|
* cleared at the action source (the fetch epic / save flow), not
|
|
86
91
|
* by the snapshot path. Carrying selection across sub-tabs is safe
|
|
@@ -107,6 +112,7 @@ function snapshotAutoCategorizedTab(autoCat) {
|
|
|
107
112
|
error: autoCat.error,
|
|
108
113
|
fetchState: normalisedFetchState,
|
|
109
114
|
pageToken: autoCat.uiState.pageToken,
|
|
115
|
+
scrollPosition: autoCat.uiState.scrollPosition,
|
|
110
116
|
totalCount: { ...autoCat.uiState.totalCount },
|
|
111
117
|
transactionIdsBySelectedPeriod,
|
|
112
118
|
};
|
|
@@ -343,6 +349,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
343
349
|
if (cached != null) {
|
|
344
350
|
autoCat.uiState.pageToken = cached.pageToken;
|
|
345
351
|
autoCat.uiState.totalCount = { ...cached.totalCount };
|
|
352
|
+
autoCat.uiState.scrollPosition = cached.scrollPosition;
|
|
346
353
|
autoCat.transactionIdsBySelectedPeriod = {
|
|
347
354
|
...cached.transactionIdsBySelectedPeriod,
|
|
348
355
|
};
|
|
@@ -353,9 +360,15 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
353
360
|
// Sub-tab has never been visited — clear the active slot so the
|
|
354
361
|
// view-epic's `fetchState === 'Not-Started' || transactionIds.length
|
|
355
362
|
// === 0` cache check falls through and triggers a fresh page-1
|
|
356
|
-
// fetch for the new dataset.
|
|
363
|
+
// fetch for the new dataset. `scrollPosition` is reset to the top
|
|
364
|
+
// so the incoming list isn't anchored to the outgoing sub-tab's
|
|
365
|
+
// offset (different rows → previous offset is meaningless).
|
|
357
366
|
autoCat.uiState.pageToken = null;
|
|
358
367
|
autoCat.uiState.totalCount = {};
|
|
368
|
+
autoCat.uiState.scrollPosition = {
|
|
369
|
+
scrollTop: 0,
|
|
370
|
+
scrollLeft: undefined,
|
|
371
|
+
};
|
|
359
372
|
autoCat.transactionIdsBySelectedPeriod = {};
|
|
360
373
|
autoCat.fetchState = 'Not-Started';
|
|
361
374
|
autoCat.error = undefined;
|
|
@@ -414,12 +427,9 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
414
427
|
const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
|
|
415
428
|
.selectedCheckBoxTransactionIds;
|
|
416
429
|
if (selectedTransaction != null &&
|
|
417
|
-
(0, transactionCategorizationLocalDataHelper_1.
|
|
418
|
-
selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
|
|
419
|
-
selectedCheckBoxTransactionIds.includes(transactionId) === false) {
|
|
430
|
+
(0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
|
|
420
431
|
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
421
|
-
...
|
|
422
|
-
.selectedCheckBoxTransactionIds,
|
|
432
|
+
...selectedCheckBoxTransactionIds,
|
|
423
433
|
transactionId,
|
|
424
434
|
];
|
|
425
435
|
}
|
|
@@ -502,9 +512,26 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
502
512
|
const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
|
|
503
513
|
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
504
514
|
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
|
+
});
|
|
505
532
|
draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
|
|
506
533
|
transactionId: transaction.id,
|
|
507
|
-
transactionReviewLocalData:
|
|
534
|
+
transactionReviewLocalData: newLocalData,
|
|
508
535
|
};
|
|
509
536
|
// Mark transaction as dirty (autofill happened)
|
|
510
537
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
|
|
@@ -517,6 +544,99 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
517
544
|
}
|
|
518
545
|
},
|
|
519
546
|
},
|
|
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
|
+
},
|
|
520
640
|
fetchTransactionCategorizationSuccess(draft, action) {
|
|
521
641
|
const { selectedTab, refreshViewInBackground } = action.payload;
|
|
522
642
|
if (draft.transactionCategorizationView[selectedTab].fetchState ===
|
|
@@ -612,9 +732,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
612
732
|
.selectedCheckBoxTransactionIds;
|
|
613
733
|
if (newTransactionLocalData.customer != null &&
|
|
614
734
|
newTransactionLocalData.customer.id != null &&
|
|
615
|
-
(0, transactionCategorizationLocalDataHelper_1.
|
|
616
|
-
selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
|
|
617
|
-
selectedCheckBoxTransactionIds.includes(transactionId) === false) {
|
|
735
|
+
(0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
|
|
618
736
|
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
619
737
|
...selectedCheckBoxTransactionIds,
|
|
620
738
|
transactionId,
|
|
@@ -688,9 +806,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
688
806
|
.selectedCheckBoxTransactionIds;
|
|
689
807
|
if (newTransactionLocalData.vendor != null &&
|
|
690
808
|
newTransactionLocalData.vendor.id != null &&
|
|
691
|
-
(0, transactionCategorizationLocalDataHelper_1.
|
|
692
|
-
selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
|
|
693
|
-
selectedCheckBoxTransactionIds.includes(transactionId) === false) {
|
|
809
|
+
(0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
|
|
694
810
|
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
695
811
|
...selectedCheckBoxTransactionIds,
|
|
696
812
|
transactionId,
|
|
@@ -923,5 +1039,5 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
923
1039
|
},
|
|
924
1040
|
},
|
|
925
1041
|
});
|
|
926
|
-
_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;
|
|
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;
|
|
927
1043
|
exports.default = expenseAutomationTransactionsView.reducer;
|
|
@@ -20,6 +20,8 @@ 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>;
|
|
23
25
|
isAccountingProjectsEnabled: boolean;
|
|
24
26
|
markAsNotMiscategorizedStatus: FetchStateAndError;
|
|
25
27
|
/**
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ID } from '../../../commonStateTypes/common';
|
|
1
2
|
import { RootState } from '../../../reducer';
|
|
2
3
|
import { ExpenseAutomationTransactionViewSelector } from '../selectorTypes/transactionsViewSelectorTypes';
|
|
4
|
+
export declare const getCategorizationInFlightRecommendationsByTransactionId: (state: RootState) => Record<ID, boolean>;
|
|
5
|
+
export declare const getCategorizationHasInFlightRecommendations: (state: RootState) => boolean;
|
|
3
6
|
export declare function getExpenseAutomationTransactionView(state: RootState): ExpenseAutomationTransactionViewSelector;
|
|
@@ -3,7 +3,9 @@ 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;
|
|
6
7
|
exports.getExpenseAutomationTransactionView = getExpenseAutomationTransactionView;
|
|
8
|
+
const toolkit_1 = require("@reduxjs/toolkit");
|
|
7
9
|
const omit_1 = __importDefault(require("lodash/omit"));
|
|
8
10
|
const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
|
|
9
11
|
const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
|
|
@@ -14,6 +16,29 @@ const accountListSelector_1 = require("../../accountList/accountListSelector");
|
|
|
14
16
|
const classListSelector_1 = require("../../classList/classListSelector");
|
|
15
17
|
const projectListSelector_1 = require("../../projectList/projectListSelector");
|
|
16
18
|
const expenseAutomationViewSelectorTypes_1 = require("../selectorTypes/expenseAutomationViewSelectorTypes");
|
|
19
|
+
// Per-transaction "any line is fetching category/class recommendations?"
|
|
20
|
+
// map for the active categorization tab. Memoized on the active tab's
|
|
21
|
+
// transactionReviewLocalDataById reference, so steady state (no fetches
|
|
22
|
+
// in flight) recomputes only when local data changes — and the row-table
|
|
23
|
+
// can read a single map lookup per row instead of re-iterating the
|
|
24
|
+
// row's lineItemById in the render path.
|
|
25
|
+
exports.getCategorizationInFlightRecommendationsByTransactionId = (0, toolkit_1.createSelector)((state) => state.expenseAutomationTransactionsViewState
|
|
26
|
+
.selectedTransactionCategorizationTab, (state) => state.expenseAutomationTransactionsViewState
|
|
27
|
+
.transactionCategorizationView, (selectedTab, transactionCategorizationView) => {
|
|
28
|
+
const tab = transactionCategorizationView[selectedTab];
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const [transactionId, data] of Object.entries(tab.transactionReviewLocalDataById)) {
|
|
31
|
+
const lineItems = Object.values(data.transactionReviewLocalData.lineItemById);
|
|
32
|
+
if (lineItems.some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
|
|
33
|
+
'In-Progress')) {
|
|
34
|
+
result[transactionId] = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
39
|
+
// Page-level "any line on the active tab is in flight?" flag. Drives the
|
|
40
|
+
// list-page Save disable.
|
|
41
|
+
exports.getCategorizationHasInFlightRecommendations = (0, toolkit_1.createSelector)(exports.getCategorizationInFlightRecommendationsByTransactionId, (inFlightByTransactionId) => Object.keys(inFlightByTransactionId).length > 0);
|
|
17
42
|
function getExpenseAutomationTransactionView(state) {
|
|
18
43
|
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
19
44
|
const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
|
|
@@ -109,6 +134,8 @@ function getExpenseAutomationTransactionView(state) {
|
|
|
109
134
|
: [];
|
|
110
135
|
const transactionCompletionStatus = allSteps.find((step) => step.step === 'transaction_categorization')?.completionStatus;
|
|
111
136
|
const completionStatus = transactionCompletionStatus ?? 'incomplete';
|
|
137
|
+
const inFlightCategoryClassRecommendationsByTransactionId = (0, exports.getCategorizationInFlightRecommendationsByTransactionId)(state);
|
|
138
|
+
const hasInFlightCategoryClassRecommendations = (0, exports.getCategorizationHasInFlightRecommendations)(state);
|
|
112
139
|
return {
|
|
113
140
|
version: 0,
|
|
114
141
|
fetchState: fetchStatus.fetchState,
|
|
@@ -122,6 +149,8 @@ function getExpenseAutomationTransactionView(state) {
|
|
|
122
149
|
classList: allClasses,
|
|
123
150
|
accountsHierarchyList,
|
|
124
151
|
classHierarchyList,
|
|
152
|
+
hasInFlightCategoryClassRecommendations,
|
|
153
|
+
inFlightCategoryClassRecommendationsByTransactionId,
|
|
125
154
|
isAccountingProjectsEnabled,
|
|
126
155
|
projectList,
|
|
127
156
|
transactionLocalData,
|
|
@@ -48,6 +48,7 @@ export interface TransactionCategorizationLineItemData extends TransactionLineBa
|
|
|
48
48
|
transactionId: ID;
|
|
49
49
|
account?: AccountBase;
|
|
50
50
|
categorizationStatus?: CategorizationStatusType;
|
|
51
|
+
categoryClassRecommendationsFetchState?: FetchStateAndError;
|
|
51
52
|
class?: ClassBase;
|
|
52
53
|
customer?: CustomerBase;
|
|
53
54
|
item?: ProductOrService;
|
|
@@ -112,11 +113,16 @@ export interface TransactionsTabViewState extends FetchedState {
|
|
|
112
113
|
* (mirrors parent-tab caching). The snapshot is period-spanning — both the
|
|
113
114
|
* `totalCount` map and `transactionIdsBySelectedPeriod` map preserve every
|
|
114
115
|
* period the user has paged through under that sub-tab.
|
|
116
|
+
*
|
|
117
|
+
* `scrollPosition` is captured per sub-tab so revisiting a sub-tab restores
|
|
118
|
+
* the user's last scroll offset; switching to a fresh / never-visited sub-tab
|
|
119
|
+
* starts at the top instead of inheriting the previous sub-tab's offset.
|
|
115
120
|
*/
|
|
116
121
|
export interface AutoCategorizedSubTabSnapshot {
|
|
117
122
|
error: ZeniAPIStatus | undefined;
|
|
118
123
|
fetchState: FetchState;
|
|
119
124
|
pageToken: PageToken;
|
|
125
|
+
scrollPosition: TransactionsViewUIState['scrollPosition'];
|
|
120
126
|
totalCount: Record<MonthYearPeriodId, number>;
|
|
121
127
|
transactionIdsBySelectedPeriod: Record<MonthYearPeriodId, ID[]>;
|
|
122
128
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { RootState } from '../../reducer';
|
|
4
|
+
import { ZeniAPI } from '../../zeniAPI';
|
|
5
|
+
import { markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, markCategoryClassRecommendationsInProgressForCategorization, setEntityRecommendationForLineIdsForCategorization } from '../expenseAutomationView/reducers/transactionsViewReducer';
|
|
6
|
+
import { markCategoryClassRecommendationsCompletedForTransactionDetail, markCategoryClassRecommendationsFailureForTransactionDetail, markCategoryClassRecommendationsInProgressForTransactionDetail, setEntityRecommendationForLineIdsForTransactionDetail } from '../transactionDetail/transactionDetailReducer';
|
|
7
|
+
import { fetchEntityRecommendationsForLineUpdate } from './recommendationReducer';
|
|
8
|
+
export type ActionType = ReturnType<typeof fetchEntityRecommendationsForLineUpdate> | ReturnType<typeof markCategoryClassRecommendationsInProgressForCategorization> | ReturnType<typeof markCategoryClassRecommendationsCompletedForCategorization> | ReturnType<typeof markCategoryClassRecommendationsFailureForCategorization> | ReturnType<typeof markCategoryClassRecommendationsInProgressForTransactionDetail> | ReturnType<typeof markCategoryClassRecommendationsCompletedForTransactionDetail> | ReturnType<typeof markCategoryClassRecommendationsFailureForTransactionDetail> | ReturnType<typeof setEntityRecommendationForLineIdsForCategorization> | ReturnType<typeof setEntityRecommendationForLineIdsForTransactionDetail>;
|
|
9
|
+
export declare const fetchEntityRecommendationsForLineUpdateEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchEntityRecommendationsForLineUpdateEpic = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const tenantSelector_1 = require("../../entity/tenant/tenantSelector");
|
|
7
|
+
const transactionHelper_1 = require("../../entity/transaction/transactionHelper");
|
|
8
|
+
const responsePayload_1 = require("../../responsePayload");
|
|
9
|
+
const accountListSelector_1 = require("../accountList/accountListSelector");
|
|
10
|
+
const transactionsViewReducer_1 = require("../expenseAutomationView/reducers/transactionsViewReducer");
|
|
11
|
+
const transactionDetailReducer_1 = require("../transactionDetail/transactionDetailReducer");
|
|
12
|
+
const recommendationHelper_1 = require("./recommendationHelper");
|
|
13
|
+
const recommendationReducer_1 = require("./recommendationReducer");
|
|
14
|
+
// Close the per-target groupBy bucket after this much idle time so inactive
|
|
15
|
+
// groups don't accumulate forever (each group holds a Subject + a chain of
|
|
16
|
+
// active subscribers; without a duration selector groupBy retains them for
|
|
17
|
+
// the lifetime of the source observable, which is the lifetime of the app).
|
|
18
|
+
// Picked at 60s to leave a generous window for re-fetches on the same
|
|
19
|
+
// payee/transaction/line target while still bounding memory.
|
|
20
|
+
const GROUP_INACTIVITY_TIMEOUT_MS = 60000;
|
|
21
|
+
// Build the groupBy key. Same flow + same transaction + same line set
|
|
22
|
+
// share a target so switchMap can cancel an in-flight prior fetch. Use
|
|
23
|
+
// NUL (\u0000) as separator because it can never appear in transaction
|
|
24
|
+
// or line ids (vs the `|` used in an earlier WIP which collides with
|
|
25
|
+
// pipe-delimited synthetic ids).
|
|
26
|
+
const SEPARATOR = '\u0000';
|
|
27
|
+
const toGroupKey = (flowType, transactionId, lineIds) => `${flowType}${SEPARATOR}${transactionId}${SEPARATOR}${[...lineIds]
|
|
28
|
+
.sort((a, b) => a.localeCompare(b))
|
|
29
|
+
.join(SEPARATOR)}`;
|
|
30
|
+
// Targeted recommendations fetch that drives the per-line skeleton, the
|
|
31
|
+
// list-page row-checkbox uncheck/recheck, and the page-level Save
|
|
32
|
+
// disable.
|
|
33
|
+
//
|
|
34
|
+
// Concurrency model:
|
|
35
|
+
// - groupBy by (flowType, transactionId, sorted lineIds) so distinct
|
|
36
|
+
// targets run in parallel.
|
|
37
|
+
// - inside each group, switchMap so a re-fetch on the SAME target
|
|
38
|
+
// cancels any prior in-flight request for that target.
|
|
39
|
+
// - groupBy duration selector closes idle groups to bound memory.
|
|
40
|
+
const fetchEntityRecommendationsForLineUpdateEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(recommendationReducer_1.fetchEntityRecommendationsForLineUpdate.match), (0, operators_1.groupBy)((action) => toGroupKey(action.payload.flowType, action.payload.transactionDetails.transactionId.id, action.payload.transactionDetails.lineIds), undefined, (group$) => group$.pipe((0, operators_1.debounce)(() => (0, rxjs_1.timer)(GROUP_INACTIVITY_TIMEOUT_MS)))), (0, operators_1.mergeMap)((group$) => group$.pipe((0, operators_1.switchMap)((action) => {
|
|
41
|
+
const { entity, amount, transactionDetails, flowType, selectedTab, isFetchCOT, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
42
|
+
const { transactionId, transaction, lineIds } = transactionDetails;
|
|
43
|
+
// Synchronously emit the start arm so the per-line skeleton +
|
|
44
|
+
// row uncheck happen before the HTTP request begins.
|
|
45
|
+
const startAction = flowType === 'categorization' && selectedTab != null
|
|
46
|
+
? (0, transactionsViewReducer_1.markCategoryClassRecommendationsInProgressForCategorization)(selectedTab, transactionId, lineIds)
|
|
47
|
+
: (0, transactionDetailReducer_1.markCategoryClassRecommendationsInProgressForTransactionDetail)(transactionId, lineIds);
|
|
48
|
+
const url = (0, recommendationHelper_1.buildRecommendationUrl)(zeniAPI.apiEndPoints.accountMicroServiceBaseUrl, entity, amount, transactionDetails, isFetchCOT === true);
|
|
49
|
+
const httpStream$ = zeniAPI
|
|
50
|
+
.getJSON(url)
|
|
51
|
+
.pipe((0, operators_1.mergeMap)((response) => {
|
|
52
|
+
if (!(0, responsePayload_1.isSuccessResponse)(response) || response.data == null) {
|
|
53
|
+
return (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
|
|
54
|
+
? (0, transactionsViewReducer_1.markCategoryClassRecommendationsFailureForCategorization)(selectedTab, transactionId, lineIds, response.status)
|
|
55
|
+
: (0, transactionDetailReducer_1.markCategoryClassRecommendationsFailureForTransactionDetail)(transactionId, lineIds, response.status));
|
|
56
|
+
}
|
|
57
|
+
const recommendationWithCOTByLineId = (0, recommendationHelper_1.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT)(response.data.recommendations, lineIds);
|
|
58
|
+
// HTTP 200 with no per-line recommendations: NOT an error.
|
|
59
|
+
// Flip lines to Completed so the skeleton resolves and
|
|
60
|
+
// downstream UI can treat this as "we tried, nothing to
|
|
61
|
+
// apply".
|
|
62
|
+
if (recommendationWithCOTByLineId == null) {
|
|
63
|
+
return (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
|
|
64
|
+
? (0, transactionsViewReducer_1.markCategoryClassRecommendationsCompletedForCategorization)(selectedTab, transactionId, lineIds)
|
|
65
|
+
: (0, transactionDetailReducer_1.markCategoryClassRecommendationsCompletedForTransactionDetail)(transactionId, lineIds));
|
|
66
|
+
}
|
|
67
|
+
const transactionWithCOT = isFetchCOT === true
|
|
68
|
+
? (0, transactionHelper_1.getTransactionWithCOTFromRecommendations)(transaction, recommendationWithCOTByLineId)
|
|
69
|
+
: (0, transactionHelper_1.getTransactionWithCOT)(transaction);
|
|
70
|
+
const uncategorizedIncomeExpense = (0, accountListSelector_1.getUncategorizedAccounts)(state$.value.accountState, state$.value.accountListState, 'accountList');
|
|
71
|
+
if (flowType === 'categorization' && selectedTab != null) {
|
|
72
|
+
return (0, rxjs_1.of)((0, transactionsViewReducer_1.setEntityRecommendationForLineIdsForCategorization)(selectedTab, transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled, (0, tenantSelector_1.getIsAccountingClassesEnabled)(state$.value)));
|
|
73
|
+
}
|
|
74
|
+
return (0, rxjs_1.of)((0, transactionDetailReducer_1.setEntityRecommendationForLineIdsForTransactionDetail)(transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense));
|
|
75
|
+
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
|
|
76
|
+
? (0, transactionsViewReducer_1.markCategoryClassRecommendationsFailureForCategorization)(selectedTab, transactionId, lineIds, (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'fetchEntityRecommendationsForLineUpdate failed: ' +
|
|
77
|
+
JSON.stringify(error)))
|
|
78
|
+
: (0, transactionDetailReducer_1.markCategoryClassRecommendationsFailureForTransactionDetail)(transactionId, lineIds, (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'fetchEntityRecommendationsForLineUpdate failed: ' +
|
|
79
|
+
JSON.stringify(error))))));
|
|
80
|
+
return (0, rxjs_1.concat)((0, rxjs_1.of)(startAction), httpStream$);
|
|
81
|
+
}))));
|
|
82
|
+
exports.fetchEntityRecommendationsForLineUpdateEpic = fetchEntityRecommendationsForLineUpdateEpic;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { RecommendationPayload, RecommendationPayloadWithCOT } from '../../commonPayloadTypes/recommendationPayload';
|
|
2
2
|
import { ID } from '../../commonStateTypes/common';
|
|
3
3
|
import { Recommendation, RecommendationWithCOT } from '../../commonStateTypes/recommendationBase';
|
|
4
|
+
import { Entity } from '../../entity/genericEntity/entity';
|
|
4
5
|
import { RecommendationByLineId, RecommendationWithCOTByLineId } from '../../entity/transaction/stateTypes/transaction';
|
|
6
|
+
import { SupportedTransaction } from '../../entity/transaction/transactionState';
|
|
5
7
|
export declare const getRecommendationFromRecommendationPayload: (payload: RecommendationPayload[]) => Recommendation[] | undefined;
|
|
6
8
|
export declare const getRecommendationByLineIdFromRecommendationPayload: (recommendationPayload: RecommendationPayload[], lineIds: ID[]) => RecommendationByLineId | undefined;
|
|
7
9
|
export declare const getRecommendationWithCOTFromRecommendationPayloadWithCOT: (payload: RecommendationPayloadWithCOT[]) => RecommendationWithCOT[] | undefined;
|
|
10
|
+
export declare const buildRecommendationUrl: (accountMicroServiceBaseUrl: string, entity: Entity, amount: number, transactionDetails: {
|
|
11
|
+
lineIds: ID[];
|
|
12
|
+
transaction: SupportedTransaction;
|
|
13
|
+
transactionId: {
|
|
14
|
+
id: string;
|
|
15
|
+
};
|
|
16
|
+
}, isFetchCOT: boolean) => string;
|
|
8
17
|
export declare const getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT: (recommendationPayload: RecommendationPayloadWithCOT[], lineIds: ID[]) => RecommendationWithCOTByLineId | undefined;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationByLineIdFromRecommendationPayload = exports.getRecommendationFromRecommendationPayload = void 0;
|
|
3
|
+
exports.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = exports.buildRecommendationUrl = exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationByLineIdFromRecommendationPayload = exports.getRecommendationFromRecommendationPayload = void 0;
|
|
4
4
|
const transactionPayload_1 = require("../../entity/transaction/payloadTypes/transactionPayload");
|
|
5
|
+
const transactionDetailState_1 = require("../transactionDetail/transactionDetailState");
|
|
5
6
|
const getRecommendationFromRecommendationPayload = (payload) => {
|
|
6
7
|
if (payload == null || payload.length === 0) {
|
|
7
8
|
return undefined;
|
|
@@ -43,6 +44,35 @@ const getRecommendationWithCOTFromRecommendationPayloadWithCOT = (payload) => {
|
|
|
43
44
|
return recommendations;
|
|
44
45
|
};
|
|
45
46
|
exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = getRecommendationWithCOTFromRecommendationPayloadWithCOT;
|
|
47
|
+
// Pluralized URL segment for entity types. Mirrors the legacy
|
|
48
|
+
// fetchEntityRecommendationsByTransactionIdEpic.toEntityTypeURL.
|
|
49
|
+
const toEntityTypeUrlSegment = (entityType) => `${entityType}s`;
|
|
50
|
+
// Build the recommendations URL for a single entity + transaction target.
|
|
51
|
+
// Centralizes the URL shape so the legacy
|
|
52
|
+
// fetchEntityRecommendationsByTransactionIdEpic and the new
|
|
53
|
+
// fetchEntityRecommendationsForLineUpdateEpic stay in lockstep.
|
|
54
|
+
const buildRecommendationUrl = (accountMicroServiceBaseUrl, entity, amount, transactionDetails, isFetchCOT) => {
|
|
55
|
+
const entityQuery = {
|
|
56
|
+
entity_name: entity.name,
|
|
57
|
+
entity_type: entity.type,
|
|
58
|
+
};
|
|
59
|
+
// Entity-id-null case: server expects entity_name / entity_type query
|
|
60
|
+
// instead of the {entityType}/{qboId}/recommendations path.
|
|
61
|
+
if (entity.qboId === entity.name) {
|
|
62
|
+
return `${accountMicroServiceBaseUrl}/1.0/recommendations?query=${encodeURIComponent(JSON.stringify(entityQuery))}`;
|
|
63
|
+
}
|
|
64
|
+
const query = {
|
|
65
|
+
amount,
|
|
66
|
+
transaction_id: (0, transactionDetailState_1.getActualTransactionID)(transactionDetails.transactionId.id),
|
|
67
|
+
transaction_type: transactionDetails.transaction.type,
|
|
68
|
+
line_ids: transactionDetails.lineIds,
|
|
69
|
+
};
|
|
70
|
+
if (isFetchCOT === true) {
|
|
71
|
+
query.include_cot = true;
|
|
72
|
+
}
|
|
73
|
+
return `${accountMicroServiceBaseUrl}/1.0/${toEntityTypeUrlSegment(entity.type)}/${entity.qboId}/recommendations?query=${encodeURIComponent(JSON.stringify(query))}`;
|
|
74
|
+
};
|
|
75
|
+
exports.buildRecommendationUrl = buildRecommendationUrl;
|
|
46
76
|
const getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = (recommendationPayload, lineIds) => {
|
|
47
77
|
if (recommendationPayload.length === 0) {
|
|
48
78
|
return undefined;
|