@zeniai/client-epic-state 5.2.52 → 5.2.53
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/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -11
- package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +19 -3
- package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +21 -21
- package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +57 -9
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
- package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +18 -7
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +8 -4
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +27 -10
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +8 -5
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +7 -0
- package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +18 -2
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +3 -2
- package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +19 -19
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +49 -0
- package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +59 -10
- package/package.json +1 -1
|
@@ -6,7 +6,6 @@ import { updateTransactions } from '../../../../entity/transaction/transactionRe
|
|
|
6
6
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
|
|
7
7
|
import { getLineItemsByTransactionsIdsFromResponse } from '../../helpers/transactionCategorizationLocalDataHelper';
|
|
8
8
|
import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateParentTotalCountForTab, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
|
|
9
|
-
import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
|
|
10
9
|
import { TRANSACTIONS_TABS, toTransactionsSortKey, } from '../../types/transactionsViewState';
|
|
11
10
|
export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match),
|
|
12
11
|
// Per-tab switchMap: cancels in-flight requests for the same tab on a new
|
|
@@ -33,9 +32,10 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
|
|
|
33
32
|
// cross-status search (omit or true) is reserved for Receipts
|
|
34
33
|
// manual-match.
|
|
35
34
|
search_all_statuses: false,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Sent on both tabs: the backend ignores it for Review's own rows but uses
|
|
36
|
+
// it to scope the Completed badge it returns alongside them.
|
|
37
|
+
sub_tab: expenseAutomationTransactionsViewState
|
|
38
|
+
.selectedTransactionCategorizationCompletedSubTab,
|
|
39
39
|
transaction_group: expenseAutomationTransactionsViewState.selectedTransactionGroup,
|
|
40
40
|
};
|
|
41
41
|
return zeniAPI
|
|
@@ -87,6 +87,17 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
|
|
|
87
87
|
parentTotalCount: response.data.parent_tab_total_count ??
|
|
88
88
|
response.data.total_count,
|
|
89
89
|
}));
|
|
90
|
+
// The inactive tab's badge, counted under the same filters. Absent on
|
|
91
|
+
// pages after the first and on the cross-tab search path, where the
|
|
92
|
+
// last known value stays correct.
|
|
93
|
+
const otherTabTotalCount = response.data.other_tab_total_count;
|
|
94
|
+
if (otherTabTotalCount != null) {
|
|
95
|
+
updateActions.push(updateParentTotalCountForTab({
|
|
96
|
+
selectedPeriod,
|
|
97
|
+
selectedTab: selectedTab === 'review' ? 'autoCategorized' : 'review',
|
|
98
|
+
parentTotalCount: otherTabTotalCount,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
90
101
|
updateActions.push(resetOtherTabsFetchState({
|
|
91
102
|
refreshViewInBackground,
|
|
92
103
|
tabs: TRANSACTIONS_TABS.filter((tab) => selectedTab !== tab),
|
|
@@ -373,6 +373,16 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
373
373
|
if (cached != null) {
|
|
374
374
|
autoCat.uiState.pageToken = cached.pageToken;
|
|
375
375
|
autoCat.uiState.totalCount = { ...cached.totalCount };
|
|
376
|
+
// Keep the stored badge in step with the restored sub-tab: the selector
|
|
377
|
+
// falls back to it for any period this sub-tab has not loaded. Merged,
|
|
378
|
+
// not replaced -- the snapshot only holds periods this sub-tab actually
|
|
379
|
+
// loaded, while the badge map also carries periods filled from a Review
|
|
380
|
+
// response's `other_tab_total_count`. Overwriting would drop those and
|
|
381
|
+
// leave the badge at 0 for any period this sub-tab never visited.
|
|
382
|
+
draft.parentTotalCountByTab.autoCategorized = {
|
|
383
|
+
...draft.parentTotalCountByTab.autoCategorized,
|
|
384
|
+
...cached.totalCount,
|
|
385
|
+
};
|
|
376
386
|
autoCat.uiState.scrollPosition = cached.scrollPosition;
|
|
377
387
|
autoCat.transactionIdsBySelectedPeriod = {
|
|
378
388
|
...cached.transactionIdsBySelectedPeriod,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
import omit from 'lodash/omit';
|
|
3
|
-
import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
|
|
3
|
+
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
4
4
|
import { getCurrentTenant, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
|
|
5
5
|
import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
|
|
6
6
|
import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
|
|
@@ -172,18 +172,35 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
172
172
|
review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
|
|
173
173
|
autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
|
|
174
174
|
};
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
175
|
+
// Tab badge counts, used exclusively by the navbar / tab strip badges. Both
|
|
176
|
+
// tabs are counted under the filters applied to the list, so a badge always
|
|
177
|
+
// equals the number of rows that tab shows.
|
|
178
|
+
//
|
|
179
|
+
// The ACTIVE tab prefers its own list count: the two are the same number (the
|
|
180
|
+
// backend's parent_tab_total_count mirrors total_count), but `uiState.totalCount`
|
|
181
|
+
// is restored by every cache path that skips a fetch -- switching back to an
|
|
182
|
+
// already-loaded period, or to a cached Completed sub-tab -- whereas the stored
|
|
183
|
+
// badge is only written when a response lands, so on those paths it would keep
|
|
184
|
+
// showing whatever was fetched last.
|
|
185
|
+
//
|
|
186
|
+
// The `in` check is load-bearing, not defensive: `totalCount` is a per-period
|
|
187
|
+
// map typed as `Record<MonthYearPeriodId, number>`, so indexing it is typed
|
|
188
|
+
// `number` but is `undefined` for any period the active (sub-)tab has not
|
|
189
|
+
// loaded -- the window between switching to a fresh sub-tab and its response
|
|
190
|
+
// landing. Falling back to the stored badge there keeps a real number in the
|
|
191
|
+
// label instead of blanking it.
|
|
179
192
|
const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
|
|
193
|
+
const storedBadgeByTab = (tab) => monthYearPeriodId != null
|
|
194
|
+
? (parentTotalCountByTab[tab][monthYearPeriodId] ?? 0)
|
|
195
|
+
: 0;
|
|
196
|
+
const badgeForTab = (tab, listCountByPeriod) => selectedTransactionCategorizationTab === tab &&
|
|
197
|
+
monthYearPeriodId != null &&
|
|
198
|
+
monthYearPeriodId in listCountByPeriod
|
|
199
|
+
? listCountByPeriod[monthYearPeriodId]
|
|
200
|
+
: storedBadgeByTab(tab);
|
|
180
201
|
const parentTabTotalCountByTab = {
|
|
181
|
-
review:
|
|
182
|
-
|
|
183
|
-
: 0,
|
|
184
|
-
autoCategorized: monthYearPeriodId != null
|
|
185
|
-
? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
|
|
186
|
-
: 0,
|
|
202
|
+
review: badgeForTab('review', totalCountByReview),
|
|
203
|
+
autoCategorized: badgeForTab('autoCategorized', totalCountByAutoCat),
|
|
187
204
|
};
|
|
188
205
|
const fetchStateByTab = {
|
|
189
206
|
review: {
|
|
@@ -3,7 +3,7 @@ import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState'
|
|
|
3
3
|
import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
|
|
4
4
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
5
5
|
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
6
|
-
import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
6
|
+
import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, NO_INVOICING_CLASS_HIERARCHY, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
7
7
|
import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, initialInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormHelper';
|
|
8
8
|
export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
|
|
9
9
|
fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
|
|
@@ -44,6 +44,21 @@ const NOT_STARTED = {
|
|
|
44
44
|
error: undefined,
|
|
45
45
|
fetchState: 'Not-Started',
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* The QuickBooks ids a price point's income account may be picked from.
|
|
49
|
+
*
|
|
50
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
51
|
+
* the pages turn this into their picker options and re-seed their form defaults
|
|
52
|
+
* when those change, so a fresh array on every store change would reset the form
|
|
53
|
+
* under the user — including mid-save.
|
|
54
|
+
*/
|
|
55
|
+
const getInvoicingCatalogIncomeAccountIds = createSelector([
|
|
56
|
+
getInvoicingQboAccountOptions,
|
|
57
|
+
(state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
|
|
58
|
+
isInvoicingQboConnected,
|
|
59
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
60
|
+
? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
61
|
+
: NO_INVOICING_ACCOUNT_IDS);
|
|
47
62
|
export const getInvoicingCatalogItemFormView = (state, id) => {
|
|
48
63
|
const { editDraftById, error, fetchState, newItemDraft, savedItemId, savedKind, } = state.editInvoicingCatalogItemDetailViewState;
|
|
49
64
|
const plan = id != null ? getInvoicingPlanById(state.invoicingPlanState, id) : undefined;
|
|
@@ -94,8 +109,9 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
|
|
|
94
109
|
isFormLoading: isInvoicingAccountingPickerDataLoading(state) ||
|
|
95
110
|
isInvoicingFormDraftPending(editDraftById, id),
|
|
96
111
|
isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
|
|
97
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
98
|
-
qboClassHierarchy: qboView?.nestedClassHierarchy ??
|
|
112
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
113
|
+
qboClassHierarchy: qboView?.nestedClassHierarchy ?? NO_INVOICING_CLASS_HIERARCHY,
|
|
114
|
+
qboIncomeAccountIds: getInvoicingCatalogIncomeAccountIds(state),
|
|
99
115
|
applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
|
|
100
116
|
applicableItemOptions,
|
|
101
117
|
localData,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { createSelector } from '@reduxjs/toolkit';
|
|
1
2
|
import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
|
|
2
3
|
import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
|
|
3
|
-
import { toInvoicingFormOption,
|
|
4
|
+
import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
4
5
|
import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
|
|
5
6
|
import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
|
|
6
|
-
import {
|
|
7
|
+
import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountByQboId, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
|
|
7
8
|
import { getInvoicingSettings, getInvoicingSettingsFetchState, } from '../settingsView/settingsViewSelector';
|
|
8
9
|
import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdForCreate, } from './editInvoicingCouponDetailViewState';
|
|
9
10
|
/**
|
|
@@ -11,6 +12,21 @@ import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdF
|
|
|
11
12
|
* the id of the coupon last saved, and — when a `couponId` is passed (edit
|
|
12
13
|
* mode) — the existing coupon entity to prefill the form.
|
|
13
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The QuickBooks ids the discounts account may be picked from.
|
|
17
|
+
*
|
|
18
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
19
|
+
* the form turns this into its picker options and re-seeds its defaults when
|
|
20
|
+
* those change, so a fresh array on every store change would reset the form
|
|
21
|
+
* under the user until their first edit.
|
|
22
|
+
*/
|
|
23
|
+
const getInvoicingCouponDiscountsAccountIds = createSelector([
|
|
24
|
+
getInvoicingQboAccountOptions,
|
|
25
|
+
(state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
|
|
26
|
+
isInvoicingQboConnected,
|
|
27
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
28
|
+
? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
29
|
+
: NO_INVOICING_ACCOUNT_IDS);
|
|
14
30
|
const NOT_STARTED = {
|
|
15
31
|
error: undefined,
|
|
16
32
|
fetchState: 'Not-Started',
|
|
@@ -51,14 +67,6 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
51
67
|
version: 1.0,
|
|
52
68
|
};
|
|
53
69
|
};
|
|
54
|
-
/**
|
|
55
|
-
* The account types the discounts slot falls back to when the config has no
|
|
56
|
-
* `discounts_account` kind. A QuickBooks discount line posts to an income
|
|
57
|
-
* account; offering every account instead would let a save be rejected.
|
|
58
|
-
*/
|
|
59
|
-
const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
|
|
60
|
-
/** The account-mapping slot a coupon's discounts account shares with settings. */
|
|
61
|
-
const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
62
70
|
/**
|
|
63
71
|
* Bundled view for the discount form screen: the edit view (save state + the
|
|
64
72
|
* coupon to prefill in edit mode), the draft `localData` (seeded from the
|
|
@@ -74,7 +82,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
74
82
|
const localData = couponId != null
|
|
75
83
|
? getInvoicingFormDraftLocalData(editDraftById, couponId, initialInvoicingCouponFormLocalData)
|
|
76
84
|
: newCouponDraft;
|
|
77
|
-
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions,
|
|
85
|
+
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
|
|
78
86
|
const isQboConnected = isInvoicingQboConnected(state);
|
|
79
87
|
const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
|
|
80
88
|
const isQboPickerDataLoading = isInvoicingAccountingPickerDataLoading(state);
|
|
@@ -84,14 +92,6 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
84
92
|
const isCreateSeedPending = couponId == null &&
|
|
85
93
|
(settingsFetchState === 'Not-Started' ||
|
|
86
94
|
settingsFetchState === 'In-Progress');
|
|
87
|
-
// The account types the discounts slot accepts come from the same config the
|
|
88
|
-
// QBO settings page reads, so the two pickers offer the same accounts and
|
|
89
|
-
// widening the slot stays a config change. Matched on the slot rather than
|
|
90
|
-
// the code as written: a config that names the kind `discounts_account_id`
|
|
91
|
-
// is the same slot, and missing it here would quietly fall back to the
|
|
92
|
-
// default and offer a different set than the settings page does.
|
|
93
|
-
const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) ===
|
|
94
|
-
DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
|
|
95
95
|
return {
|
|
96
96
|
...base,
|
|
97
97
|
applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
|
|
@@ -130,7 +130,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
130
130
|
},
|
|
131
131
|
mode: couponId != null ? 'edit' : 'create',
|
|
132
132
|
periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
|
|
133
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
134
|
-
qboIncomeAccountIds:
|
|
133
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
134
|
+
qboIncomeAccountIds: getInvoicingCouponDiscountsAccountIds(state),
|
|
135
135
|
};
|
|
136
136
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createSelector } from '@reduxjs/toolkit';
|
|
2
|
+
import { toInvoicingQboAccountMappingSlot, } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
1
3
|
import { getAccountList, getNestedAccountListHierarchy, } from '../../accountList/accountListSelector';
|
|
2
4
|
import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
|
|
3
5
|
// The shared bucket, fetched with the same arguments as the other consumers so
|
|
@@ -82,14 +84,20 @@ export const getInvoicingCatalogAccountingLookup = (state) => ({
|
|
|
82
84
|
classByQboId: (qboId) => invoicingQboClassByQboId(state, qboId),
|
|
83
85
|
});
|
|
84
86
|
/**
|
|
85
|
-
* Accounts
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
87
|
+
* Accounts as the invoicing pickers need them, from the shared
|
|
88
|
+
* accounting-service list, so whatever another screen already loaded is reused.
|
|
89
|
+
* `qboId` is the submitted value because that is what the invoicing API stores;
|
|
90
|
+
* an account without one cannot be posted, so it is dropped rather than offered.
|
|
91
|
+
*
|
|
92
|
+
* Memoized on the two slices it reads: the pickers and the lists derived from
|
|
93
|
+
* them are handed straight to forms, and a fresh array on every store change
|
|
94
|
+
* re-seeds a form the user is editing.
|
|
90
95
|
*/
|
|
91
|
-
const
|
|
92
|
-
|
|
96
|
+
export const getInvoicingQboAccountOptions = createSelector([
|
|
97
|
+
(state) => state.accountState,
|
|
98
|
+
(state) => state.accountListState,
|
|
99
|
+
], (accountState, accountListState) => {
|
|
100
|
+
const { accounts } = getAccountList(accountState, accountListState, INVOICING_ACCOUNT_LIST_KEY);
|
|
93
101
|
return accounts.flatMap((account) => {
|
|
94
102
|
if (account.qboId == null || account.isDeleted === true) {
|
|
95
103
|
return [];
|
|
@@ -102,7 +110,7 @@ const invoicingAccountingOptions = (state) => {
|
|
|
102
110
|
},
|
|
103
111
|
];
|
|
104
112
|
});
|
|
105
|
-
};
|
|
113
|
+
});
|
|
106
114
|
/**
|
|
107
115
|
* Normalises an account-type name for comparison. The config's mapping kinds
|
|
108
116
|
* name QuickBooks types the way QuickBooks displays them ("Other Current
|
|
@@ -138,6 +146,46 @@ export const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
|
|
|
138
146
|
return accounts.filter((account) => account.accountType != null &&
|
|
139
147
|
wanted.has(normalizeAccountTypeName(account.accountType)));
|
|
140
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* The account-mapping slot a catalog price point's income account shares with
|
|
151
|
+
* the settings page, and the one a discount's account shares with it.
|
|
152
|
+
*/
|
|
153
|
+
export const INVOICING_INCOME_ACCOUNT_MAPPING_SLOT = 'default_income_account';
|
|
154
|
+
export const INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
155
|
+
/**
|
|
156
|
+
* The account types a slot falls back to when the config carries no kind for
|
|
157
|
+
* it. QuickBooks posts a product, a price and a discount line to an income
|
|
158
|
+
* account; offering every account instead would let a save be rejected.
|
|
159
|
+
*/
|
|
160
|
+
export const INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = 'Income';
|
|
161
|
+
/**
|
|
162
|
+
* The QuickBooks ids a mapping slot's picker may offer, from the account types
|
|
163
|
+
* the config gives that slot — so every picker filling the same slot offers the
|
|
164
|
+
* same accounts, and widening one stays a config change.
|
|
165
|
+
*
|
|
166
|
+
* Ids rather than a filtered tree: the pickers flatten the tree into options by
|
|
167
|
+
* walking each node's children, so dropping a parent that is not itself of the
|
|
168
|
+
* wanted type would drop its matching descendants with it.
|
|
169
|
+
*
|
|
170
|
+
* Matched on the slot rather than the code as written, since a config naming
|
|
171
|
+
* the kind `discounts_account_id` means the same slot. A slot the config does
|
|
172
|
+
* not carry falls back to `defaultAccountTypes` rather than to every account.
|
|
173
|
+
*/
|
|
174
|
+
export const invoicingQboAccountIdsForMappingSlot = (accounts, accountMappingKindOptions, mappingSlot, defaultAccountTypes) => {
|
|
175
|
+
const accountTypes = accountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) === mappingSlot)?.accountTypes ?? defaultAccountTypes;
|
|
176
|
+
return invoicingAccountsForQboAccountTypes(accounts, accountTypes).map((account) => account.id);
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Stable empties for a tenant with no QBO connection.
|
|
180
|
+
*
|
|
181
|
+
* `?? []` builds a fresh array per call, and the invoicing forms re-seed their
|
|
182
|
+
* fields whenever their option lists change identity — so a form open against a
|
|
183
|
+
* disconnected tenant would reset itself on every unrelated store change, until
|
|
184
|
+
* the first edit disarms the re-seed.
|
|
185
|
+
*/
|
|
186
|
+
export const NO_INVOICING_ACCOUNT_HIERARCHY = [];
|
|
187
|
+
export const NO_INVOICING_CLASS_HIERARCHY = [];
|
|
188
|
+
export const NO_INVOICING_ACCOUNT_IDS = [];
|
|
141
189
|
/** Classes as the invoicing pickers need them, from the shared class list. */
|
|
142
190
|
const invoicingAccountingClassOptions = (state) => {
|
|
143
191
|
const { classes } = getClassList(state.classState, state.classListState);
|
|
@@ -159,7 +207,7 @@ export const getInvoicingQboView = (state) => {
|
|
|
159
207
|
const qboState = state.invoicingQboViewState;
|
|
160
208
|
const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
|
|
161
209
|
return {
|
|
162
|
-
accounts:
|
|
210
|
+
accounts: getInvoicingQboAccountOptions(state),
|
|
163
211
|
accountsFetchState: {
|
|
164
212
|
fetchState: accountList.fetchState,
|
|
165
213
|
error: accountList.error,
|
|
@@ -9,7 +9,6 @@ const transactionReducer_1 = require("../../../../entity/transaction/transaction
|
|
|
9
9
|
const responsePayload_1 = require("../../../../responsePayload");
|
|
10
10
|
const transactionCategorizationLocalDataHelper_1 = require("../../helpers/transactionCategorizationLocalDataHelper");
|
|
11
11
|
const transactionsViewReducer_1 = require("../../reducers/transactionsViewReducer");
|
|
12
|
-
const completedSubTab_1 = require("../../types/completedSubTab");
|
|
13
12
|
const transactionsViewState_1 = require("../../types/transactionsViewState");
|
|
14
13
|
const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(transactionsViewReducer_1.fetchTransactionCategorization.match),
|
|
15
14
|
// Per-tab switchMap: cancels in-flight requests for the same tab on a new
|
|
@@ -36,9 +35,10 @@ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => action
|
|
|
36
35
|
// cross-status search (omit or true) is reserved for Receipts
|
|
37
36
|
// manual-match.
|
|
38
37
|
search_all_statuses: false,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// Sent on both tabs: the backend ignores it for Review's own rows but uses
|
|
39
|
+
// it to scope the Completed badge it returns alongside them.
|
|
40
|
+
sub_tab: expenseAutomationTransactionsViewState
|
|
41
|
+
.selectedTransactionCategorizationCompletedSubTab,
|
|
42
42
|
transaction_group: expenseAutomationTransactionsViewState.selectedTransactionGroup,
|
|
43
43
|
};
|
|
44
44
|
return zeniAPI
|
|
@@ -90,6 +90,17 @@ const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => action
|
|
|
90
90
|
parentTotalCount: response.data.parent_tab_total_count ??
|
|
91
91
|
response.data.total_count,
|
|
92
92
|
}));
|
|
93
|
+
// The inactive tab's badge, counted under the same filters. Absent on
|
|
94
|
+
// pages after the first and on the cross-tab search path, where the
|
|
95
|
+
// last known value stays correct.
|
|
96
|
+
const otherTabTotalCount = response.data.other_tab_total_count;
|
|
97
|
+
if (otherTabTotalCount != null) {
|
|
98
|
+
updateActions.push((0, transactionsViewReducer_1.updateParentTotalCountForTab)({
|
|
99
|
+
selectedPeriod,
|
|
100
|
+
selectedTab: selectedTab === 'review' ? 'autoCategorized' : 'review',
|
|
101
|
+
parentTotalCount: otherTabTotalCount,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
93
104
|
updateActions.push((0, transactionsViewReducer_1.resetOtherTabsFetchState)({
|
|
94
105
|
refreshViewInBackground,
|
|
95
106
|
tabs: transactionsViewState_1.TRANSACTIONS_TABS.filter((tab) => selectedTab !== tab),
|
|
@@ -15,9 +15,9 @@ export interface TransactionCategorizationQueryPayload {
|
|
|
15
15
|
start_date: string | null;
|
|
16
16
|
/**
|
|
17
17
|
* Required. For the Completed (`auto_categorized=true`) listing, scopes the
|
|
18
|
-
* returned page + `total_count` to one of the Completed sub-tabs.
|
|
19
|
-
* Review listing
|
|
20
|
-
*
|
|
18
|
+
* returned page + `total_count` to one of the Completed sub-tabs. It is sent
|
|
19
|
+
* on the Review listing too: the backend ignores it for Review's own status
|
|
20
|
+
* set, but uses it to scope the Completed badge in `other_tab_total_count`.
|
|
21
21
|
*/
|
|
22
22
|
sub_tab: CompletedSubTab;
|
|
23
23
|
/**
|
|
@@ -44,10 +44,21 @@ export interface TransactionCategorizationPayload {
|
|
|
44
44
|
total_count: number;
|
|
45
45
|
transactions: SupportedTransactionPayload[];
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* Badge count for the OTHER tab (Completed when this response is Review, and
|
|
48
|
+
* vice versa) under the same filters, so both badges move together with the
|
|
49
|
+
* Transaction Group dropdown and the Completed sub-tab without having to
|
|
50
|
+
* fetch that tab's rows.
|
|
51
|
+
*
|
|
52
|
+
* `null`/absent when the backend computed no badge — pages after the first
|
|
53
|
+
* (a scroll cannot change either badge) and the cross-tab search path used by
|
|
54
|
+
* Receipts manual-match. Consumers must keep their last known value then.
|
|
55
|
+
*/
|
|
56
|
+
other_tab_total_count?: number | null;
|
|
57
|
+
/**
|
|
58
|
+
* Badge count for the tab this response belongs to, taken under every filter
|
|
59
|
+
* the request carried (`transaction_group`, `sub_tab`, `search_text`) — so it
|
|
60
|
+
* matches the rows rendered below it and equals {@link total_count}. Retained
|
|
61
|
+
* as its own key for wire compatibility.
|
|
51
62
|
*
|
|
52
63
|
* Optional at the wire boundary so that any version-skew scenario
|
|
53
64
|
* (delayed BE rollout, cached SW response, hotfix without the BE change)
|
|
@@ -380,6 +380,16 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
|
|
|
380
380
|
if (cached != null) {
|
|
381
381
|
autoCat.uiState.pageToken = cached.pageToken;
|
|
382
382
|
autoCat.uiState.totalCount = { ...cached.totalCount };
|
|
383
|
+
// Keep the stored badge in step with the restored sub-tab: the selector
|
|
384
|
+
// falls back to it for any period this sub-tab has not loaded. Merged,
|
|
385
|
+
// not replaced -- the snapshot only holds periods this sub-tab actually
|
|
386
|
+
// loaded, while the badge map also carries periods filled from a Review
|
|
387
|
+
// response's `other_tab_total_count`. Overwriting would drop those and
|
|
388
|
+
// leave the badge at 0 for any period this sub-tab never visited.
|
|
389
|
+
draft.parentTotalCountByTab.autoCategorized = {
|
|
390
|
+
...draft.parentTotalCountByTab.autoCategorized,
|
|
391
|
+
...cached.totalCount,
|
|
392
|
+
};
|
|
383
393
|
autoCat.uiState.scrollPosition = cached.scrollPosition;
|
|
384
394
|
autoCat.transactionIdsBySelectedPeriod = {
|
|
385
395
|
...cached.transactionIdsBySelectedPeriod,
|
|
@@ -28,10 +28,14 @@ export interface ExpenseAutomationTransactionViewSelector extends SelectorView {
|
|
|
28
28
|
isAccountingProjectsEnabled: boolean;
|
|
29
29
|
markAsNotMiscategorizedStatus: FetchStateAndError;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* Tab badge counts under the filters currently applied to the list
|
|
32
|
+
* (transaction group + Completed sub-tab + search), so each badge matches
|
|
33
|
+
* the rows its tab renders. The active tab's entry is {@link totalCountByTab}
|
|
34
|
+
* itself, so it cannot drift when a cache hit skips the fetch; the inactive
|
|
35
|
+
* tab's comes from the listing response's `other_tab_total_count`. Used
|
|
36
|
+
* exclusively to drive the navbar / tab strip badge labels — every other
|
|
37
|
+
* count consumer (pagination, in-list "showing X of Y", save flows) keeps
|
|
38
|
+
* reading {@link totalCountByTab}.
|
|
35
39
|
*/
|
|
36
40
|
parentTabTotalCountByTab: Record<TransactionsTab, number>;
|
|
37
41
|
projectList: ProjectListSelectorView;
|
|
@@ -182,18 +182,35 @@ function getExpenseAutomationTransactionView(state) {
|
|
|
182
182
|
review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
|
|
183
183
|
autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
|
|
184
184
|
};
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
185
|
+
// Tab badge counts, used exclusively by the navbar / tab strip badges. Both
|
|
186
|
+
// tabs are counted under the filters applied to the list, so a badge always
|
|
187
|
+
// equals the number of rows that tab shows.
|
|
188
|
+
//
|
|
189
|
+
// The ACTIVE tab prefers its own list count: the two are the same number (the
|
|
190
|
+
// backend's parent_tab_total_count mirrors total_count), but `uiState.totalCount`
|
|
191
|
+
// is restored by every cache path that skips a fetch -- switching back to an
|
|
192
|
+
// already-loaded period, or to a cached Completed sub-tab -- whereas the stored
|
|
193
|
+
// badge is only written when a response lands, so on those paths it would keep
|
|
194
|
+
// showing whatever was fetched last.
|
|
195
|
+
//
|
|
196
|
+
// The `in` check is load-bearing, not defensive: `totalCount` is a per-period
|
|
197
|
+
// map typed as `Record<MonthYearPeriodId, number>`, so indexing it is typed
|
|
198
|
+
// `number` but is `undefined` for any period the active (sub-)tab has not
|
|
199
|
+
// loaded -- the window between switching to a fresh sub-tab and its response
|
|
200
|
+
// landing. Falling back to the stored badge there keeps a real number in the
|
|
201
|
+
// label instead of blanking it.
|
|
189
202
|
const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
|
|
203
|
+
const storedBadgeByTab = (tab) => monthYearPeriodId != null
|
|
204
|
+
? (parentTotalCountByTab[tab][monthYearPeriodId] ?? 0)
|
|
205
|
+
: 0;
|
|
206
|
+
const badgeForTab = (tab, listCountByPeriod) => selectedTransactionCategorizationTab === tab &&
|
|
207
|
+
monthYearPeriodId != null &&
|
|
208
|
+
monthYearPeriodId in listCountByPeriod
|
|
209
|
+
? listCountByPeriod[monthYearPeriodId]
|
|
210
|
+
: storedBadgeByTab(tab);
|
|
190
211
|
const parentTabTotalCountByTab = {
|
|
191
|
-
review:
|
|
192
|
-
|
|
193
|
-
: 0,
|
|
194
|
-
autoCategorized: monthYearPeriodId != null
|
|
195
|
-
? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
|
|
196
|
-
: 0,
|
|
212
|
+
review: badgeForTab('review', totalCountByReview),
|
|
213
|
+
autoCategorized: badgeForTab('autoCategorized', totalCountByAutoCat),
|
|
197
214
|
};
|
|
198
215
|
const fetchStateByTab = {
|
|
199
216
|
review: {
|
|
@@ -139,11 +139,14 @@ export type TransactionsViewState = {
|
|
|
139
139
|
*/
|
|
140
140
|
autoCategorizedSubTabCache: Partial<Record<CompletedSubTab, AutoCategorizedSubTabSnapshot>>;
|
|
141
141
|
/**
|
|
142
|
-
* Period-keyed
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
142
|
+
* Period-keyed tab badge counts, scoped per {@link TransactionsTab}. Every
|
|
143
|
+
* count here is taken under the filters currently applied to the list
|
|
144
|
+
* (`transaction_group` + Completed `sub_tab` + search), so a badge always
|
|
145
|
+
* matches the rows that tab renders. The active tab is filled from the
|
|
146
|
+
* listing response's `parent_tab_total_count` and the inactive one from its
|
|
147
|
+
* `other_tab_total_count`. Used exclusively to drive the tab badges — every
|
|
148
|
+
* other count consumer (pagination, in-tab "showing X of Y") keeps reading
|
|
149
|
+
* from {@link TransactionsViewUIState.totalCount}.
|
|
147
150
|
*/
|
|
148
151
|
parentTotalCountByTab: Record<TransactionsTab, Record<MonthYearPeriodId, number>>;
|
|
149
152
|
selectedTransactionCategorizationCompletedSubTab: CompletedSubTab;
|
|
@@ -79,6 +79,13 @@ export interface InvoicingCatalogItemFormView extends SelectorView {
|
|
|
79
79
|
*/
|
|
80
80
|
qboAccountHierarchy: NestedAccountHierarchyForReport[];
|
|
81
81
|
qboClassHierarchy: NestedClassHierarchyForReport[];
|
|
82
|
+
/**
|
|
83
|
+
* The ids within the account tree an income account may be picked from.
|
|
84
|
+
* QuickBooks only accepts an income account on a product, so a price point
|
|
85
|
+
* mapped to anything else — A/R, deferred revenue, a bank account — fails to
|
|
86
|
+
* sync once an invoice carrying it is pushed.
|
|
87
|
+
*/
|
|
88
|
+
qboIncomeAccountIds: string[];
|
|
82
89
|
/**
|
|
83
90
|
* Loading the item, as distinct from this view's own `fetchState`, which
|
|
84
91
|
* tracks the save.
|
|
@@ -50,6 +50,21 @@ const NOT_STARTED = {
|
|
|
50
50
|
error: undefined,
|
|
51
51
|
fetchState: 'Not-Started',
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* The QuickBooks ids a price point's income account may be picked from.
|
|
55
|
+
*
|
|
56
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
57
|
+
* the pages turn this into their picker options and re-seed their form defaults
|
|
58
|
+
* when those change, so a fresh array on every store change would reset the form
|
|
59
|
+
* under the user — including mid-save.
|
|
60
|
+
*/
|
|
61
|
+
const getInvoicingCatalogIncomeAccountIds = (0, toolkit_1.createSelector)([
|
|
62
|
+
invoicingQboViewSelector_1.getInvoicingQboAccountOptions,
|
|
63
|
+
(state) => (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state).qboAccountMappingKindOptions,
|
|
64
|
+
invoicingQboViewSelector_1.isInvoicingQboConnected,
|
|
65
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
66
|
+
? (0, invoicingQboViewSelector_1.invoicingQboAccountIdsForMappingSlot)(accounts, qboAccountMappingKindOptions, invoicingQboViewSelector_1.INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, invoicingQboViewSelector_1.INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
67
|
+
: invoicingQboViewSelector_1.NO_INVOICING_ACCOUNT_IDS);
|
|
53
68
|
const getInvoicingCatalogItemFormView = (state, id) => {
|
|
54
69
|
const { editDraftById, error, fetchState, newItemDraft, savedItemId, savedKind, } = state.editInvoicingCatalogItemDetailViewState;
|
|
55
70
|
const plan = id != null ? (0, planSelector_1.getInvoicingPlanById)(state.invoicingPlanState, id) : undefined;
|
|
@@ -100,8 +115,9 @@ const getInvoicingCatalogItemFormView = (state, id) => {
|
|
|
100
115
|
isFormLoading: (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state) ||
|
|
101
116
|
(0, invoicingFormDraft_1.isInvoicingFormDraftPending)(editDraftById, id),
|
|
102
117
|
isQboPickerDataLoading: (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state),
|
|
103
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
104
|
-
qboClassHierarchy: qboView?.nestedClassHierarchy ??
|
|
118
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? invoicingQboViewSelector_1.NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
119
|
+
qboClassHierarchy: qboView?.nestedClassHierarchy ?? invoicingQboViewSelector_1.NO_INVOICING_CLASS_HIERARCHY,
|
|
120
|
+
qboIncomeAccountIds: getInvoicingCatalogIncomeAccountIds(state),
|
|
105
121
|
applicabilityValues: invoicingCatalogItemFormHelper_1.INVOICING_CATALOG_APPLICABILITY_VALUES,
|
|
106
122
|
applicableItemOptions,
|
|
107
123
|
localData,
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts
CHANGED
|
@@ -82,8 +82,9 @@ export interface InvoicingCouponFormView extends EditInvoicingCouponDetailView {
|
|
|
82
82
|
/**
|
|
83
83
|
* The account tree the discounts-account picker renders, and the QuickBooks
|
|
84
84
|
* ids within it the discounts slot accepts. Two values rather than a filtered
|
|
85
|
-
* tree: the picker
|
|
86
|
-
*
|
|
85
|
+
* tree: the picker flattens the tree into options by walking each node's
|
|
86
|
+
* children, so dropping a parent that is not itself an income account would
|
|
87
|
+
* drop its income descendants with it.
|
|
87
88
|
*
|
|
88
89
|
* Empty until QBO is connected — the form must not offer a mapping that
|
|
89
90
|
* cannot be pushed.
|
package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getInvoicingCouponFormView = exports.getEditInvoicingCouponDetailViewState = void 0;
|
|
4
|
+
const toolkit_1 = require("@reduxjs/toolkit");
|
|
4
5
|
const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
|
|
5
6
|
const couponSelector_1 = require("../../../entity/invoicing/coupon/couponSelector");
|
|
6
7
|
const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
|
|
@@ -14,6 +15,21 @@ const editInvoicingCouponDetailViewState_1 = require("./editInvoicingCouponDetai
|
|
|
14
15
|
* the id of the coupon last saved, and — when a `couponId` is passed (edit
|
|
15
16
|
* mode) — the existing coupon entity to prefill the form.
|
|
16
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* The QuickBooks ids the discounts account may be picked from.
|
|
20
|
+
*
|
|
21
|
+
* Memoized, and collapsing to one shared empty array while QBO is disconnected:
|
|
22
|
+
* the form turns this into its picker options and re-seeds its defaults when
|
|
23
|
+
* those change, so a fresh array on every store change would reset the form
|
|
24
|
+
* under the user until their first edit.
|
|
25
|
+
*/
|
|
26
|
+
const getInvoicingCouponDiscountsAccountIds = (0, toolkit_1.createSelector)([
|
|
27
|
+
invoicingQboViewSelector_1.getInvoicingQboAccountOptions,
|
|
28
|
+
(state) => (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state).qboAccountMappingKindOptions,
|
|
29
|
+
invoicingQboViewSelector_1.isInvoicingQboConnected,
|
|
30
|
+
], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
|
|
31
|
+
? (0, invoicingQboViewSelector_1.invoicingQboAccountIdsForMappingSlot)(accounts, qboAccountMappingKindOptions, invoicingQboViewSelector_1.INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, invoicingQboViewSelector_1.INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
|
|
32
|
+
: invoicingQboViewSelector_1.NO_INVOICING_ACCOUNT_IDS);
|
|
17
33
|
const NOT_STARTED = {
|
|
18
34
|
error: undefined,
|
|
19
35
|
fetchState: 'Not-Started',
|
|
@@ -55,14 +71,6 @@ const getEditInvoicingCouponDetailViewState = (state, couponId) => {
|
|
|
55
71
|
};
|
|
56
72
|
};
|
|
57
73
|
exports.getEditInvoicingCouponDetailViewState = getEditInvoicingCouponDetailViewState;
|
|
58
|
-
/**
|
|
59
|
-
* The account types the discounts slot falls back to when the config has no
|
|
60
|
-
* `discounts_account` kind. A QuickBooks discount line posts to an income
|
|
61
|
-
* account; offering every account instead would let a save be rejected.
|
|
62
|
-
*/
|
|
63
|
-
const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
|
|
64
|
-
/** The account-mapping slot a coupon's discounts account shares with settings. */
|
|
65
|
-
const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
66
74
|
/**
|
|
67
75
|
* Bundled view for the discount form screen: the edit view (save state + the
|
|
68
76
|
* coupon to prefill in edit mode), the draft `localData` (seeded from the
|
|
@@ -78,7 +86,7 @@ const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
78
86
|
const localData = couponId != null
|
|
79
87
|
? (0, invoicingFormDraft_1.getInvoicingFormDraftLocalData)(editDraftById, couponId, editInvoicingCouponDetailViewState_1.initialInvoicingCouponFormLocalData)
|
|
80
88
|
: newCouponDraft;
|
|
81
|
-
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions,
|
|
89
|
+
const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
|
|
82
90
|
const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
|
|
83
91
|
const qboView = isQboConnected ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state) : undefined;
|
|
84
92
|
const isQboPickerDataLoading = (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state);
|
|
@@ -88,14 +96,6 @@ const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
88
96
|
const isCreateSeedPending = couponId == null &&
|
|
89
97
|
(settingsFetchState === 'Not-Started' ||
|
|
90
98
|
settingsFetchState === 'In-Progress');
|
|
91
|
-
// The account types the discounts slot accepts come from the same config the
|
|
92
|
-
// QBO settings page reads, so the two pickers offer the same accounts and
|
|
93
|
-
// widening the slot stays a config change. Matched on the slot rather than
|
|
94
|
-
// the code as written: a config that names the kind `discounts_account_id`
|
|
95
|
-
// is the same slot, and missing it here would quietly fall back to the
|
|
96
|
-
// default and offer a different set than the settings page does.
|
|
97
|
-
const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => (0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(kindOption.code) ===
|
|
98
|
-
DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
|
|
99
99
|
return {
|
|
100
100
|
...base,
|
|
101
101
|
applyOnOptions: discountApplyOnOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
@@ -134,8 +134,8 @@ const getInvoicingCouponFormView = (state, couponId) => {
|
|
|
134
134
|
},
|
|
135
135
|
mode: couponId != null ? 'edit' : 'create',
|
|
136
136
|
periodUnitOptions: billingPeriodUnitOptions.map(invoicingCommonPayload_1.toInvoicingFormOption),
|
|
137
|
-
qboAccountHierarchy: qboView?.nestedAccountHierarchy ??
|
|
138
|
-
qboIncomeAccountIds: (
|
|
137
|
+
qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? invoicingQboViewSelector_1.NO_INVOICING_ACCOUNT_HIERARCHY,
|
|
138
|
+
qboIncomeAccountIds: getInvoicingCouponDiscountsAccountIds(state),
|
|
139
139
|
};
|
|
140
140
|
};
|
|
141
141
|
exports.getInvoicingCouponFormView = getInvoicingCouponFormView;
|
|
@@ -3,6 +3,7 @@ import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classes
|
|
|
3
3
|
import { FetchStateAndError } from '../../../commonStateTypes/common';
|
|
4
4
|
import { AccountBase } from '../../../entity/account/accountState';
|
|
5
5
|
import { ClassBase } from '../../../entity/class/classState';
|
|
6
|
+
import { InvoicingQboAccountMappingKindOption } from '../../../entity/invoicing/invoicingCommonPayload';
|
|
6
7
|
import { RootState } from '../../../rootStateTypes';
|
|
7
8
|
import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
|
|
8
9
|
export declare const INVOICING_ACCOUNT_LIST_KEY: "accountList";
|
|
@@ -60,6 +61,17 @@ export interface InvoicingQboView {
|
|
|
60
61
|
connection?: InvoicingQboConnection;
|
|
61
62
|
syncHealth?: InvoicingQboSyncHealth;
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Accounts as the invoicing pickers need them, from the shared
|
|
66
|
+
* accounting-service list, so whatever another screen already loaded is reused.
|
|
67
|
+
* `qboId` is the submitted value because that is what the invoicing API stores;
|
|
68
|
+
* an account without one cannot be posted, so it is dropped rather than offered.
|
|
69
|
+
*
|
|
70
|
+
* Memoized on the two slices it reads: the pickers and the lists derived from
|
|
71
|
+
* them are handed straight to forms, and a fresh array on every store change
|
|
72
|
+
* re-seeds a form the user is editing.
|
|
73
|
+
*/
|
|
74
|
+
export declare const getInvoicingQboAccountOptions: (state: RootState) => InvoicingAccountingOption[];
|
|
63
75
|
/**
|
|
64
76
|
* The accounts valid for one QBO mapping kind, whose `accountTypes` is a
|
|
65
77
|
* comma-separated list of QuickBooks type names. An unrecognised list yields no
|
|
@@ -67,6 +79,43 @@ export interface InvoicingQboView {
|
|
|
67
79
|
* required would post a mapping QuickBooks rejects.
|
|
68
80
|
*/
|
|
69
81
|
export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
|
|
82
|
+
/**
|
|
83
|
+
* The account-mapping slot a catalog price point's income account shares with
|
|
84
|
+
* the settings page, and the one a discount's account shares with it.
|
|
85
|
+
*/
|
|
86
|
+
export declare const INVOICING_INCOME_ACCOUNT_MAPPING_SLOT = "default_income_account";
|
|
87
|
+
export declare const INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT = "discounts_account";
|
|
88
|
+
/**
|
|
89
|
+
* The account types a slot falls back to when the config carries no kind for
|
|
90
|
+
* it. QuickBooks posts a product, a price and a discount line to an income
|
|
91
|
+
* account; offering every account instead would let a save be rejected.
|
|
92
|
+
*/
|
|
93
|
+
export declare const INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = "Income";
|
|
94
|
+
/**
|
|
95
|
+
* The QuickBooks ids a mapping slot's picker may offer, from the account types
|
|
96
|
+
* the config gives that slot — so every picker filling the same slot offers the
|
|
97
|
+
* same accounts, and widening one stays a config change.
|
|
98
|
+
*
|
|
99
|
+
* Ids rather than a filtered tree: the pickers flatten the tree into options by
|
|
100
|
+
* walking each node's children, so dropping a parent that is not itself of the
|
|
101
|
+
* wanted type would drop its matching descendants with it.
|
|
102
|
+
*
|
|
103
|
+
* Matched on the slot rather than the code as written, since a config naming
|
|
104
|
+
* the kind `discounts_account_id` means the same slot. A slot the config does
|
|
105
|
+
* not carry falls back to `defaultAccountTypes` rather than to every account.
|
|
106
|
+
*/
|
|
107
|
+
export declare const invoicingQboAccountIdsForMappingSlot: (accounts: InvoicingAccountingOption[], accountMappingKindOptions: InvoicingQboAccountMappingKindOption[], mappingSlot: string, defaultAccountTypes: string) => string[];
|
|
108
|
+
/**
|
|
109
|
+
* Stable empties for a tenant with no QBO connection.
|
|
110
|
+
*
|
|
111
|
+
* `?? []` builds a fresh array per call, and the invoicing forms re-seed their
|
|
112
|
+
* fields whenever their option lists change identity — so a form open against a
|
|
113
|
+
* disconnected tenant would reset itself on every unrelated store change, until
|
|
114
|
+
* the first edit disarms the re-seed.
|
|
115
|
+
*/
|
|
116
|
+
export declare const NO_INVOICING_ACCOUNT_HIERARCHY: NestedAccountHierarchyForReport[];
|
|
117
|
+
export declare const NO_INVOICING_CLASS_HIERARCHY: NestedClassHierarchyForReport[];
|
|
118
|
+
export declare const NO_INVOICING_ACCOUNT_IDS: string[];
|
|
70
119
|
/**
|
|
71
120
|
* Single bundled view for the QBO integration settings screen — the
|
|
72
121
|
* connection, the account and class options, sync health, and the fetch/action
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getInvoicingQboView = exports.invoicingAccountsForQboAccountTypes = exports.getInvoicingCatalogAccountingLookup = exports.invoicingQboClassByQboId = exports.invoicingQboAccountByQboId = exports.getInvoicingQboConnectionFetchState = exports.isInvoicingAccountingPickerDataLoading = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
|
|
3
|
+
exports.getInvoicingQboView = exports.NO_INVOICING_ACCOUNT_IDS = exports.NO_INVOICING_CLASS_HIERARCHY = exports.NO_INVOICING_ACCOUNT_HIERARCHY = exports.invoicingQboAccountIdsForMappingSlot = exports.INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = exports.INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT = exports.INVOICING_INCOME_ACCOUNT_MAPPING_SLOT = exports.invoicingAccountsForQboAccountTypes = exports.getInvoicingQboAccountOptions = exports.getInvoicingCatalogAccountingLookup = exports.invoicingQboClassByQboId = exports.invoicingQboAccountByQboId = exports.getInvoicingQboConnectionFetchState = exports.isInvoicingAccountingPickerDataLoading = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
|
|
4
|
+
const toolkit_1 = require("@reduxjs/toolkit");
|
|
5
|
+
const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
|
|
4
6
|
const accountListSelector_1 = require("../../accountList/accountListSelector");
|
|
5
7
|
const classListSelector_1 = require("../../classList/classListSelector");
|
|
6
8
|
// The shared bucket, fetched with the same arguments as the other consumers so
|
|
@@ -91,14 +93,20 @@ const getInvoicingCatalogAccountingLookup = (state) => ({
|
|
|
91
93
|
});
|
|
92
94
|
exports.getInvoicingCatalogAccountingLookup = getInvoicingCatalogAccountingLookup;
|
|
93
95
|
/**
|
|
94
|
-
* Accounts
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* Accounts as the invoicing pickers need them, from the shared
|
|
97
|
+
* accounting-service list, so whatever another screen already loaded is reused.
|
|
98
|
+
* `qboId` is the submitted value because that is what the invoicing API stores;
|
|
99
|
+
* an account without one cannot be posted, so it is dropped rather than offered.
|
|
100
|
+
*
|
|
101
|
+
* Memoized on the two slices it reads: the pickers and the lists derived from
|
|
102
|
+
* them are handed straight to forms, and a fresh array on every store change
|
|
103
|
+
* re-seeds a form the user is editing.
|
|
99
104
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
exports.getInvoicingQboAccountOptions = (0, toolkit_1.createSelector)([
|
|
106
|
+
(state) => state.accountState,
|
|
107
|
+
(state) => state.accountListState,
|
|
108
|
+
], (accountState, accountListState) => {
|
|
109
|
+
const { accounts } = (0, accountListSelector_1.getAccountList)(accountState, accountListState, exports.INVOICING_ACCOUNT_LIST_KEY);
|
|
102
110
|
return accounts.flatMap((account) => {
|
|
103
111
|
if (account.qboId == null || account.isDeleted === true) {
|
|
104
112
|
return [];
|
|
@@ -111,7 +119,7 @@ const invoicingAccountingOptions = (state) => {
|
|
|
111
119
|
},
|
|
112
120
|
];
|
|
113
121
|
});
|
|
114
|
-
};
|
|
122
|
+
});
|
|
115
123
|
/**
|
|
116
124
|
* Normalises an account-type name for comparison. The config's mapping kinds
|
|
117
125
|
* name QuickBooks types the way QuickBooks displays them ("Other Current
|
|
@@ -148,6 +156,47 @@ const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
|
|
|
148
156
|
wanted.has(normalizeAccountTypeName(account.accountType)));
|
|
149
157
|
};
|
|
150
158
|
exports.invoicingAccountsForQboAccountTypes = invoicingAccountsForQboAccountTypes;
|
|
159
|
+
/**
|
|
160
|
+
* The account-mapping slot a catalog price point's income account shares with
|
|
161
|
+
* the settings page, and the one a discount's account shares with it.
|
|
162
|
+
*/
|
|
163
|
+
exports.INVOICING_INCOME_ACCOUNT_MAPPING_SLOT = 'default_income_account';
|
|
164
|
+
exports.INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
|
|
165
|
+
/**
|
|
166
|
+
* The account types a slot falls back to when the config carries no kind for
|
|
167
|
+
* it. QuickBooks posts a product, a price and a discount line to an income
|
|
168
|
+
* account; offering every account instead would let a save be rejected.
|
|
169
|
+
*/
|
|
170
|
+
exports.INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = 'Income';
|
|
171
|
+
/**
|
|
172
|
+
* The QuickBooks ids a mapping slot's picker may offer, from the account types
|
|
173
|
+
* the config gives that slot — so every picker filling the same slot offers the
|
|
174
|
+
* same accounts, and widening one stays a config change.
|
|
175
|
+
*
|
|
176
|
+
* Ids rather than a filtered tree: the pickers flatten the tree into options by
|
|
177
|
+
* walking each node's children, so dropping a parent that is not itself of the
|
|
178
|
+
* wanted type would drop its matching descendants with it.
|
|
179
|
+
*
|
|
180
|
+
* Matched on the slot rather than the code as written, since a config naming
|
|
181
|
+
* the kind `discounts_account_id` means the same slot. A slot the config does
|
|
182
|
+
* not carry falls back to `defaultAccountTypes` rather than to every account.
|
|
183
|
+
*/
|
|
184
|
+
const invoicingQboAccountIdsForMappingSlot = (accounts, accountMappingKindOptions, mappingSlot, defaultAccountTypes) => {
|
|
185
|
+
const accountTypes = accountMappingKindOptions.find((kindOption) => (0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(kindOption.code) === mappingSlot)?.accountTypes ?? defaultAccountTypes;
|
|
186
|
+
return (0, exports.invoicingAccountsForQboAccountTypes)(accounts, accountTypes).map((account) => account.id);
|
|
187
|
+
};
|
|
188
|
+
exports.invoicingQboAccountIdsForMappingSlot = invoicingQboAccountIdsForMappingSlot;
|
|
189
|
+
/**
|
|
190
|
+
* Stable empties for a tenant with no QBO connection.
|
|
191
|
+
*
|
|
192
|
+
* `?? []` builds a fresh array per call, and the invoicing forms re-seed their
|
|
193
|
+
* fields whenever their option lists change identity — so a form open against a
|
|
194
|
+
* disconnected tenant would reset itself on every unrelated store change, until
|
|
195
|
+
* the first edit disarms the re-seed.
|
|
196
|
+
*/
|
|
197
|
+
exports.NO_INVOICING_ACCOUNT_HIERARCHY = [];
|
|
198
|
+
exports.NO_INVOICING_CLASS_HIERARCHY = [];
|
|
199
|
+
exports.NO_INVOICING_ACCOUNT_IDS = [];
|
|
151
200
|
/** Classes as the invoicing pickers need them, from the shared class list. */
|
|
152
201
|
const invoicingAccountingClassOptions = (state) => {
|
|
153
202
|
const { classes } = (0, classListSelector_1.getClassList)(state.classState, state.classListState);
|
|
@@ -169,7 +218,7 @@ const getInvoicingQboView = (state) => {
|
|
|
169
218
|
const qboState = state.invoicingQboViewState;
|
|
170
219
|
const accountList = state.accountListState.byReportId[exports.INVOICING_ACCOUNT_LIST_KEY];
|
|
171
220
|
return {
|
|
172
|
-
accounts:
|
|
221
|
+
accounts: (0, exports.getInvoicingQboAccountOptions)(state),
|
|
173
222
|
accountsFetchState: {
|
|
174
223
|
fetchState: accountList.fetchState,
|
|
175
224
|
error: accountList.error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.53",
|
|
4
4
|
"description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/esm/index.js",
|