@zeniai/client-epic-state 5.3.18 → 5.3.20

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 (78) hide show
  1. package/lib/commonStateTypes/coaBalance/coaBalance.d.ts +9 -0
  2. package/lib/commonStateTypes/coaBalance/coaBalance.js +11 -0
  3. package/lib/entity/tenant/epic/fetchAllTenantsEpic.d.ts +2 -4
  4. package/lib/entity/tenant/epic/fetchAllTenantsEpic.js +50 -42
  5. package/lib/entity/tenant/tenantPayload.d.ts +17 -1
  6. package/lib/entity/tenant/tenantReducer.d.ts +7 -2
  7. package/lib/entity/tenant/tenantReducer.js +48 -19
  8. package/lib/entity/tenant/tenantSelector.d.ts +26 -6
  9. package/lib/entity/tenant/tenantSelector.js +116 -93
  10. package/lib/entity/tenant/tenantState.d.ts +10 -0
  11. package/lib/epic.d.ts +2 -1
  12. package/lib/epic.js +3 -0
  13. package/lib/esm/commonStateTypes/coaBalance/coaBalance.js +10 -0
  14. package/lib/esm/entity/tenant/epic/fetchAllTenantsEpic.js +53 -45
  15. package/lib/esm/entity/tenant/tenantReducer.js +46 -17
  16. package/lib/esm/entity/tenant/tenantSelector.js +112 -92
  17. package/lib/esm/epic.js +4 -1
  18. package/lib/esm/index.js +7 -7
  19. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +65 -3
  20. package/lib/esm/view/aiCfoView/aiCfoViewSelector.js +25 -0
  21. package/lib/esm/view/aiCfoView/epics/transcribeAudioEpic.js +60 -0
  22. package/lib/esm/view/balanceSheet/balanceSheetForTimeframeEpic.js +9 -4
  23. package/lib/esm/view/balanceSheet/balanceSheetReducer.js +2 -0
  24. package/lib/esm/view/cashFlow/cashFlowForTimeframeEpic.js +11 -5
  25. package/lib/esm/view/cashFlow/cashFlowReducer.js +2 -0
  26. package/lib/esm/view/dashboard/dashboardEpic.js +0 -4
  27. package/lib/esm/view/dashboard/dashboardSelector.js +2 -3
  28. package/lib/esm/view/financeStatement/financeStatementEpic.js +1 -5
  29. package/lib/esm/view/financeStatement/financeStatementSelector.js +18 -0
  30. package/lib/esm/view/people/peoplePageEpic.js +0 -4
  31. package/lib/esm/view/profitAndLoss/profitAndLossForTimeframeEpic.js +9 -4
  32. package/lib/esm/view/profitAndLoss/profitAndLossReducer.js +5 -1
  33. package/lib/esm/view/spendManagement/reimbursement/editRemiView/editRemiViewReducer.js +23 -3
  34. package/lib/esm/view/spendManagement/reimbursement/editRemiView/epics/fetchRemiAndInitializeLocalStoreEpic.js +17 -4
  35. package/lib/esm/view/spendManagement/reimbursement/remiDetailView/epics/fetchRemiDetailEpic.js +1 -1
  36. package/lib/esm/view/transactionList/filterTransactionsByTimePeriod.js +18 -0
  37. package/lib/esm/view/transactionList/transactionListByAccountSelector.js +2 -1
  38. package/lib/esm/view/transactionList/transactionListByCategoryTypeSelector.js +2 -1
  39. package/lib/esm/view/transactionList/transactionListByClassSelector.js +2 -1
  40. package/lib/esm/view/transactionList/transactionListByProjectSelector.js +2 -1
  41. package/lib/index.d.ts +8 -8
  42. package/lib/index.js +56 -46
  43. package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +20 -0
  44. package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +2 -2
  45. package/lib/view/aiCfoView/aiCfoViewReducer.js +66 -4
  46. package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +8 -1
  47. package/lib/view/aiCfoView/aiCfoViewSelector.js +27 -0
  48. package/lib/view/aiCfoView/aiCfoViewState.d.ts +9 -0
  49. package/lib/view/aiCfoView/epics/transcribeAudioEpic.d.ts +19 -0
  50. package/lib/view/aiCfoView/epics/transcribeAudioEpic.js +64 -0
  51. package/lib/view/balanceSheet/balanceSheetForTimeframeEpic.js +8 -3
  52. package/lib/view/balanceSheet/balanceSheetReducer.js +2 -0
  53. package/lib/view/balanceSheet/balanceSheetState.d.ts +2 -0
  54. package/lib/view/cashFlow/cashFlowForTimeframeEpic.js +10 -4
  55. package/lib/view/cashFlow/cashFlowReducer.js +2 -0
  56. package/lib/view/cashFlow/cashFlowState.d.ts +2 -0
  57. package/lib/view/dashboard/dashboardEpic.js +0 -4
  58. package/lib/view/dashboard/dashboardSelector.d.ts +0 -2
  59. package/lib/view/dashboard/dashboardSelector.js +1 -2
  60. package/lib/view/financeStatement/financeStatementEpic.js +1 -5
  61. package/lib/view/financeStatement/financeStatementSelector.d.ts +8 -0
  62. package/lib/view/financeStatement/financeStatementSelector.js +18 -0
  63. package/lib/view/people/peoplePageEpic.js +0 -4
  64. package/lib/view/profitAndLoss/profitAndLossForTimeframeEpic.js +8 -3
  65. package/lib/view/profitAndLoss/profitAndLossReducer.js +5 -1
  66. package/lib/view/profitAndLoss/profitAndLossState.d.ts +2 -1
  67. package/lib/view/spendManagement/reimbursement/editRemiView/editRemiViewReducer.d.ts +7 -2
  68. package/lib/view/spendManagement/reimbursement/editRemiView/editRemiViewReducer.js +24 -4
  69. package/lib/view/spendManagement/reimbursement/editRemiView/epics/fetchRemiAndInitializeLocalStoreEpic.d.ts +7 -4
  70. package/lib/view/spendManagement/reimbursement/editRemiView/epics/fetchRemiAndInitializeLocalStoreEpic.js +15 -1
  71. package/lib/view/spendManagement/reimbursement/remiDetailView/epics/fetchRemiDetailEpic.js +1 -1
  72. package/lib/view/transactionList/filterTransactionsByTimePeriod.d.ts +15 -0
  73. package/lib/view/transactionList/filterTransactionsByTimePeriod.js +22 -0
  74. package/lib/view/transactionList/transactionListByAccountSelector.js +2 -1
  75. package/lib/view/transactionList/transactionListByCategoryTypeSelector.js +2 -1
  76. package/lib/view/transactionList/transactionListByClassSelector.js +2 -1
  77. package/lib/view/transactionList/transactionListByProjectSelector.js +2 -1
  78. package/package.json +1 -1
