@zeniai/client-epic-state 5.1.82 → 5.1.83-betaRD1

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 (129) hide show
  1. package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +2 -0
  2. package/lib/entity/jeSchedules/jeSchedulesPayload.js +2 -0
  3. package/lib/entity/jeSchedules/jeSchedulesSelector.d.ts +4 -0
  4. package/lib/entity/jeSchedules/jeSchedulesSelector.js +13 -2
  5. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +2 -0
  6. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  7. package/lib/entity/snackbar/snackbarTypes.js +29 -0
  8. package/lib/entity/transaction/payloadTypes/scheduleTransactionPayload.d.ts +2 -0
  9. package/lib/entity/transaction/payloadTypes/scheduleTransactionPayload.js +2 -0
  10. package/lib/entity/transaction/stateTypes/scheduleTransaction.d.ts +2 -0
  11. package/lib/epic.d.ts +5 -1
  12. package/lib/epic.js +5 -1
  13. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +2 -0
  14. package/lib/esm/entity/jeSchedules/jeSchedulesSelector.js +13 -2
  15. package/lib/esm/entity/snackbar/snackbarTypes.js +29 -0
  16. package/lib/esm/entity/transaction/payloadTypes/scheduleTransactionPayload.js +2 -0
  17. package/lib/esm/epic.js +5 -1
  18. package/lib/esm/index.js +11 -5
  19. package/lib/esm/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +6 -3
  20. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +26 -3
  21. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/newSchedule/initializeNewScheduleEpic.js +21 -0
  22. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/newSchedule/saveNewScheduleEpic.js +110 -0
  23. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/newSchedule/searchTransactionsForNewScheduleEpic.js +73 -0
  24. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +22 -0
  25. package/lib/esm/view/expenseAutomationView/helpers/newScheduleLocalDataHelper.js +140 -0
  26. package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +157 -28
  27. package/lib/esm/view/expenseAutomationView/selectorTypes/jeNewScheduleViewSelectorTypes.js +1 -0
  28. package/lib/esm/view/expenseAutomationView/selectors/jeNewScheduleViewSelector.js +70 -0
  29. package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +159 -22
  30. package/lib/esm/view/expenseAutomationView/types/jeSchedulesViewState.js +5 -6
  31. package/lib/esm/view/invoicing/createCreditNote/createCreditNoteEpic.js +24 -4
  32. package/lib/esm/view/invoicing/createInvoice/createInvoiceEpic.js +24 -4
  33. package/lib/esm/view/invoicing/createInvoice/createInvoiceSelector.js +37 -10
  34. package/lib/esm/view/invoicing/discountAction/discountActionEpic.js +33 -9
  35. package/lib/esm/view/invoicing/dunningAction/dunningActionEpic.js +35 -9
  36. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +5 -1
  37. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +32 -5
  38. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +23 -3
  39. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +24 -4
  40. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +24 -4
  41. package/lib/esm/view/invoicing/editInvoicingSubscriptionDetailView/syncInvoicingSubscriptionCouponsEpic.js +20 -3
  42. package/lib/esm/view/invoicing/invoiceAction/invoiceActionEpic.js +60 -17
  43. package/lib/esm/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.js +30 -0
  44. package/lib/esm/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +4 -29
  45. package/lib/esm/view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic.js +29 -10
  46. package/lib/esm/view/invoicing/invoicingQboView/epics/startInvoicingQboBackfillEpic.js +28 -5
  47. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNoteEpic.js +24 -4
  48. package/lib/esm/view/invoicing/issueCreditNote/issueCreditNoteSelector.js +22 -14
  49. package/lib/esm/view/invoicing/recordPayment/recordPaymentEpic.js +46 -7
  50. package/lib/esm/view/invoicing/settingsView/settingsViewEpics.js +26 -8
  51. package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +1 -1
  52. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +4 -1
  53. package/lib/esm/view/invoicing/subscriptionAction/subscriptionActionEpic.js +41 -9
  54. package/lib/index.d.ts +15 -10
  55. package/lib/index.js +71 -50
  56. package/lib/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +6 -3
  57. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.d.ts +2 -2
  58. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +24 -1
  59. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/initializeNewScheduleEpic.d.ts +8 -0
  60. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/initializeNewScheduleEpic.js +25 -0
  61. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/saveNewScheduleEpic.d.ts +45 -0
  62. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/saveNewScheduleEpic.js +114 -0
  63. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/searchTransactionsForNewScheduleEpic.d.ts +8 -0
  64. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/searchTransactionsForNewScheduleEpic.js +77 -0
  65. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.d.ts +6 -0
  66. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +26 -0
  67. package/lib/view/expenseAutomationView/helpers/newScheduleLocalDataHelper.d.ts +36 -0
  68. package/lib/view/expenseAutomationView/helpers/newScheduleLocalDataHelper.js +145 -0
  69. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  70. package/lib/view/expenseAutomationView/payload/jeSchedulesPayload.d.ts +7 -1
  71. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +44 -10
  72. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +158 -28
  73. package/lib/view/expenseAutomationView/selectorTypes/jeNewScheduleViewSelectorTypes.d.ts +25 -0
  74. package/lib/view/expenseAutomationView/selectorTypes/jeNewScheduleViewSelectorTypes.js +2 -0
  75. package/lib/view/expenseAutomationView/selectorTypes/jeSchedulesSelectorTypes.d.ts +34 -8
  76. package/lib/view/expenseAutomationView/selectors/jeNewScheduleViewSelector.d.ts +10 -0
  77. package/lib/view/expenseAutomationView/selectors/jeNewScheduleViewSelector.js +75 -0
  78. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.d.ts +4 -1
  79. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +161 -21
  80. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +58 -8
  81. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.js +8 -8
  82. package/lib/view/invoicing/createCreditNote/createCreditNoteEpic.d.ts +2 -1
  83. package/lib/view/invoicing/createCreditNote/createCreditNoteEpic.js +23 -3
  84. package/lib/view/invoicing/createInvoice/createInvoiceEpic.d.ts +2 -1
  85. package/lib/view/invoicing/createInvoice/createInvoiceEpic.js +23 -3
  86. package/lib/view/invoicing/createInvoice/createInvoiceSelector.d.ts +22 -8
  87. package/lib/view/invoicing/createInvoice/createInvoiceSelector.js +37 -10
  88. package/lib/view/invoicing/discountAction/discountActionEpic.d.ts +2 -1
  89. package/lib/view/invoicing/discountAction/discountActionEpic.js +32 -8
  90. package/lib/view/invoicing/dunningAction/dunningActionEpic.d.ts +2 -1
  91. package/lib/view/invoicing/dunningAction/dunningActionEpic.js +34 -8
  92. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.d.ts +3 -1
  93. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +5 -1
  94. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.d.ts +2 -1
  95. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +31 -4
  96. package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.d.ts +2 -1
  97. package/lib/view/invoicing/editInvoicingCouponDetailView/saveInvoicingCouponEpic.js +22 -2
  98. package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.d.ts +2 -1
  99. package/lib/view/invoicing/editInvoicingCustomerDetailView/saveInvoicingCustomerEpic.js +23 -3
  100. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.d.ts +2 -1
  101. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/saveInvoicingSubscriptionEpic.js +23 -3
  102. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/syncInvoicingSubscriptionCouponsEpic.d.ts +2 -1
  103. package/lib/view/invoicing/editInvoicingSubscriptionDetailView/syncInvoicingSubscriptionCouponsEpic.js +19 -2
  104. package/lib/view/invoicing/invoiceAction/invoiceActionEpic.d.ts +2 -1
  105. package/lib/view/invoicing/invoiceAction/invoiceActionEpic.js +59 -16
  106. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewPayload.d.ts +13 -0
  107. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.d.ts +1 -1
  108. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewReducer.js +30 -0
  109. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.d.ts +11 -31
  110. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewSelector.js +4 -29
  111. package/lib/view/invoicing/invoicingConfigView/invoicingConfigViewState.d.ts +15 -0
  112. package/lib/view/invoicing/invoicingQboView/epics/invoicingQboViewActionType.d.ts +2 -1
  113. package/lib/view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic.js +28 -9
  114. package/lib/view/invoicing/invoicingQboView/epics/startInvoicingQboBackfillEpic.js +27 -4
  115. package/lib/view/invoicing/issueCreditNote/issueCreditNoteEpic.d.ts +2 -1
  116. package/lib/view/invoicing/issueCreditNote/issueCreditNoteEpic.js +23 -3
  117. package/lib/view/invoicing/issueCreditNote/issueCreditNoteSelector.d.ts +24 -7
  118. package/lib/view/invoicing/issueCreditNote/issueCreditNoteSelector.js +23 -16
  119. package/lib/view/invoicing/recordPayment/recordPaymentEpic.d.ts +2 -1
  120. package/lib/view/invoicing/recordPayment/recordPaymentEpic.js +45 -6
  121. package/lib/view/invoicing/settingsView/settingsViewEpics.d.ts +2 -1
  122. package/lib/view/invoicing/settingsView/settingsViewEpics.js +25 -7
  123. package/lib/view/invoicing/settingsView/settingsViewReducer.d.ts +10 -1
  124. package/lib/view/invoicing/settingsView/settingsViewReducer.js +1 -1
  125. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +4 -2
  126. package/lib/view/invoicing/settingsView/settingsViewSelector.js +4 -1
  127. package/lib/view/invoicing/subscriptionAction/subscriptionActionEpic.d.ts +2 -1
  128. package/lib/view/invoicing/subscriptionAction/subscriptionActionEpic.js +40 -8
  129. package/package.json +1 -1
