@zeniai/client-epic-state 5.3.20 → 5.3.21

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 (57) hide show
  1. package/lib/entity/genericEntity/customerWithSubCustomersId.d.ts +7 -0
  2. package/lib/entity/genericEntity/customerWithSubCustomersId.js +17 -0
  3. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  4. package/lib/entity/snackbar/snackbarTypes.js +1 -0
  5. package/lib/entity/tenant/epic/fetchAllTenantsEpic.js +6 -4
  6. package/lib/entity/tenant/tenantPayload.d.ts +6 -0
  7. package/lib/entity/tenant/tenantPayload.js +5 -0
  8. package/lib/entity/tenant/tenantReducer.d.ts +6 -2
  9. package/lib/entity/tenant/tenantReducer.js +32 -5
  10. package/lib/entity/tenant/tenantSelector.d.ts +2 -1
  11. package/lib/entity/tenant/tenantSelector.js +4 -1
  12. package/lib/entity/tenant/tenantState.d.ts +10 -0
  13. package/lib/entity/tenant/tenantState.js +5 -0
  14. package/lib/epic.d.ts +2 -1
  15. package/lib/epic.js +2 -0
  16. package/lib/esm/entity/genericEntity/customerWithSubCustomersId.js +12 -0
  17. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  18. package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +6 -4
  19. package/lib/esm/entity/tenant/tenantPayload.js +4 -1
  20. package/lib/esm/entity/tenant/tenantReducer.js +31 -4
  21. package/lib/esm/entity/tenant/tenantSelector.js +2 -0
  22. package/lib/esm/entity/tenant/tenantState.js +4 -1
  23. package/lib/esm/epic.js +2 -0
  24. package/lib/esm/index.js +7 -6
  25. package/lib/esm/view/companyView/companyViewReducer.js +26 -1
  26. package/lib/esm/view/companyView/epic/companyPassport/updateCustomersTabSettingEpic.js +47 -0
  27. package/lib/esm/view/companyView/selector/companyPassportViewSelector.js +2 -0
  28. package/lib/esm/view/companyView/types/companyPassport/companyPassportViewState.js +4 -0
  29. package/lib/esm/view/customerSummaryView/customerSummaryViewReducer.js +39 -6
  30. package/lib/esm/view/customerSummaryView/customerSummaryViewSelector.js +41 -11
  31. package/lib/esm/view/customerSummaryView/epic/fetchCustomerSummaryListEpic.js +6 -1
  32. package/lib/esm/view/transactionList/fetchTransactionListByEntityEpic.js +11 -2
  33. package/lib/esm/view/transactionList/transactionListReducer.js +24 -15
  34. package/lib/esm/view/trend/fetchIncomeTrendEpic.js +6 -2
  35. package/lib/index.d.ts +9 -8
  36. package/lib/index.js +53 -46
  37. package/lib/view/companyView/companyViewReducer.d.ts +8 -1
  38. package/lib/view/companyView/companyViewReducer.js +28 -3
  39. package/lib/view/companyView/epic/companyPassport/updateCustomersTabSettingEpic.d.ts +11 -0
  40. package/lib/view/companyView/epic/companyPassport/updateCustomersTabSettingEpic.js +51 -0
  41. package/lib/view/companyView/selector/companyPassportViewSelector.d.ts +1 -0
  42. package/lib/view/companyView/selector/companyPassportViewSelector.js +4 -1
  43. package/lib/view/companyView/types/companyPassport/companyPassportViewState.d.ts +1 -0
  44. package/lib/view/companyView/types/companyPassport/companyPassportViewState.js +4 -0
  45. package/lib/view/customerSummaryView/customerSummaryViewPayload.d.ts +20 -1
  46. package/lib/view/customerSummaryView/customerSummaryViewReducer.d.ts +3 -1
  47. package/lib/view/customerSummaryView/customerSummaryViewReducer.js +42 -6
  48. package/lib/view/customerSummaryView/customerSummaryViewSelector.d.ts +13 -1
  49. package/lib/view/customerSummaryView/customerSummaryViewSelector.js +45 -12
  50. package/lib/view/customerSummaryView/customerSummaryViewState.d.ts +24 -10
  51. package/lib/view/customerSummaryView/epic/fetchCustomerSummaryListEpic.js +6 -1
  52. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  53. package/lib/view/transactionList/fetchTransactionListByEntityEpic.js +11 -2
  54. package/lib/view/transactionList/transactionListReducer.d.ts +3 -1
  55. package/lib/view/transactionList/transactionListReducer.js +24 -15
  56. package/lib/view/trend/fetchIncomeTrendEpic.js +6 -2
  57. package/package.json +1 -1
@@ -116,12 +116,26 @@ const tenant = createSlice({
116
116
  tenantSummariesById[payload.tenant_id] =
117
117
  mapTenantSummaryPayloadToTenantSummary(payload);
118
118
  });
119
- draft.tenantSummariesById = tenantSummariesById;
120
119
  draft.tenantIds = Object.keys(tenantSummariesById);
121
- // A tenant missing from the list is no longer accessible to the user.
120
+ // A tenant missing from the list is no longer accessible to the user,
121
+ // except the open one: the list hides companies by status, and the
122
+ // selected tenant's record was loaded for it on its own. Its summary
123
+ // stays too (outside `tenantIds`, so not in the switcher), so its ids
124
+ // are still known while its record loads.
125
+ const { currentTenantId } = draft;
126
+ const currentTenantSummary = currentTenantId != null
127
+ ? draft.tenantSummariesById[currentTenantId]
128
+ : undefined;
129
+ if (currentTenantId != null &&
130
+ currentTenantSummary != null &&
131
+ tenantSummariesById[currentTenantId] == null) {
132
+ tenantSummariesById[currentTenantId] = currentTenantSummary;
133
+ }
134
+ draft.tenantSummariesById = tenantSummariesById;
122
135
  const accessibleTenantsById = {};
123
136
  Object.values(draft.tenantsById).forEach((tenant) => {
124
- if (tenantSummariesById[tenant.tenantId] != null) {
137
+ if (tenantSummariesById[tenant.tenantId] != null ||
138
+ tenant.tenantId === draft.currentTenantId) {
125
139
  accessibleTenantsById[tenant.tenantId] = tenant;
126
140
  }
127
141
  });
@@ -174,6 +188,13 @@ const tenant = createSlice({
174
188
  enabled;
175
189
  }
176
190
  },
191
+ updateTenantCustomersTabSetting(draft, action) {
192
+ const { tenantId, setting, enabled } = action.payload;
193
+ const tenant = draft.tenantsById[tenantId];
194
+ if (tenant != null) {
195
+ tenant.customersTabSettings[setting] = enabled;
196
+ }
197
+ },
177
198
  // Optimistic per-key merge of the product-family flags after a cockpit
178
199
  // "enable services" write succeeds. Only the provided keys are applied so
179
200
  // other product settings are preserved.
@@ -953,7 +974,7 @@ const tenant = createSlice({
953
974
  });
954
975
  },