@@ -17,6 +17,7 @@ export const initialState = {
17
17
  error: undefined,
18
18
  accountLocked: false,
19
19
  tenantsById: {},
20
+ tenantSummariesById: {},
20
21
  onboardingTenantsById: {},
21
22
  tenantIds: [],
22
23
  signInState: 'Not-Started',
@@ -107,26 +108,24 @@ const tenant = createSlice({
107
108
  });
108
109
  },
109
110
  },
110
- updateTenantsSuccess(draft, action) {
111
- const { tenants, user } = action.payload;
111
+ updateTenantSummariesSuccess(draft, action) {
112
112
  draft.fetchState = 'Completed';
113
113
  draft.error = undefined;
114
- const [tenantsbyId, tenantIds, firstUser] = mapTenantsPayloadToState(tenants, user);
115
- tenantIds.forEach((tenantId) => {
116
- const existingTenant = draft.tenantsById[tenantId];
117
- tenantsbyId[tenantId] = applyTreasuryPromoUserFieldsFromPayload(existingTenant, tenantsbyId[tenantId], user);
118
- if (existingTenant != null) {
119
- tenantsbyId[tenantId].capitalizableAccountOverrides =
120
- tenantsbyId[tenantId].capitalizableAccountOverrides ??
121
- existingTenant.capitalizableAccountOverrides;
122
- tenantsbyId[tenantId].capitalizationOnboardingDismissedAccounts =
123
- tenantsbyId[tenantId].capitalizationOnboardingDismissedAccounts ??
124
- existingTenant.capitalizationOnboardingDismissedAccounts;
114
+ const tenantSummariesById = {};
115
+ action.payload.forEach((payload) => {
116
+ tenantSummariesById[payload.tenant_id] =
117
+ mapTenantSummaryPayloadToTenantSummary(payload);
118
+ });
119
+ draft.tenantSummariesById = tenantSummariesById;
120
+ draft.tenantIds = Object.keys(tenantSummariesById);
121
+ // A tenant missing from the list is no longer accessible to the user.
122
+ const accessibleTenantsById = {};
123
+ Object.values(draft.tenantsById).forEach((tenant) => {
124
+ if (tenantSummariesById[tenant.tenantId] != null) {
125
+ accessibleTenantsById[tenant.tenantId] = tenant;
125
126
  }
126
127
  });
127
- draft.tenantsById = tenantsbyId;
128
- draft.tenantIds = tenantIds;
129
- draft.loggedInUser = assignWith(draft.loggedInUser ?? {}, firstUser, (objValue, srcValue) => srcValue ?? objValue);
128
+ draft.tenantsById = accessibleTenantsById;
130
129
  },
131
130
  updateTenantsFailure(draft, action) {
132
131
  draft.fetchState = 'Error';
@@ -134,6 +133,7 @@ const tenant = createSlice({
134
133
  draft.error = action.payload;
135
134
  draft.tenantIds = [];
136
135
  draft.tenantsById = {};
136
+ draft.tenantSummariesById = {};
137
137
  },
138
138
  updateTenantSuccess(draft, action) {
139
139
  const { tenants, user } = action.payload;
@@ -242,6 +242,7 @@ const tenant = createSlice({
242
242
  draft.signInState = 'Error';
243
243
  draft.tenantIds = [];
244
244
  draft.tenantsById = {};
245
+ draft.tenantSummariesById = {};
245
246
  },
246
247
  updateTenantReimbursementInfo(draft, action) {
247
248
  const { tenant_id, reimbursement_info } = action.payload;
@@ -271,6 +272,24 @@ const tenant = createSlice({
271
272
  updateCurrentTenant(draft, action) {
272
273
  draft.currentTenantId = action.payload;
273
274
  },
275
+ /**
276
+ * Records a tenant's ids known before the tenants list loads (the app's
277
+ * cached tenant list), so `getCurrentTenantIds` has them from the first
278
+ * render. The list is not changed and its own entries win.
279
+ */
280
+ seedTenantSummary(draft, action) {
281
+ const { companyId, tenantEmailDomain, tenantId, tenantName } = action.payload;
282
+ if (draft.tenantSummariesById[tenantId] == null) {
283
+ draft.tenantSummariesById[tenantId] = {
284
+ companyId,
285
+ companyName: tenantName ?? '',
286
+ tenantEmailDomain,
287
+ tenantId,
288
+ // Not in `tenantIds`, so it is never ordered or listed.
289
+ userLastCheckInTime: date('1970-01-01'),
290
+ };
291
+ }
292
+ },
274
293
  fetchExcludedResources: {
275
294
  prepare(userId, tenantId) {
276
295
  return { payload: { userId, tenantId } };
@@ -934,7 +953,7 @@ const tenant = createSlice({
934
953
  });
935
954
  },
936
955
  });
937
- export const { updateTenants, fetchAllTenants, updateTenantsSuccess, 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;
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;
938
957
  export default tenant.reducer;
939
958
  /**
940
959
  * Applies treasury promo user fields from the tenants API user block when present.
@@ -976,6 +995,16 @@ function mapTenantsPayloadToState(tenantsPayload, userTenantPayload) {
976
995
  const firstUser = mapTenantPayloadToUser(tenantsPayload[0]);
977
996
  return [tenantsById, tenantIds, firstUser];
978
997
  }
998
+ function mapTenantSummaryPayloadToTenantSummary(payload) {
999
+ return {
1000
+ companyId: payload.company_id,
1001
+ companyName: payload.company_name,
1002
+ tenantEmailDomain: payload.tenant_email_domain,
1003
+ tenantId: payload.tenant_id,
1004
+ // A tenant never opened sorts last, as in `mapTenantPayloadToTenant`.
1005
+ userLastCheckInTime: date(payload.user_last_check_in_time_into_tenant ?? '1970-01-01'),
1006
+ };
1007
+ }
979
1008
  function mapTenantPayloadToTenant(payload, userTenantPayload) {
980
1009
  const tenant = {
981
1010
  accountingClassIDs: payload.accounting_class_ids,
@@ -16,50 +16,75 @@ export function getTenant(tenantState, companyState, tenantId) {
16
16
  }
17
17
  }
18
18
  export function getTenantIdByCompanyId(tenantState, companyId) {
19
- return Object.values(tenantState.tenantsById).find((tenant) => tenant.companyId === companyId)?.tenantId;
19
+ return (Object.values(tenantState.tenantsById).find((tenant) => tenant.companyId === companyId) ??
20
+ Object.values(tenantState.tenantSummariesById).find((tenantSummary) => tenantSummary.companyId === companyId))?.tenantId;
20
21
  }
21
- function getTenantsByCheckInDate(tenantState, companyState) {
22
- const tenants = tenantState.tenantIds
23
- .map((tenantId) => getTenant(tenantState, companyState, tenantId))
24
- .filter((value) => value != null);
25
- const tenantsOrdered = orderBy(tenants, (value) => value.userLastCheckInTime.valueOf(), 'desc');
26
- return {
27
- order: 'descending_last_check_in',
28
- tenantsInOrder: tenantsOrdered,
29
- };
30
- }
31
- export const getTenantBaseViewForTenantId = createSelector((state) => state.tenantState, (state) => state.companyState, (_state, tenantId) => tenantId, (tenantState, companyState, tenantId) => {
22
+ /** One tenant's entry: its full record when loaded, else its list entry. */
23
+ function getTenantListEntry(tenantState, tenantId) {
32
24
  const tenant = tenantState.tenantsById[tenantId];
33
25
  if (tenant != null) {
34
- const companyId = tenant.companyId;
35
- const companyName = companyState.companiesById[companyId]?.companyInfo.name;
36
- const tenantName = tenant.companyName ?? companyName;
37
- const tenantEmailDomain = tenant.tenantEmailDomain;
38
- const userRole = tenant.userRole;
39
- const companyLocaleInfo = companyState.companiesById[companyId]?.companyLocaleInfo;
40
26
  return {
41
- tenantId,
42
- tenantName,
43
- tenantEmailDomain,
44
- companyId,
45
- companyName,
46
- userRole,
47
- companyLocaleInfo,
27
+ companyId: tenant.companyId,
28
+ companyName: tenant.companyName,
29
+ tenantEmailDomain: tenant.tenantEmailDomain,
30
+ tenantId: tenant.tenantId,
31
+ userLastCheckInTime: tenant.userLastCheckInTime,
32
+ userRole: tenant.userRole,
48
33
  };
49
34
  }
50
- return undefined;
51
- });
52
- const getCurrentTenantHelper = (tenantState, companyState) => {
53
- const { tenantIds, currentTenantId, excludedResourcesByTenant, subscriptionSummaryByTenant, } = tenantState;
54
- let tenant = undefined;
35
+ const tenantSummary = tenantState.tenantSummariesById[tenantId];
36
+ return tenantSummary != null ? { ...tenantSummary, userRole: [] } : undefined;
37
+ }
38
+ /**
39
+ * Every tenant in the list, most recently opened first. Roles are known only
40
+ * for tenants whose full record is loaded; the list itself carries none.
41
+ */
42
+ function getTenantListEntriesByCheckInDate(tenantState) {
43
+ const entries = tenantState.tenantIds
44
+ .map((tenantId) => getTenantListEntry(tenantState, tenantId))
45
+ .filter((entry) => entry != null);
46
+ return orderBy(entries, (entry) => entry.userLastCheckInTime.valueOf(), 'desc');
47
+ }
48
+ /**
49
+ * The selected tenant's full record. With no tenant selected, or one the user
50
+ * can no longer open, it is the most recently opened loaded tenant. A selected
51
+ * tenant that is in the list but not loaded yet reads as no tenant until its
52
+ * fetch lands, so nothing renders another tenant's record in its place.
53
+ */
54
+ function getCurrentTenantView(tenantState, companyState) {
55
+ const { currentTenantId } = tenantState;
55
56
  if (currentTenantId != null) {
56
- tenant = getTenant(tenantState, companyState, currentTenantId);
57
- }
58
- if (tenant == null && tenantIds.length > 0) {
59
- const tenants = getTenantsByCheckInDate(tenantState, companyState);
60
- tenant =
61
- tenants.tenantsInOrder.length > 0 ? tenants.tenantsInOrder[0] : undefined;
57
+ const tenant = getTenant(tenantState, companyState, currentTenantId);
58
+ if (tenant != null ||
59
+ tenantState.tenantSummariesById[currentTenantId] != null) {
60
+ return tenant;
61
+ }
62
62
  }
63
+ const mostRecentLoadedTenant = getTenantListEntriesByCheckInDate(tenantState).find((entry) => tenantState.tenantsById[entry.tenantId] != null);
64
+ return mostRecentLoadedTenant != null
65
+ ? getTenant(tenantState, companyState, mostRecentLoadedTenant.tenantId)
66
+ : undefined;
67
+ }
68
+ /** Whether the tenant's full record is loaded, not just its list entry. */
69
+ export const isTenantLoaded = (state, tenantId) => state.tenantState.tenantsById[tenantId] != null;
70
+ /**
71
+ * Whether the selected tenant's full record is loaded. Screens that act on
72
+ * roles or product settings wait for this rather than for either tenant fetch
73
+ * to complete: the tenants list alone carries neither.
74
+ */
75
+ export const isCurrentTenantLoaded = (state) => {
76
+ const { currentTenantId } = state.tenantState;
77
+ return currentTenantId != null && isTenantLoaded(state, currentTenantId);
78
+ };
79
+ export const getTenantBaseViewForTenantId = createSelector((state) => state.tenantState, (state) => state.companyState, (_state, tenantId) => tenantId, (tenantState, companyState, tenantId) => {
80
+ const entry = getTenantListEntry(tenantState, tenantId);
81
+ return entry != null
82
+ ? getTenantBaseViewForEntry(entry, companyState)
83
+ : undefined;
84
+ });
85
+ const getCurrentTenantHelper = (tenantState, companyState) => {
86
+ const { currentTenantId, excludedResourcesByTenant, subscriptionSummaryByTenant, } = tenantState;
87
+ const tenant = getCurrentTenantView(tenantState, companyState);
63
88
  let excludedResources = undefined;
64
89
  if (tenant != null) {
65
90
  excludedResources = excludedResourcesByTenant[tenant.tenantId];
@@ -120,9 +145,34 @@ const getCurrentTenantHelper = (tenantState, companyState) => {
120
145
  export const getCurrentTenant = createSelector((state) => state.tenantState, (state) => state.companyState, (tenantState, companyState) => {
121
146
  return getCurrentTenantHelper(tenantState, companyState);
122
147
  });
123
- export const getTenantsByCheckInDateSelector = createSelector((state) => state.tenantState, (state) => state.companyState, (tenantState, companyState) => {
124
- return getTenantsByCheckInDate(tenantState, companyState);
148
+ /**
149
+ * The selected tenant's id as soon as it is chosen. Unlike `getCurrentTenant`,
150
+ * it is set before the tenant fetch lands (boot reads it from the cached
151
+ * tenant list).
152
+ */
153
+ export const getCurrentTenantId = (state) => state.tenantState.currentTenantId;
154
+ const getCurrentTenantIdValues = createSelector((state) => state.tenantState, (state) => state.companyState, (tenantState, companyState) => {
155
+ const tenant = getCurrentTenantView(tenantState, companyState);
156
+ if (tenant != null) {
157
+ return { tenantId: tenant.tenantId, companyId: tenant.companyId };
158
+ }
159
+ const { currentTenantId } = tenantState;
160
+ const tenantSummary = currentTenantId != null
161
+ ? tenantState.tenantSummariesById[currentTenantId]
162
+ : undefined;
163
+ return {
164
+ tenantId: tenantSummary?.tenantId,
165
+ companyId: tenantSummary?.companyId,
166
+ };
125
167
  });
168
+ /**
169
+ * The current tenant's ids before its record loads: from the record when
170
+ * loaded, otherwise from the tenants list or the entry seeded from the app's
171
+ * cache (`seedTenantSummary`). Providers keyed on them (Statsig, collaboration)
172
+ * then mount once with the final ids, on a reload and on a tenant switch.
173
+ * The result keeps its identity while the ids are unchanged.
174
+ */
175
+ export const getCurrentTenantIds = createSelector((state) => getCurrentTenantIdValues(state).tenantId, (state) => getCurrentTenantIdValues(state).companyId, (tenantId, companyId) => ({ tenantId, companyId }));
126
176
  export function getOnboardingTenantByTenantId(tenantState, tenantId) {
127
177
  const tenant = tenantState.onboardingTenantsById[tenantId];
128
178
  if (tenant != null) {
@@ -133,10 +183,7 @@ export function getOnboardingTenantByTenantId(tenantState, tenantId) {
133
183
  }
134
184
  }
135
185
  export const getTenantBaseById = createSelector((state) => state.tenantState, (state) => state.companyState, (tenantState, companyState) => {
136
- return tenantState.tenantIds.map((tenantId) => {
137
- const tenant = tenantState.tenantsById[tenantId];
138
- return getTenantBaseViewForTenant(tenant, companyState);
139
- });
186
+ return getTenantListEntriesByCheckInDate(tenantState).map((entry) => getTenantBaseViewForEntry(entry, companyState));
140
187
  });
141
188
  export const getTenantsBaseByCheckInDateSelector = createSelector((state) => state.tenantState, (state) => state.companyState, (tenantState, companyState) => {
142
189
  return getTenantsBaseByCheckInDate(tenantState, companyState);
@@ -165,16 +212,8 @@ export const isTenantUsingZeniCOA = (currentTenant) => {
165
212
  return currentTenant.company?.companyInfo.isZeniCOAInUse ?? true; // defaulting to true
166
213
  };
167
214
  export const getCurrentTenantExternalConnections = (tenantState, companyState) => {
168
- const { externalConnectionsByTenant, tenantIds, currentTenantId } = tenantState;
169
- let tenant = undefined;
170
- if (currentTenantId != null) {
171
- tenant = getTenant(tenantState, companyState, currentTenantId);
172
- }
173
- if (tenant == null && tenantIds.length > 0) {
174
- const tenants = getTenantsByCheckInDate(tenantState, companyState);
175
- tenant =
176
- tenants.tenantsInOrder.length > 0 ? tenants.tenantsInOrder[0] : undefined;
177
- }
215
+ const { externalConnectionsByTenant, currentTenantId } = tenantState;
216
+ const tenant = getCurrentTenantView(tenantState, companyState);
178
217
  let externalConnections = undefined;
179
218
  if (tenant != null) {
180
219
  externalConnections = externalConnectionsByTenant[tenant.tenantId];
@@ -188,58 +227,39 @@ export const getCurrentTenantExternalConnections = (tenantState, companyState) =
188
227
  * Helpers
189
228
  */
190
229
  function getTenantsBaseByCheckInDate(tenantState, companyState) {
191
- const tenants = tenantState.tenantIds
192
- .map((tenantId) => getTenant(tenantState, companyState, tenantId))
193
- .filter((value) => value != null);
194
- const tenantsOrdered = orderBy(tenants, (value) => value.userLastCheckInTime.valueOf(), 'desc');
195
- const tenantsBaseOrdered = tenantsOrdered.map((tenant) => getTenantBaseViewForTenant(tenant, companyState));
196
230
  return {
197
231
  order: 'descending_last_check_in',
198
- tenantsBaseInOrder: tenantsBaseOrdered,
232
+ tenantsBaseInOrder: getTenantListEntriesByCheckInDate(tenantState).map((entry) => getTenantBaseViewForEntry(entry, companyState)),
199
233
  };
200
234
  }
201
- const getTenantBaseViewForTenant = (tenant, companyState) => {
202
- const tenantId = tenant.tenantId;
203
- const companyId = tenant.companyId;
204
- const companyName = companyState.companiesById[companyId]?.companyInfo.name;
205
- const tenantName = tenant.companyName ?? companyName ?? tenant.tenantEmailDomain ?? '';
206
- const tenantEmailDomain = tenant.tenantEmailDomain;
207
- const userRole = tenant.userRole;
208
- const companyLocaleInfo = companyState.companiesById[companyId]?.companyLocaleInfo;
235
+ const getTenantBaseViewForEntry = (entry, companyState) => {
236
+ const company = companyState.companiesById[entry.companyId];
209
237
  return {
210
- tenantId,
211
- tenantName,
212
- tenantEmailDomain,
213
- companyId,
214
- companyName,
215
- userRole,
216
- companyLocaleInfo,
238
+ tenantId: entry.tenantId,
239
+ tenantName: entry.companyName ??
240
+ company?.companyInfo.name ??
241
+ entry.tenantEmailDomain ??
242
+ '',
243
+ tenantEmailDomain: entry.tenantEmailDomain,
244
+ companyId: entry.companyId,
245
+ // Companies are loaded only for opened tenants; the list carries the name.
246
+ companyName: company?.companyInfo.name ?? entry.companyName,
247
+ userRole: entry.userRole,
248
+ companyLocaleInfo: company?.companyLocaleInfo,
217
249
  };
218
250
  };
219
251
  function getTenantCoreDetailsByCheckInDate(tenantState, companyState) {
220
- const tenants = tenantState.tenantIds
221
- .map((tenantId) => getTenant(tenantState, companyState, tenantId))
222
- .filter((value) => value != null);
223
- const tenantsOrdered = orderBy(tenants, (value) => value.userLastCheckInTime.valueOf(), 'desc');
224
- const tenantsCoreDetailsOrdered = tenantsOrdered.map((tenant) => getTenantCoreDetailsViewForTenant(tenant, companyState));
225
252
  return {
226
253
  order: 'descending_last_check_in',
227
- tenantsCoreDetailsInOrder: tenantsCoreDetailsOrdered,
254
+ tenantsCoreDetailsInOrder: getTenantListEntriesByCheckInDate(tenantState).map((entry) => ({
255
+ tenantId: entry.tenantId,
256
+ tenantName: entry.companyName ??
257
+ companyState.companiesById[entry.companyId]?.companyInfo.name,
258
+ tenantEmailDomain: entry.tenantEmailDomain,
259
+ companyId: entry.companyId,
260
+ })),
228
261
  };
229
262
  }
230
- const getTenantCoreDetailsViewForTenant = (tenant, companyState) => {
231
- const tenantId = tenant.tenantId;
232
- const companyId = tenant.companyId;
233
- const companyName = companyState.companiesById[companyId]?.companyInfo.name;
234
- const tenantName = tenant.companyName ?? companyName;
235
- const tenantEmailDomain = tenant.tenantEmailDomain;
236
- return {
237
- tenantId,
238
- tenantName,
239
- tenantEmailDomain,
240
- companyId,
241
- };
242
- };
243
263
  export const toTenantCoreDetailsView = (tenantsBaseView) => {
244
264
  return tenantsBaseView.map(({ tenantId, tenantName, tenantEmailDomain, companyId }) => {
245
265
  return {
package/lib/esm/epic.js CHANGED
@@ -70,6 +70,7 @@ import { stopSubmitEpic, } from './view/aiCfoView/epics/stopSubmitEpic';
70
70
  import { stopSubmitQuestionEpic, } from './view/aiCfoView/epics/stopSubmitQuestionEpic';
71
71
  import { submitFeedbackEpic, } from './view/aiCfoView/epics/submitFeedbackEpic';
72
72
  import { submitQuestionEpic, } from './view/aiCfoView/epics/submitQuestionEpic';
73
+ import { transcribeAudioEpic, } from './view/aiCfoView/epics/transcribeAudioEpic';
73
74
  import { fetchApAgingDetailEpic, } from './view/apAgingView/apAgingDetail/fetchApAgingDetailEpic';
74
75
  import { fetchApAgingEpic, } from './view/apAgingView/apAgingReport/fetchApAgingEpic';
75
76
  import { fetchArAgingDetailEpic, } from './view/arAgingView/arAgingDetail/fetchArAgingDetailEpic';
@@ -635,7 +636,7 @@ import { saveRealTimeApprovalEpic, } from './view/spendManagement/realTimeApprov
635
636
  import { fetchCurrencyConversionValueEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/fetchCurrencyConversionValueEpic';
636
637
  import { fetchEditRemiDetailPageEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/fetchEditRemiDetailPageEpic';
637
638
  import { fetchRecommendationsAndUpdateMerchantRecommendationsEpic, waitForMerchantRecommendationFetchThenUpdateRecommendationInMerchantEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/fetchRecommendationsAndUpdateMerchantRecommendationsEpic';
638
- import { fetchRemiAndInitializeLocalStoreEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/fetchRemiAndInitializeLocalStoreEpic';
639
+ import { fetchRemiAndInitializeLocalStoreEpic, waitForRemiDetailThenInitializeLocalStoreEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/fetchRemiAndInitializeLocalStoreEpic';
639
640
  import { initializeRemiToLocalStoreEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/initializeRemiToLocalStoreEpic';
640
641
  import { parseReceiptsToRemiEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/parseReceiptsToRemiEpic';
641
642
  import { saveRemiDetailEpic, } from './view/spendManagement/reimbursement/editRemiView/epics/saveRemiDetailEpic';
@@ -1484,6 +1485,7 @@ const ALL_EPICS = [
1484
1485
  syncInvoicingSubscriptionCouponsEpic,
1485
1486
  syncTabsAfterAutomatchEpic,
1486
1487
  toggleReportUIOptionForecastModeEpic,
1488
+ transcribeAudioEpic,
1487
1489
  transferMoneyEpic,
1488
1490
  treasuryTransferMoneyEpic,
1489
1491
  triggerAiAccountantJobEpic,
@@ -1574,6 +1576,7 @@ const ALL_EPICS = [
1574
1576
  waitForForecastListThenFetchOpExWithForecastEpic,
1575
1577
  waitForForecastListThenFetchRevenueWithForecastEpic,
1576
1578
  waitForMerchantRecommendationFetchThenUpdateRecommendationInMerchantEpic,
1579
+ waitForRemiDetailThenInitializeLocalStoreEpic,
1577
1580
  waitForVendorByIdThenSaveBillUpdatetoLocalStoreEpic,
1578
1581
  waitForVendorByNameThenParsetoLocalStoreEpic,
1579
1582
  waitForVendorByNameThenUpdateBillDetailEpic,
package/lib/esm/index.js CHANGED
@@ -72,8 +72,8 @@ import { closeSnackbar, openSnackbar } from './entity/snackbar/snackbarReducer';
72
72
  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
- import { deleteConnection, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, initEmailConnectOAuth, resendVerifyDeviceOTP, resetSignInState, saveAPIKeyConnection, saveConnectorCredentials, saveExternalConnection as saveExternalConnectionForTenant, saveOAuthConnection, sendEmailMagicLinkToUser, sendSessionHeartbeat, toExternalIntegrationType, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
76
- import { DEFAULT_RECEIPTS_EMAIL, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getReceiptsEmail, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
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';
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';
@@ -332,7 +332,7 @@ import { clearInternationalWire, createPaymentInstrument, createPaymentInstrumen
332
332
  import { getInternationalWireView, } from './view/spendManagement/billPay/internationalWire/internationalWireSelector';
333
333
  import { fetchInternationalVerificationForm, submitInternationalVerificationForm, updateVerificationFormLocalData, } from './view/spendManagement/billPay/internationalWireVerification/internationalWireVerificationReducer';
334
334
  import { getIntlWireVerificationView, } from './view/spendManagement/billPay/internationalWireVerification/internationalWireVerificationSelector';
335
- import { fetchBillAttachment, fetchMagicLinkBankNameByRouting, fetchMagicLinkBankNameBySwift, fetchMagicLinkTenant, saveBankAccount, saveMagicLinkAddressInLocalStore, updateMagicLinkBankAccountLocalStoreData, updateMagicLinkInternationalBankAccountLocalStoreData, } from './view/spendManagement/billPay/magicLinkView/magicLinkViewReducer';
335
+ import { fetchBillAttachment, fetchMagicLinkBankNameByRouting, fetchMagicLinkBankNameBySwift, fetchMagicLinkTenant, saveBankAccount, saveMagicLinkAddressInLocalStore, updateMagicLinkBankAccountLocalStoreData, updateMagicLinkInternationalBankAccountLocalStoreData, updateMagicLinkTenantFailure, updateMagicLinkTenantSuccess, } from './view/spendManagement/billPay/magicLinkView/magicLinkViewReducer';
336
336
  import { getMagicLinkBankAccountView, getMagicLinkCurrentAddressState, getMagicLinkView, } from './view/spendManagement/billPay/magicLinkView/magicLinkViewSelector';
337
337
  import { fetchWiseRedirectStatus } from './view/spendManagement/billPay/wiseRedirectView/wiseRedirectReducer';
338
338
  import { getWiseRedirect } from './view/spendManagement/billPay/wiseRedirectView/wiseRedirectSelector';
@@ -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, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getIsAutoReviewIncomeWithoutCustomerEnabled, getTenantsByCheckInDateSelector, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, fetchActiveTenant, fetchAllTenants, 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, 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 };
@@ -661,7 +661,7 @@ export { isZeniClearingAccountReport, ZENI_CLEARING_ACCOUNT, isZeniClearingAccou
661
661
  export { fetchOnboardingCustomerView, fetchOnboardingCustomerSetupView, getOnboardingPlaidLinkToken, establishOnboardingPlaidConnection, updateOnboardingCustomerViewAccountDetails, updateOnboardingCustomerViewCompleteStatus, updateOnboardingCustomerViewDashboardLoaded, updateOnboardingPaymentAccountLoginStatus, updateOnboardingPaymentAccountStatus, updateOnboardingCustomerView, updateOnboardingCustomerViewLocalStoreData, updateCurrentStep, updateCurrentSubStep, updateOnboardingCustomerViewUIState, saveOnboardingCompnayOfficerPhoneInLocalStore, saveOnboardingCustomerViewDataInLocalStore, clearOnboardingCustomerViewDataInLocalStore, applyKycDocumentAutofillForOnboarding, applyKybDocumentAutofillForOnboarding, clearKycKybAutofillForOnboarding, acknowledgeOnboardingAiActivationViewed, acknowledgeOnboardingAiFinanceTeam, fetchAiAgentsActivationStatus, aiAgentsActivationCountsUpdated, aiAgentsActivationPhaseChanged, getAiAgentsActivationCounts, getAiAgentsActivationPhases, getAiAgentsActivationSubview, getOnboardingCustomerView, getProductSettingsString, ALL_ONBOARDING_STEPS, ALL_ONBOARDING_SUB_STEPS, toOnboardingStepType, toOnboardingSubStepType, };
662
662
  export { saveAccountMapping, saveAccountMappingLocalData, clearAccountMappingLocalData, getAccountMappingView, initializeAccountMappingView, };
663
663
  export { getMagicLinkView, getMagicLinkBankAccountView, getMagicLinkCurrentAddressState, };
664
- export { fetchMagicLinkTenant, fetchBillAttachment, saveBankAccount, updateMagicLinkBankAccountLocalStoreData, updateMagicLinkInternationalBankAccountLocalStoreData, fetchMagicLinkBankNameByRouting, fetchMagicLinkBankNameBySwift, saveMagicLinkAddressInLocalStore, };
664
+ export { fetchMagicLinkTenant, updateMagicLinkTenantSuccess, updateMagicLinkTenantFailure, fetchBillAttachment, saveBankAccount, updateMagicLinkBankAccountLocalStoreData, updateMagicLinkInternationalBankAccountLocalStoreData, fetchMagicLinkBankNameByRouting, fetchMagicLinkBankNameBySwift, saveMagicLinkAddressInLocalStore, };
665
665
  export { getWiseRedirect, fetchWiseRedirectStatus };
666
666
  export { getInternationalWireView, initializeInternationalWireLocalData, updateInternationalWireLocalStoreData, initializeDynamicForm, fetchInternationalWireDynamicForm, createPaymentInstrument, createPaymentInstrumentUpdateStatus, deletePaymentInstrument, clearInternationalWire, };
667
667
  export { fetchUserRoleConfig, getUserRoleConfig };
@@ -705,8 +705,8 @@ export { updateAiAccountantCustomers, updateAiAccountantCustomer, updateAiAccoun
705
705
  export { getAiAccountantCustomers, getAiAccountantJobsByTenantId, } from './entity/aiAccountantCustomer/aiAccountantCustomerSelector';
706
706
  export { cancelAiAccountantOnboarding, fetchAiAccountantCustomers, fetchAiAccountantJobs, setSelectedTenantIdsForJobTrigger, triggerAiAccountantJob, updateAiAccountantUIState, } from './view/aiAccountantView/aiAccountantViewReducer';
707
707
  export { getAiAccountantCockpitView, } from './view/aiAccountantView/aiAccountantViewSelector';
708
- export { acceptMasterTOS, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, deleteChatSession, deleteRoutine, deleteSkill, fetchChatHistory, fetchChatSessionsForUser, fetchRoutineRuns, fetchRoutines, fetchSkillDetail, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersions, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, openRoutinesBrowser, openSkillsBrowser, runRoutine, saveRoutine, saveSkill, setRoutineEnabled, setSession, setSkillVisibility, stopSubmit, stopSubmitQuestion, submitFeedback, submitQuestion, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } from './view/aiCfoView/aiCfoViewReducer';
709
- export { canCreateRoutine, getAiCfoView, getRoutineMutation, getRoutineRuns, getRoutines, getRoutinesEnabled, getSkillDetail, getSkillMutation, getSkills, getSkillsEnabled, getSkillVersionBody, getSkillVersions, getSuggestedQuestionsForPageContext, } from './view/aiCfoView/aiCfoViewSelector';
708
+ export { acceptMasterTOS, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, deleteChatSession, deleteRoutine, deleteSkill, fetchChatHistory, fetchChatSessionsForUser, fetchRoutineRuns, fetchRoutines, fetchSkillDetail, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersions, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, openRoutinesBrowser, openSkillsBrowser, runRoutine, saveRoutine, saveSkill, setRoutineEnabled, setSession, setSkillVisibility, stopSubmit, stopSubmitQuestion, submitFeedback, submitQuestion, transcribeAudio, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } from './view/aiCfoView/aiCfoViewReducer';
709
+ export { canCreateRoutine, getAiCfoView, getIsTranscribing, getRoutineMutation, getRoutineRuns, getRoutines, getRoutinesEnabled, getSkillDetail, getSkillMutation, getSkills, getSkillsEnabled, getSkillVersionBody, getSkillVersions, getSuggestedQuestionsForPageContext, getTranscriptionFailed, } from './view/aiCfoView/aiCfoViewSelector';
710
710
  export { setNewSession, setSessions, updateAiCfoAnswerCardPolicyWizardPlan, upsertAnswerPayload, upsertOrAddQuestionAnswerPayload, addQuestionPayload, clearSession, toAiCfoExports, toAiCfoVisualization, appendSyntheticAiCfoAnswer, updateSyntheticAiCfoAnswer, removeSyntheticAiCfoAnswer, } from './entity/aiCfo/aiCfoReducer';
711
711
  export { getAllQuestionAnswersForChatSession, getQuestionAnswerByIdForChatSession, getAllQuestionsForChatSession, getAiCfoSelectorView, getSyntheticAiCfoAnswersForChatSession, getSyntheticAiCfoAnswerByQuestionAnswerId, } from './entity/aiCfo/aiCfoSelector';
712
712
  export { ALL_AI_CFO_CHARTS_TYPES, ALL_AI_CFO_VISUALIZATION_TYPES, ALL_AI_CFO_ANSWER_STATE_TYPES, ALL_AI_CFO_ANSWER_RESPONSE_TYPES, ALL_INTERACTIVE_FORM_TYPES, toAiCfoVisualizationType, toAiCfoVisualizationTypeStrict, toAiCfoChartType, toAiCfoChartTypeStrict, toAiCfoAnswerStateType, toAiCfoAnswerStateTypeStrict, toAiCfoAnswerResponseType, toAiCfoAnswerResponseTypeStrict, toInteractiveFormType, toInteractiveFormTypeStrict, ALL_SYNTHETIC_AI_CFO_ANSWER_KINDS, toMessageSender, toMessageType, } from './entity/aiCfo/aiCfoState';
@@ -47,6 +47,8 @@ export const initialAiCfoViewState = {
47
47
  fetchChatSessionHistoryByChatSessionId: {},
48
48
  responseStateByChatSessionId: {},
49
49
  stopSubmitStateByChatSessionId: {},
50
+ transcriptionState: { fetchState: 'Not-Started', error: undefined },
51
+ transcribingChatSessionId: undefined,
50
52
  suggestedQuestionsByPageContext: {},
51
53
  skillDetail: {
52
54
  fetchState: 'Not-Started',
@@ -147,6 +149,19 @@ function closeSkillsBrowserState(draft) {
147
149
  draft.uiState.isSkillsBrowserOpen = false;
148
150
  clearOpenSkillState(draft);
149
151
  }
152
+ /**
153
+ * Clear a finished dictation attempt when the conversation moves on, so a failed one
154
+ * stops showing "Could not transcribe that recording" in every later chat. A running
155
+ * take keeps its state — the composer is showing a live "Transcribing…" for it.
156
+ */
157
+ function resetFinishedTranscription(draft) {
158
+ if (draft.transcriptionState.fetchState === 'In-Progress') {
159
+ return;
160
+ }
161
+ draft.transcriptionState.fetchState = 'Not-Started';
162
+ draft.transcriptionState.error = undefined;
163
+ draft.transcribingChatSessionId = undefined;
164
+ }
150
165
  const aiCfoView = createSlice({
151
166
  name: 'aiCfoView',
152
167
  initialState: initialAiCfoViewState,
@@ -154,6 +169,7 @@ const aiCfoView = createSlice({
154
169
  submitQuestion: {
155
170
  reducer(draft, action) {
156
171
  const { sessionId } = action.payload;
172
+ resetFinishedTranscription(draft);
157
173
  // A turn is being sent, so the chat body has to be what the user sees. Doing this
158
174
  // here rather than at each call site is deliberate: the composer, the sample
159
175
  // chips, Run, and the `/` menu all submit, and every one of them was a separate
@@ -299,6 +315,7 @@ const aiCfoView = createSlice({
299
315
  setSession(draft, action) {
300
316
  draft.uiState.currentChatSessionId = action.payload.sessionId;
301
317
  draft.uiState.agentId = action.payload.agentId;
318
+ resetFinishedTranscription(draft);
302
319
  // Picking a session means wanting to see that chat, so the browser cannot stay over
303
320
  // it. In the reducer because there are six dispatch sites across three separate
304
321
  // rails, and covering them one at a time missed half of them.
@@ -351,6 +368,7 @@ const aiCfoView = createSlice({
351
368
  createSessionAndSubmit: {
352
369
  reducer(draft, action) {
353
370
  draft.uiState.agentId = action.payload.agentId;
371
+ resetFinishedTranscription(draft);
354
372
  draft.createSessionAndSubmitState.fetchState = 'In-Progress';
355
373
  draft.createSessionAndSubmitState.error = undefined;
356
374
  },
@@ -551,7 +569,49 @@ const aiCfoView = createSlice({
551
569
  return { payload: { chatMessageId, error } };
552
570
  },
553
571
  },
572
+ transcribeAudio: {
573
+ reducer(draft, action) {
574
+ draft.transcriptionState.fetchState = 'In-Progress';
575
+ draft.transcriptionState.error = undefined;
576
+ draft.transcribingChatSessionId = action.payload.chatSessionId;
577
+ },
578
+ prepare(audio, language, chatSessionId) {
579
+ return { payload: { audio, chatSessionId, language } };
580
+ },
581
+ },
582
+ transcribeAudioSuccess: {
583
+ reducer(draft, action) {
584
+ draft.transcriptionState.fetchState = 'Completed';
585
+ draft.transcriptionState.error = undefined;
586
+ // The user moved to another session while this was in flight — dropping it
587
+ // is the only safe option, since appending would put their words in a
588
+ // conversation they did not dictate into.
589
+ if (action.payload.chatSessionId !== draft.uiState.currentChatSessionId) {
590
+ return;
591
+ }
592
+ const transcript = action.payload.transcript.trim();
593
+ if (transcript === '') {
594
+ // Silence, not a failure: leave the composer untouched so the user can
595
+ // simply speak again rather than hunting for what went wrong.
596
+ return;
597
+ }
598
+ // Append to whatever is in the box now rather than to a snapshot taken when
599
+ // recording began — a take produces exactly one result, so there is nothing
600
+ // to reconcile, and anything typed while transcribing survives.
601
+ const existing = draft.uiState.currentInput.trim();
602
+ draft.uiState.currentInput =
603
+ existing === '' ? transcript : `${existing} ${transcript}`;
604
+ },
605
+ prepare(transcript, chatSessionId) {
606
+ return { payload: { chatSessionId, transcript } };
607
+ },
608
+ },
609
+ transcribeAudioFailure(draft, action) {
610
+ draft.transcriptionState.fetchState = 'Error';
611
+ draft.transcriptionState.error = action.payload;
612
+ },
554
613
  clearCurrentSessionId(draft) {
614
+ resetFinishedTranscription(draft);
555
615
  // New Chat: the empty composer is the point, so the browser cannot stay over it.
556
616
  if (draft.uiState.isSkillsBrowserOpen === true) {
557
617
  closeSkillsBrowserState(draft);
@@ -835,7 +895,8 @@ const aiCfoView = createSlice({
835
895
  // The server has it. Nothing it sends from here on will contain this row,
836
896
  // so the tombstone has done its job and holding it any longer is what
837
897
  // makes a routine recreated under the same id invisible forever.
838
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
898
+ draft.routines.deletedScheduleIds =
899
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
839
900
  draft.routineMutation = {
840
901
  fetchState: 'Completed',
841
902
  error: undefined,
@@ -847,7 +908,8 @@ const aiCfoView = createSlice({
847
908
  // is what lets the refetch put the row back — leaving it hidden would
848
909
  // tell the user they had stopped something they had not, which is the
849
910
  // worst outcome this list can produce.
850
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
911
+ draft.routines.deletedScheduleIds =
912
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
851
913
  // Its own slice: the refetch that puts the row back would otherwise wipe
852
914
  // this error, leaving the row silently reappearing with no explanation.
853
915
  draft.routineMutation = {
@@ -1183,5 +1245,5 @@ const aiCfoView = createSlice({
1183
1245
  });
1184
1246
  },
1185
1247
  });
1186
- export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
1248
+ export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, transcribeAudio, transcribeAudioFailure, transcribeAudioSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
1187
1249
  export default aiCfoView.reducer;