@zeniai/client-epic-state 5.2.7 → 5.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/lib/entity/invoicing/invoicingCommonPayload.d.ts +22 -0
  2. package/lib/entity/invoicing/invoicingCommonPayload.js +51 -10
  3. package/lib/esm/entity/invoicing/invoicingCommonPayload.js +51 -10
  4. package/lib/esm/index.js +3 -2
  5. package/lib/esm/view/accountList/accountListReducer.js +0 -1
  6. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/backgroundRefetchReviewTabEpic.js +1 -0
  7. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +1 -0
  8. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +56 -11
  9. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +2 -1
  10. package/lib/esm/view/expenseAutomationView/types/transactionGroup.js +21 -0
  11. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +2 -1
  12. package/lib/esm/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +2 -0
  13. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +10 -5
  14. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +27 -3
  15. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +7 -3
  16. package/lib/index.d.ts +3 -2
  17. package/lib/index.js +52 -47
  18. package/lib/view/accountList/accountListReducer.js +0 -1
  19. package/lib/view/accountList/accountListState.d.ts +1 -1
  20. package/lib/view/expenseAutomationView/epics/transactionCategorization/backgroundRefetchReviewTabEpic.js +1 -0
  21. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +1 -0
  22. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +11 -0
  23. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +4 -1
  24. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +57 -12
  25. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +2 -0
  26. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +2 -1
  27. package/lib/view/expenseAutomationView/types/transactionGroup.d.ts +21 -0
  28. package/lib/view/expenseAutomationView/types/transactionGroup.js +25 -0
  29. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +11 -0
  30. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +5 -0
  31. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +1 -0
  32. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.d.ts +1 -0
  33. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +2 -0
  34. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +10 -5
  35. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +9 -1
  36. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +30 -4
  37. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +11 -4
  38. package/lib/view/invoicing/settingsView/settingsViewSelector.js +7 -3
  39. package/package.json +1 -1