955
976
  });
956
- export const { updateTenants, fetchAllTenants, updateTenantSummariesSuccess, seedTenantSummary, updateTenantsFailure, fetchActiveTenant, updateTenantSuccess, updateTenantFailure, updateCurrentTenant, fetchExcludedResources, updateExcludedResourcesSuccess, updateExcludedResourcesFailure, doSignIn, doMagicLinkSignIn, magicLinkSignInSuccess, magicLinkSignInFailure, sendEmailMagicLinkToUser, sendEmailMagicLinkToUserSuccess, sendEmailMagicLinkToUserFailure, updateSignInState, doSignOut, signOutSuccess, sendSessionHeartbeat, sessionHeartbeatSuccess, sessionHeartbeatFailure, updateLoggedInUser, fetchExternalConnections, saveExternalConnection, saveExternalConnectionSuccess, saveExternalConnectionFailure, fetchExternalConnectionsFailure, fetchExternalConnectionsSuccess, saveAPIKeyConnection, saveAPIKeyConnectionSuccess, saveAPIKeyConnectionFailure, saveOAuthConnection, saveOAuthConnectionSuccess, saveOAuthConnectionFailure, saveConnectorCredentials, saveConnectorCredentialsSuccess, saveConnectorCredentialsFailure, initEmailConnectOAuth, initEmailConnectOAuthSuccess, initEmailConnectOAuthFailure, deleteConnection, deleteConnectionSuccess, deleteConnectionFailure, fetchSubscriptionSummaryForTenant, updateSubscriptionSummaryForTenantSuccess, updateSubscriptionSummaryForTenantFailure, updateOnboardingTenants, removeOnboardingTenant, updateTenantReimbursementInfo, updateReferViewedForLoggedInUser, resetSignInState, trigger2FA, updateTenantAccountingClassesEnabled, updateTenantAccountingProjectsEnabled, updateTenantAutoReviewIncomeWithoutCustomer, updateTenantProductSettings, updateTenantCapitalizationOnboardingDismissed, updateTenantCapitalizationAccountOverride, updateTenantMasterTOSInfo, verifyDeviceWithTwoFA, verifyDeviceWithTwoFASuccess, verifyDeviceWithTwoFAFailure, updateTreasuryVideoViewedForLoggedInUser, updateTreasuryPromoRemindMeLaterClickedForLoggedInUser, updateTreasuryPromoIntroClosedByOutsideClickForLoggedInUser, resendVerifyDeviceOTP, resendVerifyDeviceOTPSuccess, resendVerifyDeviceOTPFailure, } = tenant.actions;
977
+ export const { updateTenants, fetchAllTenants, updateTenantSummariesSuccess, seedTenantSummary, updateTenantsFailure, fetchActiveTenant, updateTenantSuccess, updateTenantFailure, updateCurrentTenant, fetchExcludedResources, updateExcludedResourcesSuccess, updateExcludedResourcesFailure, doSignIn, doMagicLinkSignIn, magicLinkSignInSuccess, magicLinkSignInFailure, sendEmailMagicLinkToUser, sendEmailMagicLinkToUserSuccess, sendEmailMagicLinkToUserFailure, updateSignInState, doSignOut, signOutSuccess, sendSessionHeartbeat, sessionHeartbeatSuccess, sessionHeartbeatFailure, updateLoggedInUser, fetchExternalConnections, saveExternalConnection, saveExternalConnectionSuccess, saveExternalConnectionFailure, fetchExternalConnectionsFailure, fetchExternalConnectionsSuccess, saveAPIKeyConnection, saveAPIKeyConnectionSuccess, saveAPIKeyConnectionFailure, saveOAuthConnection, saveOAuthConnectionSuccess, saveOAuthConnectionFailure, saveConnectorCredentials, saveConnectorCredentialsSuccess, saveConnectorCredentialsFailure, initEmailConnectOAuth, initEmailConnectOAuthSuccess, initEmailConnectOAuthFailure, deleteConnection, deleteConnectionSuccess, deleteConnectionFailure, fetchSubscriptionSummaryForTenant, updateSubscriptionSummaryForTenantSuccess, updateSubscriptionSummaryForTenantFailure, updateOnboardingTenants, removeOnboardingTenant, updateTenantReimbursementInfo, updateReferViewedForLoggedInUser, resetSignInState, trigger2FA, updateTenantAccountingClassesEnabled, updateTenantAccountingProjectsEnabled, updateTenantAutoReviewIncomeWithoutCustomer, updateTenantCustomersTabSetting, updateTenantProductSettings, updateTenantCapitalizationOnboardingDismissed, updateTenantCapitalizationAccountOverride, updateTenantMasterTOSInfo, verifyDeviceWithTwoFA, verifyDeviceWithTwoFASuccess, verifyDeviceWithTwoFAFailure, updateTreasuryVideoViewedForLoggedInUser, updateTreasuryPromoRemindMeLaterClickedForLoggedInUser, updateTreasuryPromoIntroClosedByOutsideClickForLoggedInUser, resendVerifyDeviceOTP, resendVerifyDeviceOTPSuccess, resendVerifyDeviceOTPFailure, } = tenant.actions;
957
978
  export default tenant.reducer;