@@ -3,7 +3,8 @@ import orderBy from 'lodash/orderBy';
3
3
  import { reduceAnyFetchState } from '../../../commonStateTypes/reduceFetchState';
4
4
  import { LAST_SCHEDULE_DAY_OF_MONTH, toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
5
  import { getAccountIdsForTypes } from '../../../entity/account/accountSelector';
6
- import { getJEScheduledTransactionByJEScheduleTransactionKey, } from '../../../entity/jeSchedules/jeSchedulesSelector';
6
+ import { getJEAccruedScheduleByJEScheduleKey, getJEScheduledTransactionByJEScheduleTransactionKey, } from '../../../entity/jeSchedules/jeSchedulesSelector';
7
+ import { ALL_SCHEDULES_TYPES, } from '../../../entity/jeSchedules/jeSchedulesTypes';
7
8
  import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
8
9
  import { getAccountIdsForLabel, getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
9
10
  import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
@@ -11,7 +12,38 @@ import { getAccountsTypesForScheduleDetails } from '../../scheduleView/scheduleD
11
12
  import { filterNestedAccountHierarchyById } from '../../scheduleView/scheduleDetailView/scheduleDetailSelector';
12
13
  import { getAccountLabelForScheduleType, getAccountsTypesForScheduleList, } from '../../scheduleView/scheduleListView/scheduleListHelper';
13
14
  import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
15
+ import { ALL_JE_PAGE_TABS, getJESchedulePeriodTabKey, } from '../types/jeSchedulesViewState';
14
16
  import { getJEAccountSettingsView } from './jeAccountSettingsViewSelector';
17
+ import { getNewScheduleView } from './jeNewScheduleViewSelector';
18
+ // Matches the AI-automation user addresses webc's schedule list row already
19
+ // checks (JEScheduleReadOnlyListRow/JEScheduleDraftListRow "Method" column,
20
+ // strings.ts aiBotEmail/botEmail) to determine whether a schedule was
21
+ // created by the AI accountant vs. a human.
22
+ const AI_BOT_EMAIL = 'ai@zeni.ai';
23
+ const BOT_EMAIL = 'bot@zeni.ai';
24
+ // Exported (rather than kept as a closure inside the selector) so it can be
25
+ // unit-tested directly — confidenceScore is a separate, more narrowly
26
+ // populated field that can be null on a schedule the bot user created,
27
+ // which previously caused these schedules to display as "AI Accountant" in
28
+ // the list but count under "Manual" in the KPI summary.
29
+ export const isAiAccountantSchedule = (schedule) => schedule.createdByUser?.email === AI_BOT_EMAIL ||
30
+ schedule.createdByUser?.email === BOT_EMAIL;
31
+ // Accrued schedules are structurally distinct — no baseTransaction, carrying recurringAmount and
32
+ // memo in its place — so anything reading baseTransaction has to branch on this first.
33
+ //
34
+ // Discriminates on jeAccruedScheduleKey, which only JEAccruedSchedule carries. jeScheduleType
35
+ // cannot be used: it is a data field present on BOTH row shapes, so an accrued-typed row that
36
+ // arrived through the prepaid/fixed DTO (failed_je_postings, legacy fetches) would be
37
+ // misclassified and then crash on recurringAmount / jeAccruedScheduleKey.
38
+ export function isAccruedScheduleRow(schedule) {
39
+ return 'jeAccruedScheduleKey' in schedule;
40
+ }
41
+ function getScheduleAmount(schedule) {
42
+ if (isAccruedScheduleRow(schedule)) {
43
+ return schedule.recurringAmount.amount;
44
+ }
45
+ return schedule.baseTransaction.amount.amount;
46
+ }
15
47
  function getJEScheduleSortAccessor(sortKey) {
16
48
  switch (sortKey) {
17
49
  case 'vendor':
@@ -23,7 +55,9 @@ function getJEScheduleSortAccessor(sortKey) {
23
55
  case 'depCategory':
24
56
  return (s) => s.jeCredit.account?.accountName?.toLowerCase();
25
57
  case 'scheduleCategory':
26
- return (s) => s.baseTransaction.account?.accountName?.toLowerCase();
58
+ return (s) => isAccruedScheduleRow(s)
59
+ ? undefined
60
+ : s.baseTransaction.account?.accountName?.toLowerCase();
27
61
  case 'startMonth':
28
62
  return (s) => s.startDate?.valueOf();
29
63
  case 'type':
@@ -51,11 +85,13 @@ function getJEScheduleSortAccessor(sortKey) {
51
85
  return s.balanceAsOfToday?.amount ?? null;
52
86
  };
53
87
  case 'totalAmount':
54
- return (s) => s.baseTransaction.amount.amount;
88
+ return (s) => getScheduleAmount(s);
55
89
  case 'transactionDate':
56
- return (s) => s.baseTransaction.date.valueOf();
90
+ return (s) => s.createTime?.valueOf();
57
91
  case 'memo':
58
- return (s) => s.baseTransaction.memo?.toLowerCase();
92
+ return (s) => isAccruedScheduleRow(s)
93
+ ? s.memo.toLowerCase()
94
+ : s.baseTransaction.memo?.toLowerCase();
59
95
  default:
60
96
  return (s) => s.startDate?.valueOf();
61
97
  }
@@ -67,9 +103,14 @@ function sortJEScheduledTransactions(transactions, sortKey, sortOrder) {
67
103
  export function getExpenseAutomationJESchedulesView(state) {
68
104
  const jeScheduledTransaction = [];
69
105
  const failedJeScheduledTransaction = [];
106
+ const accruedSchedules = [];
70
107
  const { accountState, accountListState, classState, classListState, expenseAutomationJESchedulesViewState, expenseAutomationViewState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
71
- const { jeScheduleTransactionKeysByPeriod, failedJeScheduleTransactionKeysByPeriod, jeScheduleLocalDataById, postStatusById, ignoreStatusById, uiState, error, fetchState, refreshStatus, } = expenseAutomationJESchedulesViewState;
108
+ const { jeScheduleTransactionKeysByPeriodAndTab, jeScheduleAccruedKeysByPeriodAndTab, failedJeScheduleTransactionKeysByPeriodAndTab, jeScheduleLocalDataById, postStatusById, ignoreStatusById, uiState, error, fetchState, refreshStatus, newSchedule, } = expenseAutomationJESchedulesViewState;
72
109
  const accountSettingsView = getJEAccountSettingsView(state);
110
+ const newScheduleView = newSchedule.selectedDetailScheduleType === 'fixed_assets' ||
111
+ newSchedule.selectedDetailScheduleType === 'prepaid_expenses'
112
+ ? getNewScheduleView(state, newSchedule.selectedDetailScheduleType)
113
+ : undefined;
73
114
  const currentTenant = getCurrentTenant(state);
74
115
  const { selectedPeriodByTenantId } = expenseAutomationViewState;
75
116
  const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
@@ -97,27 +138,39 @@ export function getExpenseAutomationJESchedulesView(state) {
97
138
  const filteredDepAccountListNestedAccountHierarchy = filterNestedAccountHierarchyById(depAccountListNestedAccountHierarchy, filteredDepAccounts.map((account) => account.accountId));
98
139
  const classListNestedAccountHierarchy = getNestedClassListHierarchy(classListState);
99
140
  const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : null;
100
- const jeScheduleTransactionKeysForSelectedPeriod = monthYearPeriodId != null
101
- ? jeScheduleTransactionKeysByPeriod[monthYearPeriodId]
141
+ const selectedPeriodTabKey = monthYearPeriodId != null
142
+ ? getJESchedulePeriodTabKey(uiState.selectedPageTab, monthYearPeriodId)
102
143
  : null;
103
- const failedJeScheduleTransactionKeysForSelectedPeriod = monthYearPeriodId != null
104
- ? failedJeScheduleTransactionKeysByPeriod[monthYearPeriodId]
105
- : null;
106
- const jeScheduleTransactionKeys = jeScheduleTransactionKeysForSelectedPeriod ?? [];
107
- const failedJeScheduleTransactionKeys = failedJeScheduleTransactionKeysForSelectedPeriod ?? [];
144
+ const jeScheduleTransactionKeysForSelectedTab = selectedPeriodTabKey != null
145
+ ? jeScheduleTransactionKeysByPeriodAndTab[selectedPeriodTabKey]
146
+ : undefined;
147
+ const failedJeScheduleTransactionKeysForSelectedTab = selectedPeriodTabKey != null
148
+ ? failedJeScheduleTransactionKeysByPeriodAndTab[selectedPeriodTabKey]
149
+ : undefined;
150
+ // Distinguishes "this tab has never been fetched for this period" from "fetched and genuinely
151
+ // empty". Without it the page cannot tell a first visit from a real empty state, and renders an
152
+ // empty table where it should render a skeleton.
153
+ const hasLoadedForSelectedTab = jeScheduleTransactionKeysForSelectedTab != null;
154
+ const jeScheduleTransactionKeys = jeScheduleTransactionKeysForSelectedTab ?? [];
155
+ const failedJeScheduleTransactionKeys = failedJeScheduleTransactionKeysForSelectedTab ?? [];
156
+ const jeScheduleAccruedKeys = (selectedPeriodTabKey != null
157
+ ? jeScheduleAccruedKeysByPeriodAndTab[selectedPeriodTabKey]
158
+ : undefined) ?? [];
108
159
  jeScheduleTransactionKeys.forEach((jeScheduleKey) => {
109
160
  const jeScheduleDetails = getJEScheduledTransactionByJEScheduleTransactionKey(jeScheduleKey, state);
110
161
  jeScheduledTransaction.push(jeScheduleDetails);
111
162
  });
163
+ jeScheduleAccruedKeys.forEach((jeAccruedScheduleKey) => {
164
+ const accruedScheduleDetails = getJEAccruedScheduleByJEScheduleKey(jeAccruedScheduleKey, state);
165
+ if (accruedScheduleDetails != null) {
166
+ accruedSchedules.push(accruedScheduleDetails);
167
+ }
168
+ });
112
169
  failedJeScheduleTransactionKeys.forEach((jeScheduleKey) => {
113
170
  const jeScheduleDetails = getJEScheduledTransactionByJEScheduleTransactionKey(jeScheduleKey, state);
114
171
  failedJeScheduledTransaction.push(jeScheduleDetails);
115
172
  });
116
- const { sortKey, sortOrder } = uiState;
117
- const draftSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'draft'), sortKey, sortOrder);
118
- const ongoingSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'ongoing'), sortKey, sortOrder);
119
- const completedSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'completed' ||
120
- schedule.status.code === 'marked_as_completed'), sortKey, sortOrder);
173
+ const { sortKey, sortOrder, selectedJETypeFilter } = uiState;
121
174
  const unsortedResolveSchedules = [];
122
175
  failedJeScheduledTransaction.forEach((schedule) => {
123
176
  schedule.scheduledJournalEntry.forEach((journalEntry) => {
@@ -129,10 +182,73 @@ export function getExpenseAutomationJESchedulesView(state) {
129
182
  });
130
183
  const resolveSchedules = sortJEScheduledTransactions(unsortedResolveSchedules, sortKey, sortOrder);
131
184
  const validResolveForPending = unsortedResolveSchedules.filter((r) => jeScheduleLocalDataById[r.scheduledJournalEntry.scheduledJournalEntryID] != null);
185
+ const matchesTypeFilter = (schedule) => selectedJETypeFilter == null ||
186
+ schedule.jeScheduleType === selectedJETypeFilter;
187
+ const isCreatedStatus = (schedule) => schedule.status.code === 'ongoing' ||
188
+ schedule.status.code === 'completed' ||
189
+ schedule.status.code === 'marked_as_completed';
190
+ const allScheduleRows = [
191
+ ...jeScheduledTransaction,
192
+ ...accruedSchedules,
193
+ ];
132
194
  const pendingReviewSchedules = sortJEScheduledTransactions([
133
- ...jeScheduledTransaction.filter((schedule) => schedule.status.code === 'draft'),
195
+ ...allScheduleRows.filter((schedule) => schedule.status.code === 'draft' && matchesTypeFilter(schedule)),
134
196
  ...validResolveForPending,
135
197
  ], sortKey, sortOrder);
198
+ const createdSchedules = sortJEScheduledTransactions(allScheduleRows.filter((schedule) => isCreatedStatus(schedule) && matchesTypeFilter(schedule)), sortKey, sortOrder);
199
+ // KPI summary — derived from unfiltered schedules so counts are always accurate
200
+ const allSchedules = allScheduleRows;
201
+ const sumAmount = (schedules) => schedules.reduce((total, schedule) => total + (getScheduleAmount(schedule) ?? 0), 0);
202
+ const pendingReviewTotal = sumAmount(pendingReviewSchedules);
203
+ const createdTotal = sumAmount(createdSchedules);
204
+ const aiAccountantSchedules = allSchedules.filter(isAiAccountantSchedule);
205
+ const manualSchedules = allSchedules.filter((schedule) => !isAiAccountantSchedule(schedule));
206
+ const pendingUnfiltered = allSchedules.filter((schedule) => schedule.status.code === 'draft');
207
+ const kpiSummary = {
208
+ aiAccountant: {
209
+ count: aiAccountantSchedules.length,
210
+ amount: sumAmount(aiAccountantSchedules),
211
+ },
212
+ manual: {
213
+ count: manualSchedules.length,
214
+ amount: sumAmount(manualSchedules),
215
+ },
216
+ needsReview: {
217
+ count: pendingUnfiltered.length,
218
+ amount: sumAmount(pendingUnfiltered),
219
+ },
220
+ total: {
221
+ count: allSchedules.length,
222
+ amount: sumAmount(allSchedules),
223
+ },
224
+ };
225
+ // Each tab's badge counts from that tab's own bucket. A tab that has not been fetched yet
226
+ // reports undefined rather than 0, so the UI can hide the badge instead of asserting "none".
227
+ const scheduleCountsByTab = {};
228
+ ALL_JE_PAGE_TABS.forEach((pageTab) => {
229
+ const periodTabKey = monthYearPeriodId != null
230
+ ? getJESchedulePeriodTabKey(pageTab, monthYearPeriodId)
231
+ : null;
232
+ const transactionKeys = periodTabKey != null
233
+ ? jeScheduleTransactionKeysByPeriodAndTab[periodTabKey]
234
+ : undefined;
235
+ if (transactionKeys == null) {
236
+ return;
237
+ }
238
+ const accruedKeys = (periodTabKey != null
239
+ ? jeScheduleAccruedKeysByPeriodAndTab[periodTabKey]
240
+ : undefined) ?? [];
241
+ const rowsForTab = [
242
+ ...transactionKeys.map((transactionKey) => getJEScheduledTransactionByJEScheduleTransactionKey(transactionKey, state)),
243
+ ...accruedKeys
244
+ .map((accruedKey) => getJEAccruedScheduleByJEScheduleKey(accruedKey, state))
245
+ .filter((accruedRow) => accruedRow != null),
246
+ ];
247
+ scheduleCountsByTab[pageTab] =
248
+ pageTab === 'existing_schedules'
249
+ ? rowsForTab.filter((row) => row.status.code === 'ongoing').length
250
+ : rowsForTab.filter((row) => row.status.code === 'draft' || isCreatedStatus(row)).length;
251
+ });
136
252
  const allSteps = monthYearPeriodId != null
137
253
  ? getAllSteps(monthEndCloseChecksState, monthYearPeriodId, currentTenant.tenantId)
138
254
  : [];
@@ -145,6 +261,22 @@ export function getExpenseAutomationJESchedulesView(state) {
145
261
  { fetchState, error },
146
262
  monthEndFetchState,
147
263
  ]);
264
+ const countByType = (schedules) => {
265
+ const counts = ALL_SCHEDULES_TYPES.reduce((acc, type) => ({ ...acc, [type]: 0 }), {});
266
+ schedules.forEach((schedule) => {
267
+ if (schedule.jeScheduleType != null) {
268
+ counts[schedule.jeScheduleType] += 1;
269
+ }
270
+ });
271
+ return counts;
272
+ };
273
+ const jeScheduleTypeCounts = {
274
+ active: countByType(allSchedules.filter((schedule) => schedule.status.code === 'draft' ||
275
+ schedule.status.code === 'ongoing' ||
276
+ schedule.status.code === 'completed' ||
277
+ schedule.status.code === 'marked_as_completed')),
278
+ ongoing: countByType(allSchedules.filter((schedule) => schedule.status.code === 'ongoing')),
279
+ };
148
280
  return {
149
281
  uncategorizedAccounts,
150
282
  allDepreciationAccountList: filteredDepAccounts,
@@ -153,12 +285,17 @@ export function getExpenseAutomationJESchedulesView(state) {
153
285
  allAccountList: filteredAccounts,
154
286
  accountListNestedAccountHierarchy,
155
287
  allClassList: allClasses,
156
- completedSchedules,
157
- draftSchedules,
158
- ongoingSchedules,
288
+ createdSchedules,
289
+ createdTotal,
290
+ hasLoadedForSelectedTab,
291
+ jeScheduleTypeCounts,
292
+ scheduleCountsByTab,
293
+ kpiSummary,
159
294
  pendingReviewSchedules,
295
+ pendingReviewTotal,
160
296
  resolveSchedules: resolveSchedules,
161
297
  accountSettingsView,
298
+ newScheduleView,
162
299
  postStatusById: postStatusById,
163
300
  ignoreStatusById: ignoreStatusById,
164
301
  fetchState: reducedFetchStatus.fetchState,
@@ -16,9 +16,8 @@ const JE_SCHEDULE_SORT_KEYS = [
16
16
  'memo',
17
17
  ];
18
18
  export const toJEScheduleSortKey = (v) => stringToUnion(v, JE_SCHEDULE_SORT_KEYS);
19
- const JE_SCHEDULE_MAIN_TABS = [
20
- 'pending_review',
21
- 'ongoing',
22
- 'completed',
23
- ];
24
- export const toJEScheduleMainTab = (v) => stringToUnion(v, JE_SCHEDULE_MAIN_TABS);
19
+ const JE_PAGE_TABS = ['schedules', 'existing_schedules'];
20
+ export const toJEPageTab = (v) => stringToUnion(v, JE_PAGE_TABS);
21
+ export const ALL_JE_PAGE_TABS = JE_PAGE_TABS;
22
+ export const getJESchedulePeriodTabKey = (selectedPageTab, monthYearPeriodId) => `${selectedPageTab}_${monthYearPeriodId}`;
23
+ export const MIN_NEW_SCHEDULE_SEARCH_LENGTH = 2;
@@ -1,6 +1,7 @@
1
- import { of } from 'rxjs';
1
+ import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingCreditNotes } from '../../../entity/invoicing/creditNote/creditNoteReducer';
4
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
6
  import { localDataToCreateCreditNotePayload, } from './createCreditNotePayload';
6
7
  import { createCreditNoteFailure, createCreditNoteSuccess, submitCreateCreditNote, } from './createCreditNoteReducer';
@@ -15,9 +16,28 @@ export const createCreditNoteEpic = (actions$, state$, zeniAPI) => actions$.pipe
15
16
  return [
16
17
  updateInvoicingCreditNotes([response.data], 'merge'),
17
18
  createCreditNoteSuccess({ creditNoteId: response.data.id }),
19
+ openSnackbar({
20
+ messageSection: 'invoicing_credit_note_saved',
21
+ messageText: 'success',
22
+ type: 'success',
23
+ }),
18
24
  ];
19
25
  }
20
- return of(createCreditNoteFailure(response.status));
21
- }), catchError((error) => of(createCreditNoteFailure(createZeniAPIStatus('Unexpected Error', 'Create credit note REST API call errored out ' +
22
- JSON.stringify(error))))));
26
+ return from([
27
+ createCreditNoteFailure(response.status),
28
+ openSnackbar({
29
+ messageSection: 'invoicing_credit_note_saved',
30
+ messageText: 'failed',
31
+ type: 'error',
32
+ }),
33
+ ]);
34
+ }), catchError((error) => from([
35
+ createCreditNoteFailure(createZeniAPIStatus('Unexpected Error', 'Create credit note REST API call errored out ' +
36
+ JSON.stringify(error))),
37
+ openSnackbar({
38
+ messageSection: 'invoicing_credit_note_saved',
39
+ messageText: 'failed',
40
+ type: 'error',
41
+ }),
42
+ ])));
23
43
  }));
