@zeniai/client-epic-state 5.0.52 → 5.0.53-betaML3
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/commonStateTypes/recommendationBase.d.ts +2 -1
- package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -2
- package/lib/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
- package/lib/entity/account/accountState.d.ts +1 -1
- package/lib/entity/accountGroup/accountGroupState.d.ts +1 -1
- package/lib/entity/class/classReducer.d.ts +4 -4
- package/lib/entity/class/classState.d.ts +1 -1
- package/lib/entity/forecast/forecastState.d.ts +1 -1
- package/lib/entity/project/projectPayload.d.ts +8 -1
- package/lib/entity/project/projectPayload.js +15 -1
- package/lib/entity/project/projectState.d.ts +4 -2
- package/lib/entity/sectionAccountsView/sectionAccountsView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesViewReducer.d.ts +2 -2
- package/lib/entity/sectionProjectView/sectionProjectView.d.ts +1 -1
- package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +2 -2
- package/lib/entity/tenant/clearAllEpic.d.ts +2 -1
- package/lib/entity/tenant/clearAllEpic.js +2 -0
- package/lib/entity/transaction/payloadTypes/transactionLinePayload.d.ts +2 -0
- package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.d.ts +2 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.js +6 -0
- package/lib/entity/transaction/stateTypes/transaction.d.ts +2 -0
- package/lib/entity/transaction/stateTypes/transactionLine.d.ts +2 -0
- package/lib/epic.d.ts +4 -1
- package/lib/epic.js +5 -1
- package/lib/esm/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
- package/lib/esm/entity/project/projectPayload.js +12 -0
- package/lib/esm/entity/tenant/clearAllEpic.js +2 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionPayload.js +6 -0
- package/lib/esm/epic.js +5 -1
- package/lib/esm/index.js +11 -7
- package/lib/esm/reducer.js +3 -0
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
- package/lib/esm/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
- package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +100 -31
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +13 -2
- package/lib/esm/view/expenseAutomationView/types/transactionsViewState.js +1 -0
- package/lib/esm/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +44 -0
- package/lib/esm/view/projectList/fetchProjectListEpic.js +38 -0
- package/lib/esm/view/projectList/projectListPayload.js +1 -0
- package/lib/esm/view/projectList/projectListReducer.js +39 -0
- package/lib/esm/view/projectList/projectListSelector.js +9 -0
- package/lib/esm/view/projectList/projectListState.js +1 -0
- package/lib/esm/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +83 -0
- package/lib/esm/view/recommendation/recommendationReducer.js +52 -1
- package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
- package/lib/esm/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
- package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
- package/lib/esm/view/transactionDetail/transactionDetailSelector.js +14 -2
- package/lib/esm/view/transactionList/fetchTransactionListByProjectEpic.js +45 -0
- package/lib/esm/view/transactionList/parallelFetchProjectTransactionListEpic.js +32 -0
- package/lib/esm/view/transactionList/transactionListByProjectSelector.js +58 -0
- package/lib/esm/view/transactionList/transactionListReducer.js +83 -2
- package/lib/esm/view/transactionList/transactionListState.js +10 -0
- package/lib/index.d.ts +13 -9
- package/lib/index.js +42 -27
- package/lib/reducer.d.ts +3 -0
- package/lib/reducer.js +3 -0
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
- package/lib/view/auditReportView/auditReportViewState.d.ts +1 -1
- package/lib/view/billPayCard/billPayCardSelector.d.ts +1 -1
- package/lib/view/cardBalance/cardBalanceSelector.d.ts +1 -1
- package/lib/view/cashBalance/cashBalanceSelector.d.ts +1 -1
- package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +1 -1
- package/lib/view/cashPosition/cashPositionSelector.d.ts +1 -1
- package/lib/view/dashboard/dashboardReducer.d.ts +2 -2
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.d.ts +2 -1
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
- package/lib/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
- package/lib/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +2 -1
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +3 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +100 -31
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +4 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +12 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +5 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.js +1 -0
- package/lib/view/financeStatement/financeStatementReducer.d.ts +1 -1
- package/lib/view/globalMerchantView/globalMerchantViewReducer.d.ts +2 -2
- package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.d.ts +15 -1
- package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +45 -0
- package/lib/view/projectList/fetchProjectListEpic.d.ts +8 -0
- package/lib/view/projectList/fetchProjectListEpic.js +42 -0
- package/lib/view/projectList/projectListPayload.d.ts +7 -0
- package/lib/view/projectList/projectListPayload.js +2 -0
- package/lib/view/projectList/projectListReducer.d.ts +9 -0
- package/lib/view/projectList/projectListReducer.js +43 -0
- package/lib/view/projectList/projectListSelector.d.ts +8 -0
- package/lib/view/projectList/projectListSelector.js +13 -0
- package/lib/view/projectList/projectListState.d.ts +4 -0
- package/lib/view/projectList/projectListState.js +2 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.d.ts +8 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +87 -0
- package/lib/view/recommendation/recommendationReducer.d.ts +21 -1
- package/lib/view/recommendation/recommendationReducer.js +53 -2
- package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +1 -1
- package/lib/view/reportsResync/reportsResyncReducer.d.ts +2 -2
- package/lib/view/tasksCard/tasksCardSelector.d.ts +1 -1
- package/lib/view/topEx/topExSelector.d.ts +1 -1
- package/lib/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
- package/lib/view/transactionDetail/epics/transactionDetailEpic.d.ts +2 -1
- package/lib/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
- package/lib/view/transactionDetail/transactionDetailSelector.d.ts +3 -0
- package/lib/view/transactionDetail/transactionDetailSelector.js +13 -1
- package/lib/view/transactionDetail/transactionDetailState.d.ts +2 -0
- package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
- package/lib/view/transactionList/fetchTransactionListByProjectEpic.d.ts +8 -0
- package/lib/view/transactionList/fetchTransactionListByProjectEpic.js +52 -0
- package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.d.ts +6 -0
- package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.js +36 -0
- package/lib/view/transactionList/transactionListByProjectSelector.d.ts +22 -0
- package/lib/view/transactionList/transactionListByProjectSelector.js +66 -0
- package/lib/view/transactionList/transactionListPayload.d.ts +15 -0
- package/lib/view/transactionList/transactionListReducer.d.ts +25 -2
- package/lib/view/transactionList/transactionListReducer.js +83 -2
- package/lib/view/transactionList/transactionListState.d.ts +15 -0
- package/lib/view/transactionList/transactionListState.js +13 -1
- package/package.json +1 -1
package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js
CHANGED
|
@@ -183,6 +183,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
|
|
|
183
183
|
const lineId = line.id;
|
|
184
184
|
record.account = accountLine.account;
|
|
185
185
|
record.class = accountLine.class;
|
|
186
|
+
record.project = accountLine.project;
|
|
186
187
|
record.vendor =
|
|
187
188
|
accountLine.vendor != null
|
|
188
189
|
? {
|
|
@@ -305,7 +306,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
|
|
|
305
306
|
});
|
|
306
307
|
return lineItemData;
|
|
307
308
|
};
|
|
308
|
-
export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
|
|
309
|
+
export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
|
|
309
310
|
let newLineItems = {};
|
|
310
311
|
let selectedCheckBoxTransactionIds = draft.selectedCheckBoxTransactionIds;
|
|
311
312
|
let similarTransactionUpdated = false;
|
|
@@ -325,7 +326,7 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
325
326
|
const record = {
|
|
326
327
|
...transactionDetailLocalData.lineItemById[lineId],
|
|
327
328
|
};
|
|
328
|
-
// if the line is the one that is being updated, then set the account and
|
|
329
|
+
// if the line is the one that is being updated, then set the account, class, and project
|
|
329
330
|
if (transactionId === record.transactionId &&
|
|
330
331
|
transactionLineId === lineId) {
|
|
331
332
|
if (selectedAccount != null) {
|
|
@@ -334,6 +335,9 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
334
335
|
if (selectedClassBase != null) {
|
|
335
336
|
record.class = selectedClassBase;
|
|
336
337
|
}
|
|
338
|
+
if (selectedProjectBase != null) {
|
|
339
|
+
record.project = selectedProjectBase;
|
|
340
|
+
}
|
|
337
341
|
const isClassSatisfied = isAccountingClassesEnabled === false || record.class != null;
|
|
338
342
|
if (isClassSatisfied && record.account != null) {
|
|
339
343
|
if (selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
|
|
@@ -385,6 +389,10 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
385
389
|
selectedCheckBoxTransactionIds.push(transaction.id);
|
|
386
390
|
}
|
|
387
391
|
}
|
|
392
|
+
// Project intentionally does NOT propagate to other rows of
|
|
393
|
+
// the same vendor: each line can carry a different project,
|
|
394
|
+
// and auto-filling siblings (the way class does) leads to
|
|
395
|
+
// unexpected bulk selection.
|
|
388
396
|
}
|
|
389
397
|
}
|
|
390
398
|
if (line.type === 'transaction_with_product_or_service_line') {
|
|
@@ -514,6 +522,7 @@ export const setEntityRecommendationForLineIdInLocalData = (draft, transaction,
|
|
|
514
522
|
...newLineItem,
|
|
515
523
|
class: matchingRecord.class,
|
|
516
524
|
account: matchingRecord.account,
|
|
525
|
+
project: matchingRecord.project,
|
|
517
526
|
lineEntity: matchingRecord.lineEntity,
|
|
518
527
|
};
|
|
519
528
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import recordGet from 'lodash/get';
|
|
3
3
|
import uniq from 'lodash/uniq';
|
|
4
|
+
import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, } from '../../recommendation/recommendationReducer';
|
|
4
5
|
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
5
6
|
import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
|
|
6
7
|
import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
|
|
@@ -282,7 +283,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
282
283
|
},
|
|
283
284
|
setAllItemsToCategoryClassInLocalDataForCategorization: {
|
|
284
285
|
reducer(draft, action) {
|
|
285
|
-
const { vendorId, customerId, selectedAccount, selectedClassBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
286
|
+
const { vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
286
287
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
287
288
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
288
289
|
const existingAccount = transactionLocalData?.transactionReviewLocalData.lineItemById[lineId]
|
|
@@ -296,6 +297,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
296
297
|
...transactionLocalData.transactionReviewLocalData.lineItemById[lineId],
|
|
297
298
|
account: selectedAccount,
|
|
298
299
|
class: selectedClassBase,
|
|
300
|
+
project: selectedProjectBase,
|
|
299
301
|
},
|
|
300
302
|
},
|
|
301
303
|
};
|
|
@@ -324,9 +326,16 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
324
326
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
325
327
|
.transactionReviewLocalDataById, transaction.id, undefined);
|
|
326
328
|
if (transactionLocalData != null) {
|
|
327
|
-
const
|
|
329
|
+
const existingClassId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.class?.classId;
|
|
330
|
+
const existingProjectId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.project?.projectId;
|
|
331
|
+
const updatedItem = existingAccount?.accountId !== selectedAccount?.accountId
|
|
328
332
|
? 'category'
|
|
329
|
-
:
|
|
333
|
+
: existingClassId !== selectedClassBase?.classId
|
|
334
|
+
? 'class'
|
|
335
|
+
: existingProjectId !== selectedProjectBase?.projectId
|
|
336
|
+
? 'project'
|
|
337
|
+
: 'class';
|
|
338
|
+
const { localData, similarTransactionUpdated } = toSetAllLineItemsToCategoryClass(draft.transactionCategorizationView[selectedTab], transaction, transactionId, lineId, transactionLocalData.transactionReviewLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
|
|
330
339
|
draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
|
|
331
340
|
transactionId: transaction.id,
|
|
332
341
|
transactionReviewLocalData: localData,
|
|
@@ -345,7 +354,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
345
354
|
...dirtyIds,
|
|
346
355
|
]);
|
|
347
356
|
},
|
|
348
|
-
prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) {
|
|
357
|
+
prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled, selectedProjectBase) {
|
|
349
358
|
return {
|
|
350
359
|
payload: {
|
|
351
360
|
selectedTab,
|
|
@@ -357,6 +366,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
357
366
|
lineEntity,
|
|
358
367
|
selectedAccount,
|
|
359
368
|
selectedClassBase,
|
|
369
|
+
selectedProjectBase,
|
|
360
370
|
recommendations,
|
|
361
371
|
uncategorizedAccounts,
|
|
362
372
|
isAccountingClassesEnabled,
|
|
@@ -387,9 +397,25 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
387
397
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
388
398
|
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
389
399
|
if (transactionLocalData != null) {
|
|
400
|
+
const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
|
|
401
|
+
// Flip the per-line "fetching" flag to Completed for the lines this
|
|
402
|
+
// fetch covered. Reads as the success arm of the
|
|
403
|
+
// fetchEntityRecommendationsForCategorization → epic → reducer cycle.
|
|
404
|
+
lineIds.forEach((lineId) => {
|
|
405
|
+
const lineItem = newLocalData.lineItemById[lineId];
|
|
406
|
+
if (lineItem != null) {
|
|
407
|
+
newLocalData.lineItemById[lineId] = {
|
|
408
|
+
...lineItem,
|
|
409
|
+
categoryClassRecommendationsFetchState: {
|
|
410
|
+
fetchState: 'Completed',
|
|
411
|
+
error: undefined,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
});
|
|
390
416
|
draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
|
|
391
417
|
transactionId: transaction.id,
|
|
392
|
-
transactionReviewLocalData:
|
|
418
|
+
transactionReviewLocalData: newLocalData,
|
|
393
419
|
};
|
|
394
420
|
// Mark transaction as dirty (autofill happened)
|
|
395
421
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
|
|
@@ -444,7 +470,13 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
444
470
|
action.payload.status;
|
|
445
471
|
},
|
|
446
472
|
updateSelectedCustomerForTransaction(draft, action) {
|
|
447
|
-
const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
|
|
473
|
+
const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
|
|
474
|
+
// uncategorizedAccounts, isAccountingClassesEnabled, and
|
|
475
|
+
// isUncategorizedExpenseCategoryEnabled remain in the payload type for
|
|
476
|
+
// backward compatibility but are no longer read here — the post-fetch
|
|
477
|
+
// success reducer (setEntityRecommendationForLineIdsForCategorization)
|
|
478
|
+
// owns the all-fields-filled check that drives auto-selection.
|
|
479
|
+
} = action.payload;
|
|
448
480
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
449
481
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
450
482
|
if (transactionLocalData != null) {
|
|
@@ -493,18 +525,13 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
493
525
|
transactionReviewLocalData: newTransactionLocalData,
|
|
494
526
|
transactionId,
|
|
495
527
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
504
|
-
...selectedCheckBoxTransactionIds,
|
|
505
|
-
transactionId,
|
|
506
|
-
];
|
|
507
|
-
}
|
|
528
|
+
// Auto-selection of the row checkbox is intentionally deferred to
|
|
529
|
+
// setEntityRecommendationForLineIdsForCategorization (success path of
|
|
530
|
+
// the recommendations fetch). On a payee change the existing
|
|
531
|
+
// category/class are stale, so auto-selecting against them would
|
|
532
|
+
// either trigger save flows on the wrong data or rely on a later
|
|
533
|
+
// recommendations write to fix it. Letting the success reducer do the
|
|
534
|
+
// auto-add keeps "checkbox flips on" aligned with "fresh recs are in".
|
|
508
535
|
// Mark transaction as dirty (customer updated)
|
|
509
536
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
|
|
510
537
|
draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
|
|
@@ -516,7 +543,11 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
516
543
|
}
|
|
517
544
|
},
|
|
518
545
|
updateSelectedVendorForTransaction(draft, action) {
|
|
519
|
-
const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
|
|
546
|
+
const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
|
|
547
|
+
// See updateSelectedCustomerForTransaction for why
|
|
548
|
+
// uncategorizedAccounts, isAccountingClassesEnabled, and
|
|
549
|
+
// isUncategorizedExpenseCategoryEnabled are no longer destructured.
|
|
550
|
+
} = action.payload;
|
|
520
551
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
521
552
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
522
553
|
if (transactionLocalData != null) {
|
|
@@ -569,18 +600,8 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
569
600
|
},
|
|
570
601
|
transactionId,
|
|
571
602
|
};
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (newTransactionLocalData.vendor != null &&
|
|
575
|
-
newTransactionLocalData.vendor.id != null &&
|
|
576
|
-
checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
|
|
577
|
-
selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
|
|
578
|
-
selectedCheckBoxTransactionIds.includes(transactionId) === false) {
|
|
579
|
-
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
580
|
-
...selectedCheckBoxTransactionIds,
|
|
581
|
-
transactionId,
|
|
582
|
-
];
|
|
583
|
-
}
|
|
603
|
+
// See updateSelectedCustomerForTransaction above for why auto-select
|
|
604
|
+
// is deferred to setEntityRecommendationForLineIdsForCategorization.
|
|
584
605
|
// Mark transaction as dirty (vendor updated)
|
|
585
606
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
|
|
586
607
|
draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
|
|
@@ -793,6 +814,54 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
793
814
|
},
|
|
794
815
|
},
|
|
795
816
|
},
|
|
817
|
+
extraReducers: (builder) => {
|
|
818
|
+
// Start arm of the categorization-only recommendations fetch. Marks every
|
|
819
|
+
// affected line's per-line flag as In-Progress. The epic uses
|
|
820
|
+
// groupBy(transactionId+lineIds) + switchMap, so concurrent fetches for
|
|
821
|
+
// distinct (transaction, lineIds-set) targets do not cancel each other —
|
|
822
|
+
// there's no need for a global cleanup sweep here.
|
|
823
|
+
builder.addCase(fetchEntityRecommendationsForCategorization, (draft, action) => {
|
|
824
|
+
const { selectedTab, transactionDetails } = action.payload;
|
|
825
|
+
const { transactionId, lineIds } = transactionDetails;
|
|
826
|
+
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
827
|
+
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
828
|
+
if (transactionLocalData != null) {
|
|
829
|
+
lineIds.forEach((lineId) => {
|
|
830
|
+
const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
|
|
831
|
+
if (lineItem != null) {
|
|
832
|
+
transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
|
|
833
|
+
...lineItem,
|
|
834
|
+
categoryClassRecommendationsFetchState: {
|
|
835
|
+
fetchState: 'In-Progress',
|
|
836
|
+
error: undefined,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
// Failure arm. Flips the affected lines' per-line flag to Error so the
|
|
844
|
+
// skeleton resolves and the queued checkbox re-evaluation drops the entry.
|
|
845
|
+
builder.addCase(setCategoryClassRecommendationsForCategorizationFailure, (draft, action) => {
|
|
846
|
+
const { selectedTab, transactionId, lineIds, error } = action.payload;
|
|
847
|
+
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
848
|
+
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
849
|
+
if (transactionLocalData != null) {
|
|
850
|
+
lineIds.forEach((lineId) => {
|
|
851
|
+
const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
|
|
852
|
+
if (lineItem != null) {
|
|
853
|
+
transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
|
|
854
|
+
...lineItem,
|
|
855
|
+
categoryClassRecommendationsFetchState: {
|
|
856
|
+
fetchState: 'Error',
|
|
857
|
+
error,
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
},
|
|
796
865
|
});
|
|
797
866
|
export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
|
|
798
867
|
export default expenseAutomationTransactionsView.reducer;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import omit from 'lodash/omit';
|
|
2
2
|
import { reduceAllFetchState, reduceAnyFetchState, } from '../../../commonStateTypes/reduceFetchState';
|
|
3
3
|
import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
|
|
4
|
-
import { getIsAccountingClassesEnabled,
|
|
4
|
+
import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
|
|
5
5
|
import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
|
|
6
6
|
import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
|
|
7
7
|
import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
|
|
8
8
|
import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
|
|
9
|
+
import { getProjectList } from '../../projectList/projectListSelector';
|
|
9
10
|
import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
|
|
10
11
|
export function getExpenseAutomationTransactionView(state) {
|
|
11
|
-
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
12
|
+
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
12
13
|
const { selectedTransactionCategorizationTab } = expenseAutomationTransactionsViewState;
|
|
13
14
|
const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
|
|
14
15
|
const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
|
|
@@ -17,6 +18,8 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
17
18
|
const allClasses = classList.classes.map((classData) => {
|
|
18
19
|
return omit(classData, ['accountIds', 'key']);
|
|
19
20
|
});
|
|
21
|
+
const projectList = getProjectList(projectState, projectListState);
|
|
22
|
+
const isAccountingProjectsEnabled = getIsAccountingProjectsEnabled(state);
|
|
20
23
|
const accountsHierarchyList = getNestedAccountListHierarchy(accountListState, 'accountList');
|
|
21
24
|
const classHierarchyList = getNestedClassListHierarchy(classListState);
|
|
22
25
|
const currentTenant = getCurrentTenant(state);
|
|
@@ -39,6 +42,11 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
39
42
|
return null;
|
|
40
43
|
})
|
|
41
44
|
.filter((transaction) => transaction != null);
|
|
45
|
+
// True if any visible line is currently fetching category/class
|
|
46
|
+
// recommendations because the user changed the payee. Used to disable Save
|
|
47
|
+
// and to defer manual checkbox selection.
|
|
48
|
+
const hasInFlightCategoryClassRecommendations = transactionLocalData.some((transaction) => Object.values(transaction.transactionReviewLocalData.lineItemById).some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
|
|
49
|
+
'In-Progress'));
|
|
42
50
|
const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
|
|
43
51
|
const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
|
|
44
52
|
const monthEndFetchState = monthYearPeriodId != null
|
|
@@ -96,6 +104,8 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
96
104
|
classList: allClasses,
|
|
97
105
|
accountsHierarchyList,
|
|
98
106
|
classHierarchyList,
|
|
107
|
+
isAccountingProjectsEnabled,
|
|
108
|
+
projectList,
|
|
99
109
|
transactionLocalData,
|
|
100
110
|
uiState,
|
|
101
111
|
refreshStatus,
|
|
@@ -107,5 +117,6 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
107
117
|
uploadReceiptStatusById,
|
|
108
118
|
selectedTransactionId,
|
|
109
119
|
selectedTransactionLineId,
|
|
120
|
+
hasInFlightCategoryClassRecommendations,
|
|
110
121
|
};
|
|
111
122
|
}
|
|
@@ -115,6 +115,50 @@ function buildHorizontalClassColumnBalancesSlice(report, classAmounts, totalAmou
|
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Sum balances across descendant accounts (recursive).
|
|
120
|
+
*
|
|
121
|
+
* Parent rows in the horizontal pivot only carry their own direct postings
|
|
122
|
+
* (zero for "container" accounts), so callers rendering "Total {parent}" rows
|
|
123
|
+
* must aggregate from descendants to get the real per-class totals.
|
|
124
|
+
*
|
|
125
|
+
* Returns one `COABalance` per class column + total (same shape as the
|
|
126
|
+
* enriched `balancesInTimeframe.balances`), with `amount` summed and `type`
|
|
127
|
+
* set to `default_with_null_balance` for zeros. Returns `[]` when no
|
|
128
|
+
* descendants have enriched data.
|
|
129
|
+
*/
|
|
130
|
+
export function aggregateHorizontalDescendantBalances(accounts, childIndices, childrenByParent) {
|
|
131
|
+
let template;
|
|
132
|
+
const sums = new Map();
|
|
133
|
+
const walk = (idx) => {
|
|
134
|
+
const childRow = accounts[idx];
|
|
135
|
+
const childBalances = childRow?.accountReportData?.balancesInTimeframe.balances;
|
|
136
|
+
if (childBalances != null) {
|
|
137
|
+
if (template == null) {
|
|
138
|
+
template = childBalances;
|
|
139
|
+
}
|
|
140
|
+
childBalances.forEach((b, i) => {
|
|
141
|
+
sums.set(i, (sums.get(i) ?? 0) + (b.balance.amount ?? 0));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Always recurse — a missing intermediate node should not prune its
|
|
145
|
+
// descendants from the aggregate.
|
|
146
|
+
const grandChildren = childrenByParent[idx] ?? [];
|
|
147
|
+
grandChildren.forEach(walk);
|
|
148
|
+
};
|
|
149
|
+
childIndices.forEach(walk);
|
|
150
|
+
if (template == null) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return template.map((tpl, i) => {
|
|
154
|
+
const amount = sums.get(i) ?? 0;
|
|
155
|
+
return {
|
|
156
|
+
...tpl,
|
|
157
|
+
balance: { ...tpl.balance, amount },
|
|
158
|
+
type: amount === 0 ? 'default_with_null_balance' : 'default',
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
118
162
|
/**
|
|
119
163
|
* Parent index per DFS row — O(n). Matches the scan in the former
|
|
120
164
|
* `findHorizontalAccountParentIndex` (depth <= 2 rows have no parent).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap, switchMap, withLatestFrom, } from 'rxjs/operators';
|
|
3
|
+
import { updateProjects } from '../../entity/project/projectReducer';
|
|
4
|
+
import { getIsAccountingProjectsEnabled } from '../../entity/tenant/tenantSelector';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
|
|
6
|
+
import { fetchProjectList, updateProjectList, updateProjectListFailure, } from './projectListReducer';
|
|
7
|
+
const buildProjectListQuery = (isIncludeDeleted) => {
|
|
8
|
+
const query = {
|
|
9
|
+
is_deleted: isIncludeDeleted,
|
|
10
|
+
sort_by: 'project_name',
|
|
11
|
+
sort_order: 'asc',
|
|
12
|
+
is_full: true,
|
|
13
|
+
};
|
|
14
|
+
return `?query=${encodeURIComponent(JSON.stringify(query))}`;
|
|
15
|
+
};
|
|
16
|
+
export const fetchProjectListEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchProjectList.match), withLatestFrom(state$), switchMap(([action, state]) => {
|
|
17
|
+
if (!getIsAccountingProjectsEnabled(state)) {
|
|
18
|
+
return of(updateProjectList({
|
|
19
|
+
status: createZeniAPIStatus('Success', ''),
|
|
20
|
+
data: { projects: [] },
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
const { isIncludeDeleted } = action.payload;
|
|
24
|
+
const queryParams = buildProjectListQuery(isIncludeDeleted);
|
|
25
|
+
return zeniAPI
|
|
26
|
+
.getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/projects${queryParams}`)
|
|
27
|
+
.pipe(mergeMap((response) => {
|
|
28
|
+
if (isSuccessResponse(response)) {
|
|
29
|
+
return of(updateProjects(response.data?.projects ?? []), updateProjectList(response));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return of(updateProjectListFailure(response.status));
|
|
33
|
+
}
|
|
34
|
+
}), catchError((error) => {
|
|
35
|
+
return of(updateProjectListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch Projects REST API call errored out' +
|
|
36
|
+
JSON.stringify(error))));
|
|
37
|
+
}));
|
|
38
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
export const initialState = {
|
|
3
|
+
fetchState: 'Not-Started',
|
|
4
|
+
error: undefined,
|
|
5
|
+
projectIds: [],
|
|
6
|
+
hasValidState() {
|
|
7
|
+
return this.fetchState == 'Completed';
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const projectList = createSlice({
|
|
11
|
+
name: 'projectList',
|
|
12
|
+
initialState,
|
|
13
|
+
reducers: {
|
|
14
|
+
fetchProjectList: {
|
|
15
|
+
reducer(draft) {
|
|
16
|
+
draft.fetchState = 'In-Progress';
|
|
17
|
+
draft.error = undefined;
|
|
18
|
+
},
|
|
19
|
+
prepare(isIncludeDeleted) {
|
|
20
|
+
return { payload: { isIncludeDeleted: isIncludeDeleted ?? false } };
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
updateProjectList(draft, action) {
|
|
24
|
+
draft.projectIds =
|
|
25
|
+
action.payload.data?.projects?.map((project) => project.zeni_project_id) ?? [];
|
|
26
|
+
draft.fetchState = 'Completed';
|
|
27
|
+
draft.error = undefined;
|
|
28
|
+
},
|
|
29
|
+
updateProjectListFailure(draft, action) {
|
|
30
|
+
draft.fetchState = 'Error';
|
|
31
|
+
draft.error = action.payload;
|
|
32
|
+
},
|
|
33
|
+
clearProjectList(draft) {
|
|
34
|
+
Object.assign(draft, initialState);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const { fetchProjectList, updateProjectList, updateProjectListFailure, clearProjectList, } = projectList.actions;
|
|
39
|
+
export default projectList.reducer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getProjectsByIds } from '../../entity/project/projectSelector';
|
|
2
|
+
export const getProjectList = (projectState, projectListState) => {
|
|
3
|
+
return {
|
|
4
|
+
projects: getProjectsByIds(projectState, projectListState.projectIds),
|
|
5
|
+
fetchState: projectListState.fetchState,
|
|
6
|
+
error: projectListState.error,
|
|
7
|
+
version: 0,
|
|
8
|
+
};
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { from, of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, groupBy, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
|
|
4
|
+
import { getTransactionWithCOT, getTransactionWithCOTFromRecommendations, } from '../../entity/transaction/transactionHelper';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
|
|
6
|
+
import { setEntityRecommendationForLineIdsForCategorization } from '../../view/expenseAutomationView/reducers/transactionsViewReducer';
|
|
7
|
+
import { getUncategorizedAccounts } from '../accountList/accountListSelector';
|
|
8
|
+
import { getActualTransactionID } from '../transactionDetail/transactionDetailState';
|
|
9
|
+
import { getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT, getRecommendationFromRecommendationPayload, } from './recommendationHelper';
|
|
10
|
+
import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, updateRecommendationForEntity, } from './recommendationReducer';
|
|
11
|
+
// Categorization-list-only recommendations fetch.
|
|
12
|
+
//
|
|
13
|
+
// Concurrency model: groupBy on (transactionId.id, sorted lineIds) → switchMap
|
|
14
|
+
// inside each group → mergeMap to flatten groups. This means:
|
|
15
|
+
// • Same target (same transaction + same lineIds-set) — newer fetch cancels
|
|
16
|
+
// the prior in-flight fetch. Avoids the same-line stale-response race.
|
|
17
|
+
// • Different targets — run fully in parallel. Avoids the silent
|
|
18
|
+
// "stuck spinner" cancellation that the global-switchMap epic suffers
|
|
19
|
+
// from when the user moves between rows or JE lines mid-flight.
|
|
20
|
+
//
|
|
21
|
+
// The transactionsViewReducer's per-line categoryClassRecommendationsFetchState
|
|
22
|
+
// flag is set to In-Progress when the action enters this epic (start arm in
|
|
23
|
+
// extraReducers), Completed via setEntityRecommendationForLineIdsForCategorization,
|
|
24
|
+
// or Error via setCategoryClassRecommendationsForCategorizationFailure.
|
|
25
|
+
export const fetchEntityRecommendationsForCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchEntityRecommendationsForCategorization.match), groupBy((action) => {
|
|
26
|
+
const { transactionId, lineIds } = action.payload.transactionDetails;
|
|
27
|
+
return `${transactionId.id}|${[...lineIds].sort().join('|')}`;
|
|
28
|
+
}), mergeMap((group$) => group$.pipe(switchMap((action) => {
|
|
29
|
+
const { entity, amount, transactionDetails, isFetchCOT, selectedTab, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
30
|
+
const query = {
|
|
31
|
+
amount,
|
|
32
|
+
transaction_id: getActualTransactionID(transactionDetails.transactionId.id),
|
|
33
|
+
transaction_type: transactionDetails.transaction.type,
|
|
34
|
+
line_ids: transactionDetails.lineIds,
|
|
35
|
+
};
|
|
36
|
+
if (isFetchCOT === true) {
|
|
37
|
+
query.include_cot = true;
|
|
38
|
+
}
|
|
39
|
+
const entityQuery = {
|
|
40
|
+
entity_name: entity.name,
|
|
41
|
+
entity_type: entity.type,
|
|
42
|
+
};
|
|
43
|
+
const recommendationUrl = entity.qboId === entity.name // entity id null case
|
|
44
|
+
? `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/recommendations?query=${encodeURIComponent(JSON.stringify(entityQuery))}`
|
|
45
|
+
: `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/${toEntityTypeURL(entity.type)}/${entity.qboId}/recommendations?query=${encodeURIComponent(JSON.stringify(query))}`;
|
|
46
|
+
return zeniAPI
|
|
47
|
+
.getJSON(recommendationUrl)
|
|
48
|
+
.pipe(mergeMap((response) => {
|
|
49
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
50
|
+
const actions = [];
|
|
51
|
+
const { transactionId, transaction, lineIds } = transactionDetails;
|
|
52
|
+
const recommendationWithCOTByLineId = getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT(response.data.recommendations, lineIds);
|
|
53
|
+
const recommendations = getRecommendationFromRecommendationPayload(response.data.recommendations);
|
|
54
|
+
const transactionWithCOT = isFetchCOT === true &&
|
|
55
|
+
recommendationWithCOTByLineId != null
|
|
56
|
+
? getTransactionWithCOTFromRecommendations(transaction, recommendationWithCOTByLineId)
|
|
57
|
+
: getTransactionWithCOT(transaction);
|
|
58
|
+
actions.push(updateRecommendationForEntity({
|
|
59
|
+
entityId: entity.id,
|
|
60
|
+
entityType: entity.type,
|
|
61
|
+
recommendations,
|
|
62
|
+
}));
|
|
63
|
+
if (recommendationWithCOTByLineId != null) {
|
|
64
|
+
const uncategorizedIncomeExpense = getUncategorizedAccounts(state$.value.accountState, state$.value.accountListState, 'accountList');
|
|
65
|
+
actions.push(setEntityRecommendationForLineIdsForCategorization(selectedTab, transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled, getIsAccountingClassesEnabled(state$.value)));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Successful HTTP but no per-line recommendations — still
|
|
69
|
+
// need to clear the In-Progress flag, otherwise the
|
|
70
|
+
// skeleton sticks. Treat as a "no recommendations" success
|
|
71
|
+
// by firing the failure action with a benign error so the
|
|
72
|
+
// line flag flips out of In-Progress.
|
|
73
|
+
actions.push(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionId, lineIds, createZeniAPIStatus('Unexpected Error', 'Recommendations response missing per-line data')));
|
|
74
|
+
}
|
|
75
|
+
return from(actions);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, response.status));
|
|
79
|
+
}
|
|
80
|
+
}), catchError((error) => of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, createZeniAPIStatus('Unexpected Error', 'Fetch recommendations for categorization REST API call errored out' +
|
|
81
|
+
JSON.stringify(error))))));
|
|
82
|
+
}))));
|
|
83
|
+
const toEntityTypeURL = (entityType) => `${entityType}s`;
|
|
@@ -106,12 +106,63 @@ const recommendation = createSlice({
|
|
|
106
106
|
};
|
|
107
107
|
},
|
|
108
108
|
},
|
|
109
|
+
// Categorization-list-only fetch action. Mirrors the
|
|
110
|
+
// fetchEntityRecommendationsByTransactionId payload but is observed by a
|
|
111
|
+
// separate epic (with groupBy + switchMap, keyed on
|
|
112
|
+
// transactionId+lineIds-set) so concurrent fetches for distinct targets do
|
|
113
|
+
// not cancel each other. Reducer hooks in transactionsViewReducer use this
|
|
114
|
+
// to drive the per-line categoryClassRecommendationsFetchState flag.
|
|
115
|
+
fetchEntityRecommendationsForCategorization: {
|
|
116
|
+
reducer(draft, action) {
|
|
117
|
+
const { entity } = action.payload;
|
|
118
|
+
const key = getEntityRecommendationKey(entity.type, entity.id);
|
|
119
|
+
const existingRecommendation = draft.recommendationByEntity[key];
|
|
120
|
+
if (existingRecommendation == null) {
|
|
121
|
+
draft.recommendationByEntity[key] = {
|
|
122
|
+
...initialEntityRecommendationState,
|
|
123
|
+
fetchState: 'In-Progress',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
prepare(entity, amount, transactionDetails, selectedTab, isFetchCOT = false, isUncategorizedExpenseCategoryEnabled) {
|
|
128
|
+
return {
|
|
129
|
+
payload: {
|
|
130
|
+
entity,
|
|
131
|
+
amount,
|
|
132
|
+
transactionDetails,
|
|
133
|
+
selectedTab,
|
|
134
|
+
isFetchCOT,
|
|
135
|
+
isUncategorizedExpenseCategoryEnabled,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
setCategoryClassRecommendationsForCategorizationFailure: {
|
|
141
|
+
reducer(
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
143
|
+
_draft,
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
145
|
+
_action) {
|
|
146
|
+
// The transactionsViewReducer handles the per-line flag flip; this
|
|
147
|
+
// reducer slice has no recommendation cache to update for the failure.
|
|
148
|
+
},
|
|
149
|
+
prepare(selectedTab, transactionId, lineIds, error) {
|
|
150
|
+
return {
|
|
151
|
+
payload: {
|
|
152
|
+
selectedTab,
|
|
153
|
+
transactionId,
|
|
154
|
+
lineIds,
|
|
155
|
+
error,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
},
|
|
109
160
|
clearRecommendation(draft) {
|
|
110
161
|
Object.assign(draft, initialState);
|
|
111
162
|
},
|
|
112
163
|
},
|
|
113
164
|
});
|
|
114
|
-
export const { fetchEntityRecommendationsByTransactionId, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
|
|
165
|
+
export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
|
|
115
166
|
export default recommendation.reducer;
|
|
116
167
|
const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
|
|
117
168
|
const key = getEntityRecommendationKey(entityType, entityId);
|