958
979
  /**
959
980
  * Applies treasury promo user fields from the tenants API user block when present.
@@ -1012,6 +1033,12 @@ function mapTenantPayloadToTenant(payload, userTenantPayload) {
1012
1033
  isAccountingProjectsEnabled: payload.is_accounting_projects_enabled ?? false,
1013
1034
  isAutoReviewIncomeWithoutCustomerEnabled: payload.accounting_config?.customers
1014
1035
  ?.auto_review_income_without_customer ?? false,
1036
+ customersTabSettings: {
1037
+ includeSubCustomersInTotals: payload.accounting_config?.customers?.include_sub_customers_in_totals ??
1038
+ false,
1039
+ listTopLevelCustomersOnly: payload.accounting_config?.customers?.list_top_level_customers_only ??
1040
+ false,
1041
+ },
1015
1042
  bookCloseDate: payload.book_close_date != null
1016
1043
  ? date(payload.book_close_date)
1017
1044
  : undefined,
@@ -1,4 +1,5 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
+ import recordGet from 'lodash/get';
2
3
  import orderBy from 'lodash/orderBy';
3
4
  import { getCompanyByCompanyId } from '../company/companySelector';
4
5
  export function getTenant(tenantState, companyState, tenantId) {
@@ -318,6 +319,7 @@ export const getIsAutoReviewIncomeWithoutCustomerEnabled = (state) => {
318
319
  return (tenantsById[currentTenantId]?.isAutoReviewIncomeWithoutCustomerEnabled ??
319
320
  false);
320
321
  };
322
+ export const getTenantCustomersTabSettings = (tenantState, tenantId) => recordGet(tenantState.tenantsById, [tenantId, 'customersTabSettings'], undefined);
321
323
  export const isTenantBookkeepingEnabled = (tenant) => {
322
324
  // if the tenants API takes time, tenant can be non null with productSettings as null
323
325
  return tenant != null && tenant.productSettings != null
@@ -1 +1,4 @@
1
- export {};
1
+ export const DEFAULT_CUSTOMERS_TAB_SETTINGS = {
2
+ includeSubCustomersInTotals: false,
3
+ listTopLevelCustomersOnly: false,
4
+ };
package/lib/esm/epic.js CHANGED
@@ -161,6 +161,7 @@ import { updateCapitalizationAccountThresholdEpic, } from './view/companyView/ep
161
161
  import { updateCompanyDetailsEpic, } from './view/companyView/epic/companyPassport/updateCompanyDetailsEpic';
162
162
  import { updateCompanyOfficerEpic, } from './view/companyView/epic/companyPassport/updateCompanyOfficerEpic';
163
163
  import { updateCompanyPassportLocalStoreDataEpic, } from './view/companyView/epic/companyPassport/updateCompanyPassportLocalStoreDataEpic';
164
+ import { updateCustomersTabSettingEpic, } from './view/companyView/epic/companyPassport/updateCustomersTabSettingEpic';
164
165
  import { updatePrimaryContactEpic, } from './view/companyView/epic/companyPassport/updatePrimaryContactEpic';
165
166
  import { fetchAllCockpitViewsEpic, } from './view/companyView/epic/fetchAllCockpitViewsEpic';
166
167
  import { fetchCompanyManagementViewEpic, } from './view/companyView/epic/fetchCompanyManagementViewEpic';
@@ -1517,6 +1518,7 @@ const ALL_EPICS = [
1517
1518
  updateCompanyProductServicesEpic,
1518
1519
  updateCompanyQboRealmIdEpic,
1519
1520
  updateCompanyTaskManagerViewFiltersEpic,
1521
+ updateCustomersTabSettingEpic,
1520
1522
  updateDashboardLayoutEpic,
1521
1523
  updateDebitCardPinAttemptEpic,
1522
1524
  updateDepositAccountEpic,
package/lib/esm/index.js CHANGED
@@ -73,7 +73,7 @@ import { getSnackbar } from './entity/snackbar/snackbarSelector';
73
73
  import { ALL_WEEK_DAYS, toDayOfWeek, toPriorityCodeType, toTaskStatusCodeType, } from './entity/task/taskState';
74
74
  import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
75
75
  import { deleteConnection, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, initEmailConnectOAuth, resendVerifyDeviceOTP, resetSignInState, saveAPIKeyConnection, saveConnectorCredentials, saveExternalConnection as saveExternalConnectionForTenant, saveOAuthConnection, seedTenantSummary, sendEmailMagicLinkToUser, sendSessionHeartbeat, toExternalIntegrationType, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
76
- import { DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getCurrentTenantId, getCurrentTenantIds, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isCurrentTenantLoaded, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantLoaded, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
76
+ import { DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getCurrentTenantId, getCurrentTenantIds, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantCustomersTabSettings, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isCurrentTenantLoaded, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantLoaded, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
77
77
  import { toExternalSupportedTool } from './entity/tenant/tenantTypes';
78
78
  import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
79
79
  import { getLastNotificationTime, getNotifications, } from './entity/toastNotification/toastNotificationSelector';
@@ -169,11 +169,11 @@ import { fetchCompanyMonthEndReportHistoricData, fetchCompanyMonthEndReportHisto
169
169
  import { getCompanyMonthEndReportHistoricData, getCompanyMonthEndReportSelectorView, } from './view/companyMonthEndReportView/companyMonthEndReportViewSelector';
170
170
  import { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, updateCompanyTaskManagerViewFilters, } from './view/companyTaskManagerView/companyTaskManagerViewReducer';
171
171
  import { getCompanyTaskManagerView, } from './view/companyTaskManagerView/companyTaskManagerViewSelector';
172
- import { clearQboProjectsReconnectRequest, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, disableAccountingProjects, dismissCapitalizationOnboarding, enableAccountingProjects, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, resumeEnableAccountingProjects, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, updateAutoReviewIncomeWithoutCustomer, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState, updateCompanyProductServices, updateCompanyQboRealmId, } from './view/companyView/companyViewReducer';
172
+ import { clearQboProjectsReconnectRequest, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, disableAccountingProjects, dismissCapitalizationOnboarding, enableAccountingProjects, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, resumeEnableAccountingProjects, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, updateAutoReviewIncomeWithoutCustomer, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState, updateCompanyProductServices, updateCompanyQboRealmId, updateCustomersTabSetting, } from './view/companyView/companyViewReducer';
173
173
  import { canSendMonthEndEmailReport, shouldEnableCalendarPickerForLastReportSent, } from './view/companyView/helpers/cockpitHelpers';
174
174
  import { getParentSubsidiaryManagementView } from './view/companyView/parentSubsidiaryView/parentSubsidiaryViewSelector';
175
175
  import { getAddonListZeniSku, getCompanyManagementView, getCompanyProductServicesUpdateStatus, getCompanyQboRealmIdUpdateStatus, getPlanListZeniSku, } from './view/companyView/selector/companyManagementViewSelector';
176
- import { getCompanyPassportLocalStoreData, getCompanyPassportView, isCompanyPassportDataToBeSaved, } from './view/companyView/selector/companyPassportViewSelector';
176
+ import { getCompanyPassportLocalStoreData, getCompanyPassportView, getIsCustomersTabSettingsUpdateInProgress, isCompanyPassportDataToBeSaved, } from './view/companyView/selector/companyPassportViewSelector';
177
177
  import { getCompanyPortfolioView, isInfiniteRunway, } from './view/companyView/selector/companyPortfolioViewSelector';
178
178
  import { getAllCockpitTabsFilterView, } from './view/companyView/selector/getAllCockpitTabsFilterView';
179
179
  import { ALL_COCKPIT_TABS_FILE_TYPES, ALL_COCKPIT_TABS_IDS, toCockpitTabsFileTypeStrict, toCockpitTabsIDStrict, } from './view/companyView/types/cockpitTabsID';
@@ -509,7 +509,7 @@ export { toTimeframeTick, mapTimePeriodtoTimeframeTick, toTimePeriod, toAbsolute
509
509
  export { toVendorSpendTrendFilterTabsTypeStrict, } from './entity/vendorExpense/vendorSpendTrendFilterTabType';
510
510
  export { getNumberOfPeriods };
511
511
  export { SCHEDULE_DAYS_OF_MONTH, toScheduleDaysOfMonth, toMonth, toMonthStrict, toQuarter, toQuarterStrict, };
512
- export { getTenantBaseById, getCurrentTenant, getCurrentTenantId, getCurrentTenantIds, isCurrentTenantLoaded, isTenantLoaded, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, fetchActiveTenant, fetchAllTenants, seedTenantSummary, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, doSignOut, sendSessionHeartbeat, resetSignInState, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, deleteConnection, saveAPIKeyConnection, saveConnectorCredentials, saveOAuthConnection, initEmailConnectOAuth, toExternalIntegrationType, toExternalSupportedTool, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, DEFAULT_RECEIPTS_EMAIL, getReceiptsEmail, };
512
+ export { getTenantBaseById, getCurrentTenant, getCurrentTenantId, getCurrentTenantIds, isCurrentTenantLoaded, isTenantLoaded, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getTenantCustomersTabSettings, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, fetchActiveTenant, fetchAllTenants, seedTenantSummary, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, doSignOut, sendSessionHeartbeat, resetSignInState, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, deleteConnection, saveAPIKeyConnection, saveConnectorCredentials, saveOAuthConnection, initEmailConnectOAuth, toExternalIntegrationType, toExternalSupportedTool, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, DEFAULT_RECEIPTS_EMAIL, getReceiptsEmail, };
513
513
  export { toAccountType, toAccountGroupType, getAccountGroupKey, getAllAccounts, getTransactionFilterAccountOptions, };
514
514
  export { getAllClasses, getClassById, getClassFilterOptions, } from './entity/class/classSelector';
515
515
  export { getForecast };
@@ -599,7 +599,7 @@ export { newAddressInLocalStore, getAddress, getNewAddress, getAllNewAddresses,
599
599
  export { toManagementSortKeyType, toOnboardingSortKeyType, toPortfolioSortKeyType, toHealthSortKeyType, toTaskManagerSortKeyType, updateCompanyTaskManagerViewFilters, };
600
600
  export { ALL_COCKPIT_TABS_FILE_TYPES, ALL_COCKPIT_TABS_IDS, toCockpitTabsIDStrict, toCockpitTabsFileTypeStrict, };
601
601
  export { isAllowedValueWithCode, isAllowedValueWithID, };
602
- export { fetchCompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, updateAutoReviewIncomeWithoutCustomer, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCompanyProductServices, getCompanyProductServicesUpdateStatus, updateCompanyQboRealmId, getCompanyQboRealmIdUpdateStatus, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, };
602
+ export { fetchCompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, getIsCustomersTabSettingsUpdateInProgress, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, updateAutoReviewIncomeWithoutCustomer, updateCustomersTabSetting, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCompanyProductServices, getCompanyProductServicesUpdateStatus, updateCompanyQboRealmId, getCompanyQboRealmIdUpdateStatus, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, };
603
603
  export { toCompanyPassportLocalData, };
604
604
  export { COMPANY_ONBOARDING_INDUSTRY_TYPE_CODES, COMPANY_ONBOARDING_SUB_INDUSTRY_CODES_BY_INDUSTRY, COMPANY_PURPOSE_OF_ACCOUNT_CODES, COMPANY_SOURCE_OF_FUNDS_CODES, COMPANY_TRANSACTION_VOLUME_CODES, COMPANY_US_NEXUS_TYPE_CODES, getCompanyOnboardingSubIndustryCodesForIndustry, };
605
605
  export { getUncategorizedAccounts, getNestedAccountListHierarchy, };
@@ -876,5 +876,6 @@ export { getInvoicingOnboardingGateView } from './view/invoicing/invoicingConfig
876
876
  export { getExpenseAutomationJEOneTimeAccrualDetailView, getJEScheduleRowType, isAccruedScheduleRow, isAiAccountantSchedule, isExistingTabRow, isOneTimeAccrualRow, isRenderableOneTimeAccrual, isPendingReviewRow, } from './view/expenseAutomationView/selectors/jeSchedulesViewSelector';
877
877
  export { getActualMatchingAmount, getActualMatchingReversalDate, getActualMatchingMatchModalConfirmStatus, getCloseMatchAfterIgnoreMatchId, getCloseMatchAfterUndo, getCloseMatchAfterUndoMatchId, parseActualMatchingMatchId, getExpenseAutomationActualMatchingMatchView, getExpenseAutomationActualMatchingView, isAutoReversedAccrual, isManuallyReversedAccrual, isPendingReviewAccrual, } from './view/expenseAutomationView/selectors/actualMatchingViewSelector';
878
878
  export { fetchCustomerSummaryDetail, fetchCustomerSummaryList, resetCustomerSummaryDetailUiState, saveCustomerSummaryDetail, updateCustomerAllTransactionsTabData, updateCustomerDetailSelectedTab, updateCustomerSummaryRevenueSelection, updateCustomerSummaryScrollYOffset, updateCustomerSummarySortUiState, updateCustomerTrendFilter, } from './view/customerSummaryView/customerSummaryViewReducer';
879
- export { getCustomerSummaryAllTransactionsTabData, getCustomerSummaryDetail, getCustomerSummaryList, getCustomerSummarySelectedTabType, getCustomerSummaryTrendFilter, isCustomersTabVisible, } from './view/customerSummaryView/customerSummaryViewSelector';
879
+ export { getCustomerSummaryAllTransactionsTabData, getCustomerSummaryDetail, getCustomerSummaryList, getCustomerSummarySelectedTabType, getCustomerSummaryTrendFilter, getCustomersTabSettings, isCustomersTabVisible, } from './view/customerSummaryView/customerSummaryViewSelector';
880
880
  export { CUSTOMER_REVENUE_KEYS } from './view/customerSummaryView/customerSummaryViewState';
881
+ export { fromCustomerEntityId, toCustomerWithSubCustomersId, } from './entity/genericEntity/customerWithSubCustomersId';
@@ -335,6 +335,31 @@ const companyView = createSlice({
335
335
  error: action.payload.status,
336
336
  };
337
337
  },
338
+ updateCustomersTabSetting: {
339
+ reducer(draft,
340
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
341
+ _action) {
342
+ draft.passportView.customersTabSettingsUpdateStatus = {
343
+ fetchState: 'In-Progress',
344
+ error: undefined,
345
+ };
346
+ },
347
+ prepare(companyId, setting, enabled, tenantId) {
348
+ return { payload: { companyId, enabled, setting, tenantId } };
349
+ },
350
+ },
351
+ updateCustomersTabSettingSuccess(draft) {
352
+ draft.passportView.customersTabSettingsUpdateStatus = {
353
+ fetchState: 'Completed',
354
+ error: undefined,
355
+ };
356
+ },
357
+ updateCustomersTabSettingFailure(draft, action) {
358
+ draft.passportView.customersTabSettingsUpdateStatus = {
359
+ fetchState: 'Error',
360
+ error: action.payload.status,
361
+ };
362
+ },
338
363
  enableAccountingProjects: {
339
364
  reducer(draft,
340
365
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -779,5 +804,5 @@ const companyView = createSlice({
779
804
  });
780
805
  },
781
806
  });
782
- export const { fetchCompanyPassportView, fetchAllCockpitViews, fetchOnboardingView, fetchManagementView, fetchSubscriptionView, fetchPortfolioView, fetchCompanyManagementView, companyManagementSaveUpdates, companyManagementSavePendingUpdates, companyManagementDiscardUpdates, companyManagementSaveUpdatesSuccess, companyManagementSaveUpdatesFailure, updateCompanyViewOnSuccess, updateCompanyViewOnFailure, fetchCompanyPortfolioView, fetchZeniUsers, updateZeniUsersOnSuccess, updateZeniUsersOnFailure, updateCompanyPortfolioUIState, updateCompanyManagementUIState, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, companyPassportUpdateCompanyDetails, companyPassportUpdateCompanyDetailsSuccess, companyPassportUpdateCompanyDetailsFailure, updateAccountingClassesEnabled, updateAccountingClassesEnabledSuccess, updateAccountingClassesEnabledFailure, updateAutoReviewIncomeWithoutCustomer, updateAutoReviewIncomeWithoutCustomerSuccess, updateAutoReviewIncomeWithoutCustomerFailure, updateCompanyProductServices, updateCompanyProductServicesSuccess, updateCompanyProductServicesFailure, updateCompanyQboRealmId, updateCompanyQboRealmIdSuccess, updateCompanyQboRealmIdFailure, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, updateAccountingProjectsEnabledSuccess, updateAccountingProjectsEnabledFailure, requestQboProjectsReconnect, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, updateCapitalizationAccountThresholdSuccess, updateCapitalizationAccountThresholdFailure, dismissCapitalizationOnboarding, companyPassportUpdatePrimaryContact, companyPassportUpdatePrimaryContactSuccess, companyPassportUpdatePrimaryContactFailure, companyPassportUpdateCompanyOfficer, companyPassportUpdateCompanyOfficerSuccess, companyPassportUpdateCompanyOfficerFailure, companyPassportCreateCompanyOfficers, companyPassportCreateCompanyOfficersSuccess, companyPassportCreateCompanyOfficersFailure, updateCompanyPassportLocalStoreData, saveIndustryAndIncDateInCompanyPassportLocalStore, saveCompanyPassportDetails, fetchParentSubsidiaryManagementView, updateParentSubsidiaryView, updateParentSubsidiaryViewOnFailure, updateCompanyDownloadState, fetchCompanyMetaData, updateCompanyMetaDataOnSuccess, updateCompanyMetaDataOnFailure, updateMetaDataOnSendMonthEndReport, } = companyView.actions;
807
+ export const { fetchCompanyPassportView, fetchAllCockpitViews, fetchOnboardingView, fetchManagementView, fetchSubscriptionView, fetchPortfolioView, fetchCompanyManagementView, companyManagementSaveUpdates, companyManagementSavePendingUpdates, companyManagementDiscardUpdates, companyManagementSaveUpdatesSuccess, companyManagementSaveUpdatesFailure, updateCompanyViewOnSuccess, updateCompanyViewOnFailure, fetchCompanyPortfolioView, fetchZeniUsers, updateZeniUsersOnSuccess, updateZeniUsersOnFailure, updateCompanyPortfolioUIState, updateCompanyManagementUIState, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, companyPassportUpdateCompanyDetails, companyPassportUpdateCompanyDetailsSuccess, companyPassportUpdateCompanyDetailsFailure, updateAccountingClassesEnabled, updateAccountingClassesEnabledSuccess, updateAccountingClassesEnabledFailure, updateAutoReviewIncomeWithoutCustomer, updateAutoReviewIncomeWithoutCustomerSuccess, updateAutoReviewIncomeWithoutCustomerFailure, updateCustomersTabSetting, updateCustomersTabSettingSuccess, updateCustomersTabSettingFailure, updateCompanyProductServices, updateCompanyProductServicesSuccess, updateCompanyProductServicesFailure, updateCompanyQboRealmId, updateCompanyQboRealmIdSuccess, updateCompanyQboRealmIdFailure, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, updateAccountingProjectsEnabledSuccess, updateAccountingProjectsEnabledFailure, requestQboProjectsReconnect, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, updateCapitalizationAccountThresholdSuccess, updateCapitalizationAccountThresholdFailure, dismissCapitalizationOnboarding, companyPassportUpdatePrimaryContact, companyPassportUpdatePrimaryContactSuccess, companyPassportUpdatePrimaryContactFailure, companyPassportUpdateCompanyOfficer, companyPassportUpdateCompanyOfficerSuccess, companyPassportUpdateCompanyOfficerFailure, companyPassportCreateCompanyOfficers, companyPassportCreateCompanyOfficersSuccess, companyPassportCreateCompanyOfficersFailure, updateCompanyPassportLocalStoreData, saveIndustryAndIncDateInCompanyPassportLocalStore, saveCompanyPassportDetails, fetchParentSubsidiaryManagementView, updateParentSubsidiaryView, updateParentSubsidiaryViewOnFailure, updateCompanyDownloadState, fetchCompanyMetaData, updateCompanyMetaDataOnSuccess, updateCompanyMetaDataOnFailure, updateMetaDataOnSendMonthEndReport, } = companyView.actions;
783
808
  export default companyView.reducer;
@@ -0,0 +1,47 @@
1
+ import { from } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { updateCompanies } from '../../../../entity/company/companyReducer';
4
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
+ import { CUSTOMERS_TAB_SETTING_API_KEYS } from '../../../../entity/tenant/tenantPayload';
6
+ import { updateTenantCustomersTabSetting } from '../../../../entity/tenant/tenantReducer';
7
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
8
+ import { updateCustomersTabSetting, updateCustomersTabSettingFailure, updateCustomersTabSettingSuccess, } from '../../companyViewReducer';
9
+ const failureActions = (status) => [
10
+ updateCustomersTabSettingFailure({ status }),
11
+ openSnackbar({
12
+ messageSection: 'customers_tab_settings_update',
13
+ messageText: 'failed',
14
+ type: 'error',
15
+ }),
16
+ ];
17
+ /** Settings › Accounting › Customers: save one Customers tab setting (accounting_config is deep-merged). */
18
+ export const updateCustomersTabSettingEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(updateCustomersTabSetting.match),
19
+ // The two settings are separate keys: a quick second toggle mustn't cancel the first save
20
+ mergeMap((action) => {
21
+ const { companyId, enabled, setting, tenantId } = action.payload;
22
+ return zeniAPI
23
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/companies/${companyId}`, {
24
+ accounting_config: {
25
+ customers: { [CUSTOMERS_TAB_SETTING_API_KEYS[setting]]: enabled },
26
+ },
27
+ })
28
+ .pipe(mergeMap((response) => {
29
+ const companies = response.data?.companies ?? [];
30
+ if (!isSuccessResponse(response) || companies.length === 0) {
31
+ return from(failureActions(response.status));
32
+ }
33
+ return from([
34
+ updateCompanies({ payload: companies, schema: {} }),
35
+ updateTenantCustomersTabSetting({ enabled, setting, tenantId }),
36
+ updateCustomersTabSettingSuccess(),
37
+ openSnackbar({
38
+ messageSection: 'customers_tab_settings_update',
39
+ messageText: 'success',
40
+ type: 'success',
41
+ }),
42
+ ]);
43
+ }), catchError((error) => from(failureActions(createZeniAPIStatus('Unexpected Error', 'Update Customers tab setting errored out: ' +
44
+ (error instanceof Error
45
+ ? error.message
46
+ : JSON.stringify(error)))))));
47
+ }));
@@ -153,3 +153,5 @@ export const isCompanyPassportDataToBeSaved = (companyLocalStoreData, companyVie
153
153
  .reduce((isDetailsUpdated, isCurDetailsUpdated) => isDetailsUpdated || isCurDetailsUpdated, false);
154
154
  return isCompanyDataUpdated;
155
155
  };
156
+ export const getIsCustomersTabSettingsUpdateInProgress = (state) => state.companyViewState.passportView.customersTabSettingsUpdateStatus
157
+ .fetchState === 'In-Progress';
@@ -17,6 +17,10 @@ export const initialCompanyPassportView = {
17
17
  fetchState: 'Not-Started',
18
18
  error: undefined,
19
19
  },
20
+ customersTabSettingsUpdateStatus: {
21
+ fetchState: 'Not-Started',
22
+ error: undefined,
23
+ },
20
24
  needsQboProjectsReconnect: false,
21
25
  companyDetails: {
22
26
  localData: undefined,
@@ -1,4 +1,6 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
+ import isEqual from 'lodash/isEqual';
3
+ import { updateTenantCustomersTabSetting } from '../../entity/tenant/tenantReducer';
2
4
  import { clearAll } from '../../rootActions';
3
5
  import { CUSTOMER_REVENUE_KEYS, } from './customerSummaryViewState';
4
6
  export const initialState = {
@@ -20,6 +22,9 @@ export const initialState = {
20
22
  detailCustomerId: undefined,
21
23
  error: undefined,
22
24
  };
25
+ const toReference = (payload) => payload != null
26
+ ? { id: payload.customer_id, name: payload.customer_name ?? '' }
27
+ : undefined;
23
28
  export const toCustomerSummary = (payload) => ({
24
29
  id: payload.customer_id,
25
30
  qboId: payload.qbo_id ?? undefined,
@@ -28,6 +33,22 @@ export const toCustomerSummary = (payload) => ({
28
33
  currencyCode: payload.currency_code ?? undefined,
29
34
  currencySymbol: payload.currency_symbol ?? undefined,
30
35
  revenue: Object.fromEntries(CUSTOMER_REVENUE_KEYS.map((key) => [key, payload[key] ?? 0])),
36
+ hasChildren: payload.has_children === true,
37
+ isProject: payload.is_project === true,
38
+ isSubCustomer: payload.is_sub_customer === true,
39
+ parentId: payload.parent_customer_id ?? undefined,
40
+ parentPath: (payload.parent_path ?? []).flatMap((parent) => toReference(parent) ?? []),
41
+ familyTree: payload.family_tree?.map(toCustomerSummary),
42
+ // Only the details response carries them
43
+ totals: payload.total_balance != null
44
+ ? {
45
+ balance: payload.total_balance,
46
+ revenue: Object.fromEntries(CUSTOMER_REVENUE_KEYS.map((key) => [
47
+ key,
48
+ payload[`total_${key}`] ?? 0,
49
+ ])),
50
+ }
51
+ : undefined,
31
52
  account: payload.account?.account_id != null
32
53
  ? {
33
54
  id: payload.account.account_id,
@@ -73,11 +94,16 @@ const customerSummaryView = createSlice({
73
94
  },
74
95
  },
75
96
  updateCustomerSummaryList: {
76
- prepare(response, keepExistingListItems) {
77
- return { payload: { keepExistingListItems, response } };
97
+ prepare(response, keepExistingListItems, settings) {
98
+ return { payload: { keepExistingListItems, response, settings } };
78
99
  },
79
100
  reducer(draft, action) {
80
- const { response, keepExistingListItems } = action.payload;
101
+ const { response, keepExistingListItems, settings } = action.payload;
102
+ // A next page fetched under other settings than the listed rows: drop it, the list refetches
103
+ if (keepExistingListItems && !isEqual(draft.listSettings, settings)) {
104
+ return;
105
+ }
106
+ draft.listSettings = settings;
81
107
  const customers = (response.data?.customers ?? []).map(toCustomerSummary);
82
108
  const existingIds = new Set(draft.customers.map(({ id }) => id));
83
109
  draft.customers = keepExistingListItems
@@ -126,12 +152,13 @@ const customerSummaryView = createSlice({
126
152
  detail.customer = toCustomerSummary(action.payload.customer);
127
153
  detail.fetchState = 'Completed';
128
154
  // Keep the list row in sync after an edit.
155
+ const { account, accountingClass } = detail.customer;
129
156
  const index = draft.customers.findIndex(({ id }) => id === action.payload.customerId);
130
157
  if (index >= 0) {
131
158
  draft.customers[index] = {
132
159
  ...draft.customers[index],
133
- account: detail.customer.account,
134
- accountingClass: detail.customer.accountingClass,
160
+ account,
161
+ accountingClass,
135
162
  };
136
163
  }
137
164
  },
@@ -188,8 +215,14 @@ const customerSummaryView = createSlice({
188
215
  },
189
216
  },
190
217
  extraReducers: (builder) => {
191
- builder.addCase(clearAll, (draft) => {
218
+ builder
219
+ .addCase(clearAll, (draft) => {
192
220
  Object.assign(draft, initialState);
221
+ })
222
+ // Totals on / off switch the detail page's All transactions tab to another entity: restart its window.
223
+ // (The list refetches through `getCustomerSummaryList`; the details carry both figures.)
224
+ .addCase(updateTenantCustomersTabSetting, (draft) => {
225
+ draft.allTransactionsTabData = undefined;
193
226
  });
194
227
  },
195
228
  });
@@ -1,5 +1,8 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
- import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
2
+ import isEqual from 'lodash/isEqual';
3
+ import { toCustomerWithSubCustomersId } from '../../entity/genericEntity/customerWithSubCustomersId';
4
+ import { getIsAccountingClassesEnabled, getTenantCustomersTabSettings, } from '../../entity/tenant/tenantSelector';
5
+ import { DEFAULT_CUSTOMERS_TAB_SETTINGS, } from '../../entity/tenant/tenantState';
3
6
  import { getNestedAccountListHierarchy } from '../accountList/accountListSelector';
4
7
  import { getNestedClassListHierarchy } from '../classList/classListSelector';
5
8
  const isLoadingFetchState = (fetchState) => fetchState === 'Not-Started' || fetchState === 'In-Progress';
@@ -7,16 +10,43 @@ const NOT_STARTED_DETAIL = {
7
10
  fetchState: 'Not-Started',
8
11
  saveState: 'Not-Started',
9
12
  };
10
- export const getCustomerSummaryList = (state) => state.customerSummaryViewState;
11
- export const getCustomerSummaryDetail = createSelector((state, customerId) => state.customerSummaryViewState.detailById[customerId], (state) => state.accountListState, (state) => state.classListState, getIsAccountingClassesEnabled, (detail, accountListState, classListState, isAccountingClassesEnabled) => ({
12
- ...(detail ?? NOT_STARTED_DETAIL),
13
- accountsHierarchyList: getNestedAccountListHierarchy(accountListState, 'accountList'),
14
- classHierarchyList: getNestedClassListHierarchy(classListState),
15
- isAccountingClassesEnabled,
16
- isReferenceDataLoading: isLoadingFetchState(accountListState.byReportId.accountList.fetchState) ||
17
- (isAccountingClassesEnabled &&
18
- isLoadingFetchState(classListState.fetchState)),
19
- }));
13
+ export const getCustomersTabSettings = (state) => {
14
+ const { currentTenantId } = state.tenantState;
15
+ return ((currentTenantId != null
16
+ ? getTenantCustomersTabSettings(state.tenantState, currentTenantId)
17
+ : undefined) ?? DEFAULT_CUSTOMERS_TAB_SETTINGS);
18
+ };
19
+ /** Rows fetched under other Customers tab settings (however those changed) read as not fetched yet. */
20
+ export const getCustomerSummaryList = createSelector((state) => state.customerSummaryViewState, getCustomersTabSettings, (view, settings) => view.listSettings == null || isEqual(view.listSettings, settings)
21
+ ? view
22
+ : {
23
+ ...view,
24
+ customers: [],
25
+ error: undefined,
26
+ fetchState: 'Not-Started',
27
+ totalCount: 0,
28
+ uiState: { ...view.uiState, pageToken: null, scrollYOffset: 0 },
29
+ });
30
+ export const getCustomerSummaryDetail = createSelector((state, customerId) => state.customerSummaryViewState.detailById[customerId], (state) => state.accountListState, (state) => state.classListState, getIsAccountingClassesEnabled, (state) => getCustomersTabSettings(state).includeSubCustomersInTotals, (_state, customerId) => customerId, (detail, accountListState, classListState, isAccountingClassesEnabled, includeSubCustomersInTotals, customerId) => {
31
+ const customer = detail?.customer;
32
+ // Rolled up from the start (in parallel with the details); own figures if the details turn out to have no totals
33
+ const includesSubCustomers = includeSubCustomersInTotals &&
34
+ (customer == null || customer.totals != null);
35
+ return {
36
+ ...(detail ?? NOT_STARTED_DETAIL),
37
+ accountsHierarchyList: getNestedAccountListHierarchy(accountListState, 'accountList'),
38
+ classHierarchyList: getNestedClassListHierarchy(classListState),
39
+ isAccountingClassesEnabled,
40
+ isReferenceDataLoading: isLoadingFetchState(accountListState.byReportId.accountList.fetchState) ||
41
+ (isAccountingClassesEnabled &&
42
+ isLoadingFetchState(classListState.fetchState)),
43
+ includesSubCustomers,
44
+ figures: includesSubCustomers ? customer?.totals : customer,
45
+ trendEntityId: includesSubCustomers
46
+ ? toCustomerWithSubCustomersId(customerId)
47
+ : customerId,
48
+ };
49
+ });
20
50
  export const getCustomerSummarySelectedTabType = (state) => state.customerSummaryViewState.selectedTabType;
21
51
  export const getCustomerSummaryAllTransactionsTabData = (state) => state.customerSummaryViewState.allTransactionsTabData;
22
52
  export const getCustomerSummaryTrendFilter = (state) => state.customerSummaryViewState.trendFilter;
@@ -2,21 +2,26 @@ import { of } from 'rxjs';
2
2
  import { catchError, exhaustMap, filter, map, startWith, switchMap, } from 'rxjs/operators';
3
3
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
4
4
  import { fetchCustomerSummaryList, updateCustomerSummaryList, updateCustomerSummaryListFailure, } from '../customerSummaryViewReducer';
5
+ import { getCustomersTabSettings } from '../customerSummaryViewSelector';
5
6
  export const fetchCustomerSummaryListEpic = (actions$, state$, zeniAPI) => {
6
7
  const fetch$ = actions$.pipe(filter(fetchCustomerSummaryList.match));
7
8
  const fetchPage = (action) => {
8
9
  const { keepExistingListItems, pageToken, searchString } = action.payload;
9
10
  const { uiState } = state$.value.customerSummaryViewState;
11
+ const settings = getCustomersTabSettings(state$.value);
10
12
  const query = {
11
13
  page_token: pageToken,
12
14
  sort_by: uiState.sortKey,
13
15
  sort_order: uiState.sortOrder === 'ascending' ? 'asc' : 'desc',
14
16
  search_text: searchString,
17
+ // Top-level only: a search also finds a customer by its sub-customers' and projects' names
18
+ top_level_only: settings.listTopLevelCustomersOnly,
19
+ include_sub_customers: settings.includeSubCustomersInTotals,
15
20
  };
16
21
  return zeniAPI
17
22
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/customers/summary?query=${encodeURIComponent(JSON.stringify(query))}`)
18
23
  .pipe(map((response) => isSuccessResponse(response)
19
- ? updateCustomerSummaryList(response, keepExistingListItems)
24
+ ? updateCustomerSummaryList(response, keepExistingListItems, settings)
20
25
  : updateCustomerSummaryListFailure(response.status)), catchError((error) => of(updateCustomerSummaryListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch customer summary list REST API call errored out' +
21
26
  JSON.stringify(error))))));