@@ -1,8 +1,9 @@
1
- import { of } from 'rxjs';
1
+ import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateAddresses } from '../../../entity/address/addressReducer';
4
4
  import { extractInvoicingInvoiceAddresses } from '../../../entity/invoicing/invoice/invoicePayload';
5
5
  import { updateInvoices } from '../../../entity/invoicing/invoice/invoiceReducer';
6
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
6
7
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
7
8
  import { addInvoiceToList } from '../invoiceList/invoiceListReducer';
8
9
  import { createInvoice, createInvoiceFailure, createInvoiceSuccess, } from './createInvoiceReducer';
@@ -22,9 +23,28 @@ export const createInvoiceEpic = (actions$, _state$, zeniAPI) => actions$.pipe(f
22
23
  status: response.data.status,
23
24
  }),
24
25
  createInvoiceSuccess({ invoiceId: response.data.id }),
26
+ openSnackbar({
27
+ messageSection: 'invoicing_invoice_saved',
28
+ messageText: 'success',
29
+ type: 'success',
30
+ }),
25
31
  ];
26
32
  }
27
- return of(createInvoiceFailure(response.status));
28
- }), catchError((error) => of(createInvoiceFailure(createZeniAPIStatus('Unexpected Error', 'Create invoice REST API call errored out ' +
29
- JSON.stringify(error))))));
33
+ return from([
34
+ createInvoiceFailure(response.status),
35
+ openSnackbar({
36
+ messageSection: 'invoicing_invoice_saved',
37
+ messageText: 'failed',
38
+ type: 'error',
39
+ }),
40
+ ]);
41
+ }), catchError((error) => from([
42
+ createInvoiceFailure(createZeniAPIStatus('Unexpected Error', 'Create invoice REST API call errored out ' +
43
+ JSON.stringify(error))),
44
+ openSnackbar({
45
+ messageSection: 'invoicing_invoice_saved',
46
+ messageText: 'failed',
47
+ type: 'error',
48
+ }),
49
+ ])));
30
50
  }));