@@ -88,12 +88,34 @@ export interface InvoicingStripeAccountPayload {
88
88
  export declare const INVOICING_QBO_CUSTOMER_MATCHING_MODES: readonly ["link_existing", "always_create"];
89
89
  export type InvoicingQboCustomerMatchingMode = (typeof INVOICING_QBO_CUSTOMER_MATCHING_MODES)[number];
90
90
  export declare function toInvoicingQboCustomerMatchingMode(value: string): InvoicingQboCustomerMatchingMode;
91
+ /**
92
+ * A mapped account as the newer settings payload returns it: the platform id,
93
+ * plus hydrated fields the client only displays.
94
+ */
95
+ export interface InvoicingQboMappedAccountPayload {
96
+ account_id?: string | null;
97
+ account_name?: string | null;
98
+ fully_qualified_name?: string | null;
99
+ qbo_id?: string | null;
100
+ }
101
+ /**
102
+ * Account mappings arrive in two shapes. The original sends a bare id per slot
103
+ * (`deposit_account_id`); the newer one sends an object holding the platform id
104
+ * (`deposit_account: {account_id}`). Both are declared because a tenant can be
105
+ * served either depending on when its backend rolled, and a page that understands
106
+ * only one renders an empty picker over data that exists.
107
+ */
91
108
  export interface InvoicingQboAccountMappingsPayload {
109
+ default_income_account?: InvoicingQboMappedAccountPayload | null;
92
110
  default_income_account_id?: string | null;
111
+ deposit_account?: InvoicingQboMappedAccountPayload | null;
93
112
  deposit_account_id?: string | null;
94
113
  deposit_account_overrides?: Record<string, string> | null;
114
+ discounts_account?: InvoicingQboMappedAccountPayload | null;
95
115
  discounts_account_id?: string | null;
116
+ rounding_account?: InvoicingQboMappedAccountPayload | null;
96
117
  rounding_account_id?: string | null;
118
+ write_off_account?: InvoicingQboMappedAccountPayload | null;
97
119
  write_off_account_id?: string | null;
98
120
  }
99
121
  /**
@@ -153,6 +153,23 @@ function toInvoicingQboCustomerMatchingMode(value) {
153
153
  throw new Error(`Unknown InvoicingQboCustomerMatchingMode: ${value}`);
154
154
  }
155
155
  }
156
+ /**
157
+ * The id for one mapping slot, from whichever shape the backend sent. The object
158
+ * form wins when present; a blank id in either is treated as unset so the picker
159
+ * shows its placeholder rather than selecting nothing.
160
+ */
161
+ function readInvoicingQboMappedAccountId(mappedAccount, legacyAccountId) {
162
+ // `qbo_id` first: the pickers key their options on the QuickBooks id, so an
163
+ // internal `account_id` would not select anything even when it is the same
164
+ // account. Both are read because the mapping arrives either way depending on
165
+ // how it was saved.
166
+ const candidates = [
167
+ mappedAccount?.qbo_id,
168
+ mappedAccount?.account_id,
169
+ legacyAccountId,
170
+ ];
171
+ return candidates.find((candidate) => candidate != null && candidate !== '');
172
+ }
156
173
  /**
157
174
  * Read one account-mapping dropdown value by its config `code` (the codes in
158
175
  * `qbo_account_mapping_kind_options` match the `account_mappings` field names).
@@ -160,15 +177,15 @@ function toInvoicingQboCustomerMatchingMode(value) {
160
177
  function getInvoicingQboAccountMappingId(accountMappings, code) {
161
178
  switch (code) {
162
179
  case 'deposit_account_id':
163
- return accountMappings?.deposit_account_id ?? undefined;
180
+ return readInvoicingQboMappedAccountId(accountMappings?.deposit_account, accountMappings?.deposit_account_id);
164
181
  case 'default_income_account_id':
165
- return accountMappings?.default_income_account_id ?? undefined;
182
+ return readInvoicingQboMappedAccountId(accountMappings?.default_income_account, accountMappings?.default_income_account_id);
166
183
  case 'discounts_account_id':
167
- return accountMappings?.discounts_account_id ?? undefined;
184
+ return readInvoicingQboMappedAccountId(accountMappings?.discounts_account, accountMappings?.discounts_account_id);
168
185
  case 'rounding_account_id':
169
- return accountMappings?.rounding_account_id ?? undefined;
186
+ return readInvoicingQboMappedAccountId(accountMappings?.rounding_account, accountMappings?.rounding_account_id);
170
187
  case 'write_off_account_id':
171
- return accountMappings?.write_off_account_id ?? undefined;
188
+ return readInvoicingQboMappedAccountId(accountMappings?.write_off_account, accountMappings?.write_off_account_id);
172
189
  default:
173
190
  return undefined;
174
191
  }
@@ -178,18 +195,42 @@ function getInvoicingQboAccountMappingId(accountMappings, code) {
178
195
  * `code` set to `accountId`. Unknown codes leave the mappings unchanged.
179
196
  */
180
197
  function withInvoicingQboAccountMappingId(accountMappings, code, accountId) {
198
+ // Both slots are written together. Setting only the bare id would leave the
199
+ // object shape behind, and the reader prefers it — so the selection just made
200
+ // would read back as whatever was mapped before. The object is dropped rather
201
+ // than rebuilt: only the id is known here, not the name that came with it.
181
202
  const base = accountMappings ?? {};
182
203
  switch (code) {
183
204
  case 'deposit_account_id':
184
- return { ...base, deposit_account_id: accountId };
205
+ return {
206
+ ...base,
207
+ deposit_account: undefined,
208
+ deposit_account_id: accountId,
209
+ };
185
210
  case 'default_income_account_id':
186
- return { ...base, default_income_account_id: accountId };
211
+ return {
212
+ ...base,
213
+ default_income_account: undefined,
214
+ default_income_account_id: accountId,
215
+ };
187
216
  case 'discounts_account_id':
188
- return { ...base, discounts_account_id: accountId };
217
+ return {
218
+ ...base,
219
+ discounts_account: undefined,
220
+ discounts_account_id: accountId,
221
+ };
189
222
  case 'rounding_account_id':
190
- return { ...base, rounding_account_id: accountId };
223
+ return {
224
+ ...base,
225
+ rounding_account: undefined,
226
+ rounding_account_id: accountId,
227
+ };
191
228
  case 'write_off_account_id':
192
- return { ...base, write_off_account_id: accountId };
229
+ return {
230
+ ...base,
231
+ write_off_account: undefined,
232
+ write_off_account_id: accountId,
233
+ };
193
234
  default:
194
235
  return base;
195
236
  }
@@ -128,6 +128,23 @@ export function toInvoicingQboCustomerMatchingMode(value) {
128
128
  throw new Error(`Unknown InvoicingQboCustomerMatchingMode: ${value}`);
129
129
  }
130
130
  }
131
+ /**
132
+ * The id for one mapping slot, from whichever shape the backend sent. The object
133
+ * form wins when present; a blank id in either is treated as unset so the picker
134
+ * shows its placeholder rather than selecting nothing.
135
+ */
136
+ function readInvoicingQboMappedAccountId(mappedAccount, legacyAccountId) {
137
+ // `qbo_id` first: the pickers key their options on the QuickBooks id, so an
138
+ // internal `account_id` would not select anything even when it is the same
139
+ // account. Both are read because the mapping arrives either way depending on
140
+ // how it was saved.
141
+ const candidates = [
142
+ mappedAccount?.qbo_id,
143
+ mappedAccount?.account_id,
144
+ legacyAccountId,
145
+ ];
146
+ return candidates.find((candidate) => candidate != null && candidate !== '');
147
+ }
131
148
  /**
132
149
  * Read one account-mapping dropdown value by its config `code` (the codes in
133
150
  * `qbo_account_mapping_kind_options` match the `account_mappings` field names).
@@ -135,15 +152,15 @@ export function toInvoicingQboCustomerMatchingMode(value) {
135
152
  export function getInvoicingQboAccountMappingId(accountMappings, code) {
136
153
  switch (code) {
137
154
  case 'deposit_account_id':
138
- return accountMappings?.deposit_account_id ?? undefined;
155
+ return readInvoicingQboMappedAccountId(accountMappings?.deposit_account, accountMappings?.deposit_account_id);
139
156
  case 'default_income_account_id':
140
- return accountMappings?.default_income_account_id ?? undefined;
157
+ return readInvoicingQboMappedAccountId(accountMappings?.default_income_account, accountMappings?.default_income_account_id);
141
158
  case 'discounts_account_id':
142
- return accountMappings?.discounts_account_id ?? undefined;
159
+ return readInvoicingQboMappedAccountId(accountMappings?.discounts_account, accountMappings?.discounts_account_id);
143
160
  case 'rounding_account_id':
144
- return accountMappings?.rounding_account_id ?? undefined;
161
+ return readInvoicingQboMappedAccountId(accountMappings?.rounding_account, accountMappings?.rounding_account_id);
145
162
  case 'write_off_account_id':
146
- return accountMappings?.write_off_account_id ?? undefined;
163
+ return readInvoicingQboMappedAccountId(accountMappings?.write_off_account, accountMappings?.write_off_account_id);
147
164
  default:
148
165
  return undefined;
149
166
  }
@@ -153,18 +170,42 @@ export function getInvoicingQboAccountMappingId(accountMappings, code) {
153
170
  * `code` set to `accountId`. Unknown codes leave the mappings unchanged.
154
171
  */
155
172
  export function withInvoicingQboAccountMappingId(accountMappings, code, accountId) {
173
+ // Both slots are written together. Setting only the bare id would leave the
174
+ // object shape behind, and the reader prefers it — so the selection just made
175
+ // would read back as whatever was mapped before. The object is dropped rather
176
+ // than rebuilt: only the id is known here, not the name that came with it.
156
177
  const base = accountMappings ?? {};
157
178
  switch (code) {
158
179
  case 'deposit_account_id':
159
- return { ...base, deposit_account_id: accountId };
180
+ return {
181
+ ...base,
182
+ deposit_account: undefined,
183
+ deposit_account_id: accountId,
184
+ };
160
185
  case 'default_income_account_id':
161
- return { ...base, default_income_account_id: accountId };
186
+ return {
187
+ ...base,
188
+ default_income_account: undefined,
189
+ default_income_account_id: accountId,
190
+ };
162
191
  case 'discounts_account_id':
163
- return { ...base, discounts_account_id: accountId };
192
+ return {
193
+ ...base,
194
+ discounts_account: undefined,
195
+ discounts_account_id: accountId,
196
+ };
164
197
  case 'rounding_account_id':
165
- return { ...base, rounding_account_id: accountId };
198
+ return {
199
+ ...base,
200
+ rounding_account: undefined,
201
+ rounding_account_id: accountId,
202
+ };
166
203
  case 'write_off_account_id':
167
- return { ...base, write_off_account_id: accountId };
204
+ return {
205
+ ...base,
206
+ write_off_account: undefined,
207
+ write_off_account_id: accountId,
208
+ };
168
209
  default:
169
210
  return base;
170
211
  }
package/lib/esm/index.js CHANGED
@@ -187,13 +187,14 @@ import { clearExpenseAutomationFluxAnalysisView, fetchFluxAnalysisView, reviewFl
187
187
  import { clearJeScheduleLocalData as clearExpenseAutomationJEScheduleLocalData, clearExpenseAutomationJESchedulesView, fetchJeSchedulesPage as fetchExpenseAutomationJESchedulesPage, ignoreRecommendedJeSchedule as ignoreExpenseAutomationJESchedule, initializeAccountSettingsView as initializeJeAccountSettingsView, initializeJeScheduleLocalData, removeJeScheduleTransactionKey, retryJeSchedule as retryExpenseAutomationJESchedule, saveAccountSettings as saveJeAccountSettings, saveAccountSettingsLocalData as saveJeAccountSettingsLocalData, updateJESchedulesUIState as updateExpenseAutomationJESchedulesUIState, updateJeScheduleLocalDataById, updateJeScheduleTransactionKeys, } from './view/expenseAutomationView/reducers/jeSchedulesViewReducer';
188
188
  import { acknowledgeBulkUploadConfirmMatchComplete, bulkUploadAutomatchingTimedOut, bulkUploadReceipts, bulkUploadReceiptsFailure, bulkUploadReceiptsSuccess, clearBulkUpload, clearBulkUploadBatchDetailsForScopeChange, clearManualSearchResults, clearMissingReceiptsTabNavigation, confirmBulkUploadMatch, confirmBulkUploadMatchFailure, confirmBulkUploadMatchSuccess, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsFailure, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, fetchBulkUploadBatchesFailure, fetchBulkUploadBatchesSuccess, fetchCompletedTransactions, fetchCompletedTransactionsFailure, fetchCompletedTransactionsSuccess, fetchMissingReceipts as fetchExpenseAutomationMissingReceipts, fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, markBatchDetailRefreshAttempted, markMissingReceiptAsDone as markExpenseAutomationMissingReceiptAsDone, pusherBatchStatusUpdate, refreshBatchDetailsForBatchId, requestMissingReceiptsTabNavigation, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, searchTransactionsForManualMatch, searchTransactionsForManualMatchFailure, searchTransactionsForManualMatchSuccess, setBulkUploadCompletedSubTab, setBulkUploadResultsTab, setBulkUploadSortConfig, setBulkUploadUploadedFileCount, storeBatchDetails, updateBulkUploadProgress, updateMissingReceiptUploadState as updateExpenseAutomationMissingReceiptUploadState, updateMissingReceiptsUIState as updateExpenseAutomationMissingReceiptsUIState, uploadMissingReceiptSuccess as uploadExpenseAutomationMissingReceiptSuccess, } from './view/expenseAutomationView/reducers/missingReceiptsViewReducer';
189
189
  import { clearStatementDateConflict, confirmStatementMapping, deleteAccountStatement, excludeAccountFromReconciliation, fetchReconciliation as fetchReconciliationView, includeAccountInReconciliation, parseCombinedStatement, parseStatement, parseStatementFailure, parseStatementSuccess, reparseStatement, reparseStatementFailure, reparseStatementSuccess, resetReparseStatementStatus, saveReconciliationDetail as saveExpenseAutomationReconciliationDetail, saveReconciliationReview as saveExpenseAutomationReconciliationReview, setConnectionInProgressForAccount as setConnectionInProgressForAccountReconciliation, setStatementParseInProgress, setStatementProcessingDismissedToBackground, submitCombinedStatementUpdate, submitStatementUpdate, updateAccountReconciliationLocalData as updateExpenseAutomationAccountReconciliationLocalData, updateSelectedAccountId as updateExpenseAutomationAccountReconciliationSelectedAccountId, updateSelectedTab as updateExpenseAutomationAccountReconciliationSelectedTab, updateReconListScrollPosition as updateExpenseAutomationReconListScrollPosition, updateReviewTabSortState as updateExpenseAutomationReconReviewTabListSortState, updateReviewTabLocalData as updateExpenseAutomationReconReviewTabLocalData, updateReconcileTabListScrollState as updateExpenseAutomationReconcileTabListScrollState, updateReconcileTabListSortState as updateExpenseAutomationReconcileTabListSortState, updateReconcileTabLocalData as updateExpenseAutomationReconcileTabLocalData, updateSelectedDrawerAccountId as updateExpenseAutomationSelectedDrawerAccountId, updateNodeCollapseState, updateParsedStatementData, updateStatementProcessingBackgroundCompletedSteps, updateStatementProcessingFailed, updateStatementUpdateLocalData, updateStatementUploadChosen, uploadAccountStatement, } from './view/expenseAutomationView/reducers/reconciliationViewReducer';
190
- import { backgroundRefetchReviewTab, clearExpenseAutomationTransactionsView, fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationView, initializeTransactionCategorizationViewLocalData, markCategoryClassRecommendationsFailureForCategorization, markTransactionAsNotMiscategorized, removeTransactionFromAllTabs, saveTransactionCategorization, saveTransactionCategorizationLocalData, setAllItemsToCategoryClassInLocalDataForCategorization, setEntityRecommendationForLineIdsForCategorization, syncTransactionCategorizationFromDetailSave, updateCurrentSelectedTransactionCategorizationTab, updateSelectedCheckboxTransactionIds, updateSelectedCustomerForTransaction, updateSelectedTransactionId, updateSelectedVendorForTransaction, updateTransactionCategorization, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, updateTransactionCategorizationUIState, updateTransactionCategorizationUploadReceiptState, updateTransactionFilters, uploadTransactionCategorizationReceiptSuccess, } from './view/expenseAutomationView/reducers/transactionsViewReducer';
190
+ import { backgroundRefetchReviewTab, clearExpenseAutomationTransactionsView, fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationView, initializeTransactionCategorizationViewLocalData, markCategoryClassRecommendationsFailureForCategorization, markTransactionAsNotMiscategorized, removeTransactionFromAllTabs, saveTransactionCategorization, saveTransactionCategorizationLocalData, setAllItemsToCategoryClassInLocalDataForCategorization, setEntityRecommendationForLineIdsForCategorization, syncTransactionCategorizationFromDetailSave, updateCurrentSelectedTransactionCategorizationTab, updateSelectedCheckboxTransactionIds, updateSelectedCustomerForTransaction, updateSelectedTransactionId, updateSelectedVendorForTransaction, updateTransactionCategorization, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, updateTransactionCategorizationUIState, updateTransactionCategorizationUploadReceiptState, updateTransactionFilters, updateTransactionGroup, uploadTransactionCategorizationReceiptSuccess, } from './view/expenseAutomationView/reducers/transactionsViewReducer';
191
191
  import { getExpenseAutomationFluxAnalysisView } from './view/expenseAutomationView/selectors/fluxAnalysisViewSelector';
192
192
  import { getExpenseAutomationReconciliationView, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, isAccountReconReport, } from './view/expenseAutomationView/selectors/reconciliationViewSelector';
193
193
  import { getExpenseAutomationTransactionView, getLastTransferEntryReplacement, } from './view/expenseAutomationView/selectors/transactionCategorizationSelector';
194
194
  import { applyTransactionFilters } from './view/expenseAutomationView/transactionFilterHelpers';
195
195
  import { TRANSACTION_FILTER_CATEGORIES, } from './view/expenseAutomationView/transactionFilterTypes';
196
196
  import { DEFAULT_COMPLETED_SUB_TAB, toCompletedSubTab, } from './view/expenseAutomationView/types/completedSubTab';
197
+ import { DEFAULT_TRANSACTION_GROUP, TRANSACTION_GROUPS, toTransactionGroup, } from './view/expenseAutomationView/types/transactionGroup';
197
198
  import { toJEScheduleMainTab as toExpenseAutomationJEScheduleMainTab, toJEScheduleSortKey as toExpenseAutomationJEScheduleSortKey, } from './view/expenseAutomationView/types/jeSchedulesViewState';
198
199
  import { BATCH_FILE_STATUSES, isUnmatchedTabFileStatus, toBatchFileStatus, toBatchStatusValue, toBulkUploadPhase, toBulkUploadResultsTab, toBulkUploadSortKey, toMatchSource, toMissingReceiptsTab, } from './view/expenseAutomationView/types/missingReceiptsViewState';
199
200
  import { toMissingReceiptsSortKey as toExpenseAutomationMissingReceiptsSortKey, } from './view/expenseAutomationView/types/missingReceiptsViewState';
@@ -482,7 +483,7 @@ export { stringToUnion, stringToUnionStrict };
482
483
  export { fetchMonthEndCloseChecks, fetchMonthClosePerformanceTrend, getMonthEndCloseChecksViewByTenantId, ALL_MONTH_END_CLOSE_CHECKS_FREQUENCY, };
483
484
  export {
484
485
  // Bulk Upload Types
485
- BATCH_FILE_STATUSES, isUnmatchedTabFileStatus, DEFAULT_COMPLETED_SUB_TAB, toBatchFileStatus, toBatchStatusValue, toBulkUploadPhase, toBulkUploadResultsTab, toBulkUploadSortKey, toCompletedSubTab, toMatchSource, toMissingReceiptsTab, toExpenseAutomationJEScheduleMainTab, toExpenseAutomationJEScheduleSortKey, getExpenseAutomationView, toExpenseAutomationMissingReceiptsSortKey, toExpenseAutomationTransactionsTabKey, toExpenseAutomationViewType, uploadExpenseAutomationMissingReceiptSuccess, markExpenseAutomationMissingReceiptAsDone, fetchAllExpenseAutomationTabs, refreshExpenseAutomationCurrentTab, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, fetchExpenseAutomationMissingReceipts,
486
+ BATCH_FILE_STATUSES, isUnmatchedTabFileStatus, DEFAULT_COMPLETED_SUB_TAB, DEFAULT_TRANSACTION_GROUP, TRANSACTION_GROUPS, toTransactionGroup, toBatchFileStatus, toBatchStatusValue, toBulkUploadPhase, toBulkUploadResultsTab, toBulkUploadSortKey, toCompletedSubTab, toMatchSource, toMissingReceiptsTab, toExpenseAutomationJEScheduleMainTab, toExpenseAutomationJEScheduleSortKey, getExpenseAutomationView, toExpenseAutomationMissingReceiptsSortKey, toExpenseAutomationTransactionsTabKey, toExpenseAutomationViewType, uploadExpenseAutomationMissingReceiptSuccess, markExpenseAutomationMissingReceiptAsDone, fetchAllExpenseAutomationTabs, refreshExpenseAutomationCurrentTab, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionGroup, fetchExpenseAutomationMissingReceipts,
486
487
  // Bulk Upload Actions
487
488
  bulkUploadReceipts, bulkUploadAutomatchingTimedOut, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, restoreBulkUploadAutomatchingOnMount, restoreBulkUploadMatchingState, updateBulkUploadProgress, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, clearMissingReceiptsTabNavigation, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, storeBatchDetails, fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, clearBulkUploadBatchDetailsForScopeChange, markBatchDetailRefreshAttempted, refreshBatchDetailsForBatchId, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, setBulkUploadUploadedFileCount, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults, acknowledgeBulkUploadConfirmMatchComplete, fetchCompletedTransactions, fetchCompletedTransactionsSuccess, fetchCompletedTransactionsFailure, fetchFluxAnalysisView, clearExpenseAutomationFluxAnalysisView, updateOperatingExpensesIdsForReview as updateFluxOperatingExpensesIdsForReview, updateSelectedSectionIdsForReview as updateFluxAnalysisSelectedSectionIdsForReview, reviewFluxAnalysisView, updateExpenseAutomationMissingReceiptUploadState, updateExpenseAutomationMissingReceiptsUIState, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, getExpenseAutomationFluxAnalysisView, updateCurrentSelectedView, updateCurrentSelectedPeriod, getExpenseAutomationTransactionView, getLastTransferEntryReplacement, updateFluxAnalysisViewUIState, updateFluxAnalysisViewPageMetaData, MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, getLineItemsByTransactionIdsFromLocalData, isAnyItemWithUncategorizedExpenseAccount, excludeAccountFromReconciliation, includeAccountInReconciliation, saveExpenseAutomationReconciliationDetail, updateExpenseAutomationReconcileTabListScrollState, updateExpenseAutomationReconReviewTabListSortState, updateExpenseAutomationReconcileTabListSortState, updateExpenseAutomationReconcileTabLocalData, updateExpenseAutomationAccountReconciliationSelectedTab, updateExpenseAutomationAccountReconciliationSelectedAccountId, getExpenseAutomationReconciliationView, getLedgerAccountsForCombinedStatementMapping, getReparseStatementStatusByAccountId, getUnavailableLedgerAccountIds, fetchReconciliationView, uploadAccountStatementIntoDocumentAI, updateExpenseAutomationReconListScrollPosition, setConnectionInProgressForAccountReconciliation, getAccountReconByAccountIdAndSelectedPeriod, toReconciliationTabsType, isAccountReconReport, updateExpenseAutomationReconReviewTabLocalData, updateExpenseAutomationSelectedDrawerAccountId, updateStatementProcessingFailed, saveExpenseAutomationReconciliationReview, updateExpenseAutomationAccountReconciliationLocalData, toReconciliationAccountSource, deleteAccountStatement, uploadAccountStatement, parseCombinedStatement, parseStatement, parseStatementSuccess, parseStatementFailure, reparseStatement, reparseStatementSuccess, reparseStatementFailure, resetReparseStatementStatus, updateParsedStatementData, updateNodeCollapseState, updateStatementUploadChosen, submitCombinedStatementUpdate, submitStatementUpdate, clearStatementDateConflict, confirmStatementMapping, updateStatementUpdateLocalData, isReviewTransactionBankTransferType, isReviewTransactionBillPaymentType, isReviewTransactionCreditCardPaymentType, isReviewTransactionDepositType, isReviewTransactionExpenseType, isReviewTransactionCreditCardCreditType, setStatementParseInProgress, setStatementProcessingDismissedToBackground, updateStatementProcessingBackgroundCompletedSteps, };
488
489
  export { fetchTransactionCategorization, fetchTransactionCategorizationView, updateTransactionCategorizationUIState, updateTransactionFilters, updateSelectedCheckboxTransactionIds, markCategoryClassRecommendationsFailureForCategorization, setEntityRecommendationForLineIdsForCategorization, initializeTransactionCategorizationViewLocalData, setAllItemsToCategoryClassInLocalDataForCategorization, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, createTransferEntry, createTransferEntryFailure, createTransferEntryReplacedTransaction, createTransferEntrySuccess, resetCreateTransferEntryStatus, clearTransferEntryRouteReplacement, fetchAccountsForTransferFlow, removeTransactionFromAllTabs, clearExpenseAutomationTransactionsView, toTransactionsSortKey, };
@@ -28,7 +28,6 @@ export const initialState = {
28
28
  reimbursementAccountList: initialAccountList,
29
29
  schedulesAccountList: initialAccountList,
30
30
  billPayAccountList: initialAccountList,
31
- invoicingAccountList: initialAccountList,
32
31
  },
33
32
  };
34
33
  const accountList = createSlice({
@@ -27,6 +27,7 @@ export const backgroundRefetchReviewTabEpic = (actions$, state$, zeniAPI) => act
27
27
  page_token: pageToken,
28
28
  page_size: PAGE_SIZE,
29
29
  search_text: reviewUiState.searchString,
30
+ transaction_group: state.expenseAutomationTransactionsViewState.selectedTransactionGroup,
30
31
  };
31
32
  return zeniAPI.getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/transactions?query=${encodeURIComponent(JSON.stringify(queryParam))}`);
32
33
  };
@@ -36,6 +36,7 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
36
36
  sub_tab: selectedTab === 'autoCategorized'
37
37
  ? expenseAutomationTransactionsViewState.selectedTransactionCategorizationCompletedSubTab
38
38
  : DEFAULT_COMPLETED_SUB_TAB,
39
+ transaction_group: expenseAutomationTransactionsViewState.selectedTransactionGroup,
39
40
  };
40
41
  return zeniAPI
41
42
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/expense-automation/transactions?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
@@ -6,7 +6,18 @@ import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vend
6
6
  import { clearAll } from '../../../rootActions';
7
7
  import { filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, shouldAutoSelectAndAdd, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
8
8
  import { DEFAULT_COMPLETED_SUB_TAB, } from '../types/completedSubTab';
9
+ import { DEFAULT_TRANSACTION_GROUP, } from '../types/transactionGroup';
9
10
  import { TRANSACTIONS_TABS, } from '../types/transactionsViewState';
11
+ export const initialUIState = {
12
+ sortKey: 'date',
13
+ sortOrder: 'descending',
14
+ searchString: '',
15
+ nodeCollapseState: {},
16
+ scrollPosition: { scrollTop: 0, scrollLeft: undefined },
17
+ pageToken: null,
18
+ totalCount: {},
19
+ limit: 10,
20
+ };
10
21
  export const initialTransactionTabViewState = {
11
22
  saveStatus: {
12
23
  fetchState: 'Not-Started',
@@ -20,16 +31,7 @@ export const initialTransactionTabViewState = {
20
31
  fetchState: 'Not-Started',
21
32
  error: undefined,
22
33
  },
23
- uiState: {
24
- sortKey: 'date',
25
- sortOrder: 'descending',
26
- searchString: '',
27
- nodeCollapseState: {},
28
- scrollPosition: { scrollTop: 0, scrollLeft: undefined },
29
- pageToken: null,
30
- totalCount: {},
31
- limit: 10,
32
- },
34
+ uiState: { ...initialUIState },
33
35
  filters: {
34
36
  categoryCombinationOperator: 'AND',
35
37
  categories: [],
@@ -55,6 +57,7 @@ export const initialState = {
55
57
  },
56
58
  selectedTransactionCategorizationTab: 'review',
57
59
  selectedTransactionCategorizationCompletedSubTab: DEFAULT_COMPLETED_SUB_TAB,
60
+ selectedTransactionGroup: DEFAULT_TRANSACTION_GROUP,
58
61
  transactionCategorizationView: {
59
62
  review: { ...initialTransactionTabViewState },
60
63
  autoCategorized: { ...initialTransactionTabViewState },
@@ -400,6 +403,48 @@ const expenseAutomationTransactionsView = createSlice({
400
403
  return { payload: { newSubTab } };
401
404
  },
402
405
  },
406
+ updateTransactionGroup: {
407
+ reducer(draft, action) {
408
+ const { newGroup } = action.payload;
409
+ if (draft.selectedTransactionGroup === newGroup) {
410
+ return;
411
+ }
412
+ draft.selectedTransactionGroup = newGroup;
413
+ // A group change re-scopes the entire dataset on the backend (by
414
+ // account_type), so every parent tab, sub-tab snapshot and count is now
415
+ // stale. Drop the sub-tab cache and parent counts, and reset each tab's
416
+ // paginated data slots so the view epic's cache check falls through to a
417
+ // fresh page-1 fetch. User preferences (search, sort, limit, collapse
418
+ // state) are intentionally preserved, mirroring the sub-tab switch.
419
+ draft.autoCategorizedSubTabCache = {};
420
+ draft.parentTotalCountByTab = { review: {}, autoCategorized: {} };
421
+ TRANSACTIONS_TABS.forEach((tab) => {
422
+ const view = draft.transactionCategorizationView[tab];
423
+ // Reset uiState to its initial values but keep the user prefs (search,
424
+ // sort, limit, collapse) in place -- mirroring the sub-tab switch. Then
425
+ // drop the loaded rows and any stale selection pointing at a now-absent
426
+ // row to avoid transient invalid state.
427
+ const { searchString, sortKey, sortOrder, nodeCollapseState, limit } = view.uiState;
428
+ view.uiState = {
429
+ ...initialUIState,
430
+ searchString,
431
+ sortKey,
432
+ sortOrder,
433
+ nodeCollapseState,
434
+ limit,
435
+ };
436
+ view.transactionIdsBySelectedPeriod = {};
437
+ view.fetchState = 'Not-Started';
438
+ view.error = undefined;
439
+ view.selectedTransactionId = undefined;
440
+ view.selectedTransactionLineId = undefined;
441
+ view.selectedCheckBoxTransactionIds = [];
442
+ });
443
+ },
444
+ prepare(newGroup) {
445
+ return { payload: { newGroup } };
446
+ },
447
+ },
403
448
  updateTransactionCategorization: {
404
449
  reducer(draft, action) {
405
450
  const selectedTab = action.payload.selectedTab;
@@ -1075,5 +1120,5 @@ const expenseAutomationTransactionsView = createSlice({
1075
1120
  });
1076
1121
  },
1077
1122
  });
1078
- export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, markCategoryClassRecommendationsInProgressForCategorization, markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
1123
+ export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionGroup, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, markCategoryClassRecommendationsInProgressForCategorization, markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
1079
1124
  export default expenseAutomationTransactionsView.reducer;
@@ -86,7 +86,7 @@ export const toTransactionView = (transaction, transactionLocalData) => {
86
86
  export { getLastTransferEntryReplacement } from '../../createTransferEntry/createTransferEntrySelector';
87
87
  export function getExpenseAutomationTransactionView(state) {
88
88
  const { accountState, classState, classListState, accountListState, createTransferEntryState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, } = state;
89
- const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
89
+ const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, selectedTransactionGroup, } = expenseAutomationTransactionsViewState;
90
90
  const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, filters, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
91
91
  const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
92
92
  const accountList = getAccountList(accountState, accountListState, 'accountList');
@@ -240,5 +240,6 @@ export function getExpenseAutomationTransactionView(state) {
240
240
  creditCardTransferAccountsList,
241
241
  transferAccounts: transferAccountsList,
242
242
  selectedTransactionCategorizationCompletedSubTab,
243
+ selectedTransactionGroup,
243
244
  };
244
245
  }
@@ -0,0 +1,21 @@
1
+ import { stringToUnion } from '../../../commonStateTypes/stringToUnion';
2
+ /**
3
+ * Statement-level grouping for the Transaction Categorization listing, forwarded
4
+ * verbatim to the listing API as `transaction_group`. When revenue automation is
5
+ * enabled for the tenant the backend scopes the whole view by the line
6
+ * account_type: `'expense'` -> expense accounts, `'income'` -> income accounts,
7
+ * `'all'` -> both. When off, every value falls back to the legacy expense scope
8
+ * on the backend, so the group is only sent when the gate is on.
9
+ *
10
+ * The wire value for expenses is the singular `'expense'` (matches the backend
11
+ * `TransactionGroup` enum) even though the picker labels it "Expenses".
12
+ * Balance-sheet groups (assets, liability) are reserved for later and are not
13
+ * part of this union yet.
14
+ */
15
+ export const TRANSACTION_GROUPS = ['all', 'income', 'expense'];
16
+ export const toTransactionGroup = (v) => stringToUnion(v != null ? v.trim().toLowerCase() : '', TRANSACTION_GROUPS);
17
+ /**
18
+ * Default group for the listing — matches the picker's default selection ("All").
19
+ * Single source of truth for initial slice state and any fall-back call site.
20
+ */
21
+ export const DEFAULT_TRANSACTION_GROUP = 'all';
@@ -1,7 +1,7 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
3
3
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
4
- import { INVOICING_INCOME_ACCOUNT_TYPES, getInvoicingQboView, invoicingAccountHierarchyForTypes, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
4
+ import { INVOICING_INCOME_ACCOUNT_TYPES, getInvoicingQboView, invoicingAccountHierarchyForTypes, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
5
5
  import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, entityToInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormConfig';
6
6
  export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
7
7
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -53,6 +53,7 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
53
53
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = getInvoicingConfigView(state);
54
54
  return {
55
55
  fieldHelp: invoicingFieldHelpForGroup(state, 'catalog'),
56
+ isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
56
57
  qboClassHierarchy: isQboConnected
57
58
  ? getInvoicingQboView(state).nestedClassHierarchy
58
59
  : [],
@@ -1,3 +1,4 @@
1
+ import { isInvoicingQboConnected } from '../invoicingQboView/invoicingQboViewSelector';
1
2
  import { deriveInvoicingOnboardingStatus, } from './invoicingConfigViewPayload';
2
3
  export const getInvoicingConfigView = (state) => {
3
4
  const { acceptTermsFetchState, enableFetchState, ...configState } = state.invoicingConfigViewState;
@@ -14,6 +15,7 @@ export const getInvoicingOnboardingGateView = (state) => {
14
15
  acceptTermsState: acceptTermsFetchState,
15
16
  enableState: enableFetchState,
16
17
  fetchState,
18
+ isQboConnected: isInvoicingQboConnected(state),
17
19
  status: deriveInvoicingOnboardingStatus(state.invoicingConfigViewState.isInvoicingTosAccepted, state.invoicingConfigViewState.isInvoicingEnabled),
18
20
  };
19
21
  };
@@ -9,12 +9,17 @@ import { INVOICING_ACCOUNT_LIST_KEY } from '../invoicingQboViewSelector';
9
9
  export const fetchInvoicingAccountingPickersEpic = (actions$, state$) => actions$.pipe(filter(fetchInvoicingAccountingPickers.match), withLatestFrom(state$), mergeMap(([, state]) => {
10
10
  const actions = [];
11
11
  const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
12
- if (accountList.hasValidState() === false &&
12
+ // `hasValidState` only reports that a fetch finished, not that it
13
+ // returned anything usable. The shared bucket can be Completed and empty
14
+ // because a screen that needs fewer accounts filled it first, which would
15
+ // leave the invoicing pickers empty with no request in flight to fix it.
16
+ const isAccountListUsable = accountList.hasValidState() && accountList.accountIds.length > 0;
17
+ if (isAccountListUsable === false &&
13
18
  accountList.fetchState !== 'In-Progress') {
14
- // AP/AR accounts cannot back an invoicing mapping, so they are excluded
15
- // here — which is why this uses invoicing's own bucket rather than the
16
- // general `accountList` other screens fill without those exclusions.
17
- actions.push(fetchAccountList(INVOICING_ACCOUNT_LIST_KEY, true, true, true));
19
+ // Same arguments the vendor and categorization screens pass, so this
20
+ // shares their cached list rather than replacing it with a narrower one.
21
+ // Invoicing narrows to income accounts when it builds its options.
22
+ actions.push(fetchAccountList(INVOICING_ACCOUNT_LIST_KEY, true));
18
23
  }
19
24
  const classList = state.classListState;
20
25
  if (classList.hasValidState() === false &&
@@ -1,10 +1,34 @@
1
1
  import { getAccountList, getNestedAccountListHierarchy, } from '../../accountList/accountListSelector';
2
2
  import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
3
- // Its own bucket: invoicing needs the AP/AR exclusions that the vendor and
4
- // transaction screens sharing `accountList` do not pass.
5
- export const INVOICING_ACCOUNT_LIST_KEY = 'invoicingAccountList';
3
+ // The shared bucket, fetched with the same arguments as the other consumers so
4
+ // whichever screen loads first satisfies the rest. Invoicing narrows to income
5
+ // accounts when it builds its options, so it does not need its own copy.
6
+ export const INVOICING_ACCOUNT_LIST_KEY = 'accountList';
6
7
  /** Whether QBO is connected, without building the option lists to find out. */
7
8
  export const isInvoicingQboConnected = (state) => state.invoicingQboViewState.connection?.connected === true;
9
+ /**
10
+ * Whether the accounting pickers are still waiting on their data. Only true when
11
+ * QBO is connected: a tenant without it has no pickers to populate, so the form
12
+ * must not sit behind a skeleton waiting for lists it will never show.
13
+ */
14
+ export const isInvoicingAccountingPickerDataLoading = (state) => {
15
+ const isPending = (fetchState) => fetchState === 'Not-Started' || fetchState === 'In-Progress';
16
+ // The connection settles first and decides whether the lists are fetched at
17
+ // all, so it counts as loading too. Reporting "not loading" while it is in
18
+ // flight renders the form, then takes it away again the moment the tenant
19
+ // turns out to be connected.
20
+ if (isPending(getInvoicingQboConnectionFetchState(state).fetchState)) {
21
+ return true;
22
+ }
23
+ if (!isInvoicingQboConnected(state)) {
24
+ return false;
25
+ }
26
+ const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
27
+ return (isPending(accountList.fetchState) ||
28
+ isPending(state.classListState.fetchState));
29
+ };
30
+ /** The QBO connection fetch on its own, without building the bundled view. */
31
+ export const getInvoicingQboConnectionFetchState = (state) => state.invoicingQboViewState.connectionFetchState;
8
32
  /**
9
33
  * Account types that can back invoicing revenue. Named here so screens ask for
10
34
  * "income accounts" rather than repeating the accounting service's vocabulary.
@@ -63,9 +63,10 @@ export function deriveInvoicingEnablePrerequisites(settings, isQboConnected, qbo
63
63
  * two integration prerequisites kept separate so the hub can name the step still
64
64
  * outstanding instead of showing a dead button.
65
65
  *
66
- * `canEnableInvoicing` folds in the onboarding step, so the screen does not need
67
- * to re-check it: the footer is only offered mid-onboarding and only once both
68
- * integrations are ready.
66
+ * `isInvoicingEnabled` decides whether the footer renders; `canEnableInvoicing`
67
+ * whether its button is live, and it still requires the onboarding step so the
68
+ * terms cannot be skipped. A tenant partway through therefore sees the footer,
69
+ * disabled and naming what is left, rather than nothing at all.
69
70
  *
70
71
  * Deliberately not folded into `getInvoicingConfigView`: that selector gates
71
72
  * every invoicing route and stays a pure read of the config slice. The settings
@@ -75,7 +76,10 @@ export const getInvoicingSettingsHubView = (state) => {
75
76
  const { dunningScheduleActionOptions, enableState, error: configError, fetchState: configFetchState, qboAccountMappingKindOptions, qboCustomerMatchingOptions, qboSyncStatusOptions, status, } = getInvoicingConfigView(state);
76
77
  const { canEnable, isQboConfigured, isStripeConnected } = deriveInvoicingEnablePrerequisites(state.invoicingSettingsViewState.settings, state.invoicingQboViewState.connection?.connected === true, qboAccountMappingKindOptions);
77
78
  return {
79
+ // Keeps the onboarding step in the gate: a tenant that has not accepted the
80
+ // terms cannot enable invoicing, whatever the integrations say.
78
81
  canEnableInvoicing: status === 'settings' && canEnable,
82
+ isInvoicingEnabled: state.invoicingConfigViewState.isInvoicingEnabled === true,
79
83
  configError,
80
84
  configFetchState,
81
85
  dunningScheduleActionOptions,