22
27
  };
@@ -2,6 +2,7 @@ import lodashGet from 'lodash/get';
2
2
  import { from, of } from 'rxjs';
3
3
  import { catchError, filter, mergeMap } from 'rxjs/operators';
4
4
  import { toString } from '../../commonStateTypes/timePeriod';
5
+ import { fromCustomerEntityId } from '../../entity/genericEntity/customerWithSubCustomersId';
5
6
  import { updateTransactions } from '../../entity/transaction/transactionReducer';
6
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
7
8
  import { fetchEntityTransactionList, updateTransactionListByEntity, updateTransactionListByEntityFailure, } from './transactionListReducer';
@@ -13,12 +14,20 @@ export const fetchTransactionListByEntityEpic = (actions$, state$, zeniAPI) => a
13
14
  if (action.payload.cacheOverride === true ||
14
15
  entityTransactions == null ||
15
16
  entityTransactions.hasValidState() === false) {
17
+ // Customers tab rolled-up view: cached under its own id, sent as the customer id plus the flag
18
+ const { customerId: entityId, includeSubCustomers } = action.payload.entity.type === 'customer'
19
+ ? fromCustomerEntityId(action.payload.entity.id)
20
+ : {
21
+ customerId: action.payload.entity.id,
22
+ includeSubCustomers: false,
23
+ };
16
24
  const queryParam = {
17
25
  start_date: toString(action.payload.period.start),
18
26
  end_date: toString(action.payload.period.end),
19
27
  sort_by: 'create_time',
20
28
  sort_order: 'desc',
21
- entity_id: action.payload.entity.id,
29
+ entity_id: entityId,
30
+ ...(includeSubCustomers ? { include_sub_customers: true } : {}),
22
31
  entity_name: action.payload.entity.name,
23
32
  entity_type: action.payload.entity.type,
24
33
  is_fetch_all_transactions: action.payload.isFetchAllTransactions === true
@@ -31,7 +40,7 @@ export const fetchTransactionListByEntityEpic = (actions$, state$, zeniAPI) => a
31
40
  if (isSuccessResponse(response) && response.data != null) {
32
41
  const updateActions = [];
33
42
  updateActions.push(updateTransactions(response.data.transactions, (value) => value.transaction_id));
34
- updateActions.push(updateTransactionListByEntity(response.data));
43
+ updateActions.push(updateTransactionListByEntity(response.data, action.payload.entity.id));
35
44
  return from(updateActions);
36
45
  }
37
46
  else {
@@ -164,21 +164,30 @@ const transactionList = createSlice({
164
164
  };
165
165
  },
166
166
  },
167
- updateTransactionListByEntity(draft, action) {
168
- const transactionIDs = action.payload.transactions.map((transactionPayload) => transactionPayload.transaction_id);
169
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
170
- const entity = mapTransactionQueryMetaDataToEntity(action.payload.query);
171
- const key = getEntityTranKey(entity, {
172
- start: toAbsoluteDay(action.payload.query.start_date),
173
- end: toAbsoluteDay(action.payload.query.end_date),
174
- });
175
- draft.byEntity.transactionListByEntityKey[key] = {
176
- ...initialEntityTransactionState,
177
- fetchState: 'Completed',
178
- error: undefined,
179
- entity: entity,
180
- transactionIDs,
181
- };
167
+ updateTransactionListByEntity: {
168
+ // `entityId`: the requested entity's id when it differs from the query's (a rolled-up customer)
169
+ prepare(payload, entityId) {
170
+ return { payload, meta: { entityId } };
171
+ },
172
+ reducer(draft, action) {
173
+ const transactionIDs = action.payload.transactions.map((transactionPayload) => transactionPayload.transaction_id);
174
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
175
+ const entity = {
176
+ ...mapTransactionQueryMetaDataToEntity(action.payload.query),
177
+ ...(action.meta.entityId != null && { id: action.meta.entityId }),
178
+ };
179
+ const key = getEntityTranKey(entity, {
180
+ start: toAbsoluteDay(action.payload.query.start_date),
181
+ end: toAbsoluteDay(action.payload.query.end_date),
182
+ });
183
+ draft.byEntity.transactionListByEntityKey[key] = {
184
+ ...initialEntityTransactionState,
185
+ fetchState: 'Completed',
186
+ error: undefined,
187
+ entity: entity,
188
+ transactionIDs,
189
+ };
190
+ },
182
191
  },
183
192
  updateTransactionListByEntityFailure: {
184
193
  reducer(draft, action) {
@@ -3,6 +3,7 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { toTimePeriod, } from '../../commonStateTypes/timeframeTick';
4
4
  import { getCustomerIncomeTrendKey } from '../../entity/customerIncome/customerIncomeTrendKey';
5
5
  import { updateCustomerIncomeTrend } from '../../entity/customerIncome/customerIncomeTrendReducer';
6
+ import { fromCustomerEntityId } from '../../entity/genericEntity/customerWithSubCustomersId';
6
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
7
8
  import { date as zeniDate } from '../../zeniDayJS';
8
9
  import { fetchIncomeTrend, updateTrendFailure, updateTrendSuccess, } from './trendReducer';
@@ -25,11 +26,14 @@ switchMap((action) => {
25
26
  return of(updateTrendSuccess(requestedPeriod));
26
27
  }
27
28
  }
29
+ // The rolled-up view is cached under its own id; the API takes the customer id plus the flag
30
+ const { customerId: apiCustomerId, includeSubCustomers } = fromCustomerEntityId(customerId);
28
31
  const query = {
29
- entity_id: customerId,
32
+ entity_id: apiCustomerId,
30
33
  entity_type: 'customer',
31
- customer_id: customerId,
34
+ customer_id: apiCustomerId,
32
35
  customer_name: customerName,
36
+ ...(includeSubCustomers ? { include_sub_customers: true } : {}),
33
37
  ...(dateRange != null
34
38
  ? { start_date: dateRange.startDate, end_date: dateRange.endDate }
35
39
  : {}),