@@ -1,25 +1,52 @@
1
+ import { getAddressByAddressId } from '../../../entity/address/addressSelector';
1
2
  import { getInvoicingCatalogListView, getInvoicingCatalogPrices, } from '../invoicingCatalogListView/invoicingCatalogListViewSelector';
2
3
  import { getInvoicingCustomerListView } from '../invoicingCustomerListView/invoicingCustomerListViewSelector';
3
4
  import { getInvoicingSettings } from '../settingsView/settingsViewSelector';
5
+ /**
6
+ * Display name for a customer option: company, else the full name, else the
7
+ * email, else the raw id.
8
+ */
9
+ const customerDisplayName = (customer) => {
10
+ const fullName = [customer.firstName, customer.lastName]
11
+ .filter((part) => part != null && part.trim() !== '')
12
+ .join(' ')
13
+ .trim();
14
+ const company = customer.company?.trim() ?? '';
15
+ const email = customer.email?.trim() ?? '';
16
+ return company !== ''
17
+ ? company
18
+ : fullName !== ''
19
+ ? fullName
20
+ : email !== ''
21
+ ? email
22
+ : customer.id;
23
+ };
4
24
  /**
5
25
  * Single bundled view for the create-invoice screen — web-ui consumes only this.
6
26
  * Carries the form draft (`localData`), the option sources the form needs
7
- * (customers, catalog plans/products, settings), the create-invoice fetch
8
- * state/error, and the id of the invoice last created (`savedInvoiceId`, to
9
- * navigate on success). Returns raw entity lists; the screen maps them to its
10
- * option shapes.
27
+ * (customer picker rows, catalog plan names, products, settings), the
28
+ * create-invoice fetch state/error, and the id of the invoice last created
29
+ * (`savedInvoiceId`, to navigate on success).
11
30
  */
12
31
  export const getCreateInvoiceFormView = (state) => {
13
32
  const createInvoiceState = state.invoicingCreateInvoiceState;
14
33
  const catalogListView = getInvoicingCatalogListView(state);
15
- const plans = getInvoicingCatalogPrices(catalogListView.plans);
16
34
  return {
17
- customers: getInvoicingCustomerListView(state).customers,
35
+ customers: getInvoicingCustomerListView(state).customers.map((customer) => ({
36
+ billingAddress: customer.billingAddressId != null
37
+ ? getAddressByAddressId(state.addressState, customer.billingAddressId)
38
+ : undefined,
39
+ email: customer.email,
40
+ id: customer.id,
41
+ name: customerDisplayName(customer),
42
+ })),
18
43
  localData: createInvoiceState.formDraft,
19
- planNames: plans
20
- .map((plan) => plan.name ?? '')
21
- .filter((name) => name !== ''),
22
- plans,
44
+ planNames: catalogListView.plans.flatMap((plan) => (plan.subFamilies ?? [])
45
+ .filter((subFamily) => (subFamily.name ?? '') !== '')
46
+ .map((subFamily) => ({
47
+ planName: plan.name ?? '',
48
+ subFamilyName: subFamily.name ?? '',
49
+ }))),
23
50
  products: getInvoicingCatalogPrices(catalogListView.products),
24
51
  savedInvoiceId: createInvoiceState.createdInvoiceId,
25
52
  settings: getInvoicingSettings(state),
@@ -1,11 +1,28 @@
1
- import { of } from 'rxjs';
1
+ import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingCoupons } from '../../../entity/invoicing/coupon/couponReducer';
4
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
6
  import { runInvoicingDiscountAction, runInvoicingDiscountActionFailure, runInvoicingDiscountActionSuccess, } from './discountActionReducer';
7
+ /** Snackbar copy per action kind; unlisted kinds run without one. */
8
+ const SNACKBAR_SECTION_BY_KIND = {
9
+ archive: 'invoicing_discount_archive',
10
+ delete: 'invoicing_discount_delete',
11
+ unarchive: 'invoicing_discount_unarchive',
12
+ };
6
13
  export const runInvoicingDiscountActionEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(runInvoicingDiscountAction.match), mergeMap((action) => {
7
14
  const { action: kind, id } = action.payload;
8
15
  const requestBody = { action: kind };
16
+ const snackbarSection = SNACKBAR_SECTION_BY_KIND[kind];
17
+ const snackbar = (messageText) => snackbarSection == null
18
+ ? []
19
+ : [
20
+ openSnackbar({
21
+ messageSection: snackbarSection,
22
+ messageText,
23
+ type: messageText === 'success' ? 'success' : 'error',
24
+ }),
25
+ ];
9
26
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/discounts/${encodeURIComponent(id)}/actions`;
10
27
  return zeniAPI
11
28
  .postAndGetJSON(apiUrl, requestBody)
@@ -14,15 +31,22 @@ export const runInvoicingDiscountActionEpic = (actions$, _state$, zeniAPI) => ac
14
31
  return [
15
32
  updateInvoicingCoupons([response.data], 'merge'),
16
33
  runInvoicingDiscountActionSuccess({ id }),
34
+ ...snackbar('success'),
17
35
  ];
18
36
  }
19
- return of(runInvoicingDiscountActionFailure({
20
- error: response.status,
37
+ return from([
38
+ runInvoicingDiscountActionFailure({
39
+ error: response.status,
40
+ id,
41
+ }),
42
+ ...snackbar('failed'),
43
+ ]);
44
+ }), catchError((error) => from([
45
+ runInvoicingDiscountActionFailure({
46
+ error: createZeniAPIStatus('Unexpected Error', 'Discount action REST API call errored out ' +
47
+ JSON.stringify(error)),
21
48
  id,
22
- }));
23
- }), catchError((error) => of(runInvoicingDiscountActionFailure({
24
- error: createZeniAPIStatus('Unexpected Error', 'Discount action REST API call errored out ' +
25
- JSON.stringify(error)),
26
- id,
27
- }))));
49
+ }),
50
+ ...snackbar('failed'),
51
+ ])));
28
52
  }));
@@ -1,10 +1,29 @@
1
- import { of } from 'rxjs';
1
+ import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateInvoicingDunningCases } from '../../../entity/invoicing/dunningCase/dunningCaseReducer';
4
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
6
  import { runInvoicingDunningAction, runInvoicingDunningActionFailure, runInvoicingDunningActionSuccess, } from './dunningActionReducer';
7
+ /**
8
+ * Snackbar copy is per action kind; anything not listed here runs without one.
9
+ */
10
+ const SNACKBAR_SECTION_BY_KIND = {
11
+ cancel: 'invoicing_dunning_cancel',
12
+ resolve: 'invoicing_dunning_case_resolved',
13
+ retry: 'invoicing_dunning_retry',
14
+ };
6
15
  export const runInvoicingDunningActionEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(runInvoicingDunningAction.match), mergeMap((action) => {
7
16
  const { action: kind, body, id } = action.payload;
17
+ const snackbarSection = SNACKBAR_SECTION_BY_KIND[kind];
18
+ const snackbar = (messageText) => snackbarSection == null
19
+ ? []
20
+ : [
21
+ openSnackbar({
22
+ messageSection: snackbarSection,
23
+ messageText,
24
+ type: messageText === 'success' ? 'success' : 'error',
25
+ }),
26
+ ];
8
27
  const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/dunning/${encodeURIComponent(id)}/${kind}`;
9
28
  return zeniAPI
10
29
  .postAndGetJSON(apiUrl, (body ?? {}))
@@ -13,15 +32,22 @@ export const runInvoicingDunningActionEpic = (actions$, _state$, zeniAPI) => act
13
32
  return [
14
33
  updateInvoicingDunningCases([response.data], 'merge'),
15
34
  runInvoicingDunningActionSuccess({ id }),
35
+ ...snackbar('success'),
16
36
  ];
17
37
  }
18
- return of(runInvoicingDunningActionFailure({
19
- error: response.status,
38
+ return from([
39
+ runInvoicingDunningActionFailure({
40
+ error: response.status,
41
+ id,
42
+ }),
43
+ ...snackbar('failed'),
44
+ ]);
45
+ }), catchError((error) => from([
46
+ runInvoicingDunningActionFailure({
47
+ error: createZeniAPIStatus('Unexpected Error', 'Dunning action REST API call errored out ' +
48
+ JSON.stringify(error)),
20
49
  id,
21
- }));
22
- }), catchError((error) => of(runInvoicingDunningActionFailure({
23
- error: createZeniAPIStatus('Unexpected Error', 'Dunning action REST API call errored out ' +
24
- JSON.stringify(error)),
25
- id,
26
- }))));
50
+ }),
51
+ ...snackbar('failed'),
52
+ ])));
27
53
  }));
@@ -116,17 +116,21 @@ export const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan',
116
116
  export const createSubFamilyFormToPayload = (itemId, itemType, values) => {
117
117
  const name = values.name.trim();
118
118
  const priceCode = trimOrUndefined(values.priceCode) ?? toProductCode(name);
119
+ const tiered = isTieredCatalogPricingModel(values.pricingModel);
119
120
  const payload = {
120
121
  currency_code: 'USD',
121
122
  free_quantity: Number(values.freeQuantity) || 0,
122
123
  is_taxable: true,
123
124
  item_id: itemId,
124
125
  name,
125
- price: values.priceDollars?.amount ?? 0,
126
+ price: tiered ? 0 : (values.priceDollars?.amount ?? 0),
126
127
  price_code: priceCode,
127
128
  pricing_model: values.pricingModel || 'flat_fee',
128
129
  show_description_in_invoices: true,
129
130
  };
131
+ if (tiered) {
132
+ payload.tiers = (values.tiers ?? []).map(tierToPayload);
133
+ }
130
134
  const description = trimOrUndefined(values.description);
131
135
  if (description != null) {
132
136
  payload.description = description;
@@ -1,7 +1,8 @@
1
- import { forkJoin, of } from 'rxjs';
1
+ import { forkJoin, from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { mergeInvoicingPlanSubFamily, updateInvoicingPlans, } from '../../../entity/invoicing/plan/planReducer';
4
4
  import { updateInvoicingProducts } from '../../../entity/invoicing/product/productReducer';
5
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
5
6
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
6
7
  import { addInvoicingCatalogItem } from '../invoicingCatalogListView/invoicingCatalogListViewReducer';
7
8
  import { localDataToSaveInvoicingCatalogItemPayload, } from './editInvoicingCatalogItemDetailViewPayload';
@@ -38,7 +39,14 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
38
39
  const failedProductResponse = productResponses.find((productResponse) => !isSuccessResponse(productResponse) ||
39
40
  productResponse.data == null);
40
41
  if (failedProductResponse != null) {
41
- return of(submitInvoicingCatalogItemFormFailure(failedProductResponse.status));
42
+ return from([
43
+ submitInvoicingCatalogItemFormFailure(failedProductResponse.status),
44
+ openSnackbar({
45
+ messageSection: 'invoicing_catalog_item_saved',
46
+ messageText: 'failed',
47
+ type: 'error',
48
+ }),
49
+ ]);
42
50
  }
43
51
  const savedProducts = productResponses.flatMap((productResponse) => productResponse.data != null ? [productResponse.data] : []);
44
52
  const resultActions = [
@@ -65,6 +73,11 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
65
73
  : 'plan';
66
74
  resultActions.push(addInvoicingCatalogItem({ id: savedItem.id, type: itemType }));
67
75
  }
76
+ resultActions.push(openSnackbar({
77
+ messageSection: 'invoicing_catalog_item_saved',
78
+ messageText: 'success',
79
+ type: 'success',
80
+ }));
68
81
  return resultActions;
69
82
  };
70
83
  const productUpdates = saveInvoicingCatalogItem.match(action)
@@ -75,7 +88,21 @@ export const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actio
75
88
  }
76
89
  return forkJoin(productUpdates.map((productUpdate) => zeniAPI.putAndGetJSON(`${baseUrl}/catalog/products/${encodeURIComponent(productUpdate.id)}`, productUpdate.data))).pipe(mergeMap(completeSave));
77
90
  }
78
- return of(submitInvoicingCatalogItemFormFailure(response.status));
79
- }), catchError((error) => of(submitInvoicingCatalogItemFormFailure(createZeniAPIStatus('Unexpected Error', 'Save catalog item REST API call errored out ' +
80
- JSON.stringify(error))))));
91
+ return from([
92
+ submitInvoicingCatalogItemFormFailure(response.status),
93
+ openSnackbar({
94
+ messageSection: 'invoicing_catalog_item_saved',
95
+ messageText: 'failed',
96
+ type: 'error',
97
+ }),
98
+ ]);
99
+ }), catchError((error) => from([
100
+ submitInvoicingCatalogItemFormFailure(createZeniAPIStatus('Unexpected Error', 'Save catalog item REST API call errored out ' +
101
+ JSON.stringify(error))),
102
+ openSnackbar({
103
+ messageSection: 'invoicing_catalog_item_saved',
104
+ messageText: 'failed',
105
+ type: 'error',
106
+ }),
107
+ ])));
81
108
  }));