@zeniai/client-epic-state 5.1.56 → 5.1.57-betaDI1

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 (64) hide show
  1. package/lib/coreEpics.js +3 -2
  2. package/lib/entity/accountRecon/accountReconPayload.d.ts +1 -1
  3. package/lib/entity/accountRecon/accountReconReducer.js +15 -1
  4. package/lib/entity/chargeCard/chargeCard.d.ts +1 -1
  5. package/lib/entity/notificationRegistry/notificationRegistryPayload.d.ts +18 -0
  6. package/lib/entity/notificationRegistry/notificationRegistryPayload.js +28 -0
  7. package/lib/entity/notificationRegistry/notificationRegistryReducer.d.ts +6 -0
  8. package/lib/entity/notificationRegistry/notificationRegistryReducer.js +26 -0
  9. package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -0
  10. package/lib/entity/notificationRegistry/notificationRegistrySelector.js +10 -0
  11. package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +31 -0
  12. package/lib/entity/notificationRegistry/notificationRegistryState.js +10 -0
  13. package/lib/entity/tenant/clearAllEpic.d.ts +3 -1
  14. package/lib/entity/tenant/clearAllEpic.js +4 -0
  15. package/lib/epic.d.ts +2 -1
  16. package/lib/epic.js +2 -1
  17. package/lib/esm/coreEpics.js +3 -2
  18. package/lib/esm/entity/accountRecon/accountReconReducer.js +15 -1
  19. package/lib/esm/entity/notificationRegistry/notificationRegistryPayload.js +24 -0
  20. package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +22 -0
  21. package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +6 -0
  22. package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
  23. package/lib/esm/entity/tenant/clearAllEpic.js +4 -0
  24. package/lib/esm/epic.js +2 -1
  25. package/lib/esm/index.js +6 -2
  26. package/lib/esm/reducer.js +6 -0
  27. package/lib/esm/view/expenseAutomationView/selectors/statementParseViewSelector.js +0 -224
  28. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +103 -0
  29. package/lib/esm/view/notificationPreferencesView/notificationPreferencesEndpoint.js +5 -0
  30. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewPayload.js +36 -0
  31. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +222 -0
  32. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +25 -0
  33. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewState.js +4 -0
  34. package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +21 -2
  35. package/lib/esm/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  36. package/lib/esm/view/settingsView/settingsViewHelpers.js +3 -2
  37. package/lib/index.d.ts +9 -4
  38. package/lib/index.js +53 -42
  39. package/lib/reducer.d.ts +6 -0
  40. package/lib/reducer.js +6 -0
  41. package/lib/view/common/recurringViewHelper.d.ts +2 -2
  42. package/lib/view/expenseAutomationView/selectors/statementParseViewSelector.d.ts +1 -15
  43. package/lib/view/expenseAutomationView/selectors/statementParseViewSelector.js +1 -233
  44. package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +7 -44
  45. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.d.ts +34 -0
  46. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +107 -0
  47. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.d.ts +2 -0
  48. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.js +9 -0
  49. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.d.ts +7 -0
  50. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.js +41 -0
  51. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +30 -0
  52. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +226 -0
  53. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +7 -0
  54. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +32 -0
  55. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +14 -0
  56. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.js +8 -0
  57. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.d.ts +9 -1
  58. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +21 -2
  59. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  60. package/lib/view/settingsView/settingsViewHelpers.d.ts +1 -1
  61. package/lib/view/settingsView/settingsViewHelpers.js +3 -2
  62. package/lib/view/settingsView/settingsViewPayload.d.ts +4 -0
  63. package/lib/view/settingsView/settingsViewState.d.ts +1 -1
  64. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyStatementParseFormChange = exports.toStatementUpdateLocalDataFromFormView = exports.getStatementParseFormView = exports.mergeStatementParseDeletedTransactionIds = exports.areStatementParseBalancesConsistent = exports.resolveStatementParseBalanceFields = exports.shouldRecalculateStatementParseBalances = exports.isStatementParseManualBalanceField = exports.isStatementParseTransactionFieldChange = exports.getDateFilteredDeletedTransactionIds = exports.getStatementTransactionPeriodById = exports.getParsedStatementDataForView = exports.filterStatementTransactionsByPeriod = exports.isStatementTransactionInPeriod = exports.getEffectiveStatementPeriod = void 0;
3
+ exports.mergeStatementParseDeletedTransactionIds = exports.getDateFilteredDeletedTransactionIds = exports.getStatementTransactionPeriodById = exports.getParsedStatementDataForView = exports.filterStatementTransactionsByPeriod = exports.isStatementTransactionInPeriod = exports.getEffectiveStatementPeriod = void 0;
4
4
  const zeniDayJS_1 = require("../../../zeniDayJS");
5
5
  const getEffectiveStatementPeriod = (parsedMeta, localMeta) => ({
6
6
  statementStartDate: localMeta?.statementStartDate != null && localMeta.statementStartDate !== ''
@@ -73,122 +73,6 @@ const getDateFilteredDeletedTransactionIds = (allParsedTransactions, statementPe
73
73
  .map((transaction) => transaction.statementTransactionId);
74
74
  };
75
75
  exports.getDateFilteredDeletedTransactionIds = getDateFilteredDeletedTransactionIds;
76
- const roundCurrency = (value) => Math.round(value * 100) / 100;
77
- const toApiDateString = (value) => {
78
- if (value == null || value === '') {
79
- return '';
80
- }
81
- return (0, zeniDayJS_1.date)(value).format('YYYY-MM-DD');
82
- };
83
- const computeStatementTransactionTotals = (transactions) => {
84
- const totalDeposits = roundCurrency(transactions
85
- .filter((txn) => txn.transactionDirection === 'credit')
86
- .reduce((sum, txn) => sum + Math.abs(txn.amount ?? 0), 0));
87
- const totalPayments = roundCurrency(transactions
88
- .filter((txn) => txn.transactionDirection === 'debit')
89
- .reduce((sum, txn) => sum + Math.abs(txn.amount ?? 0), 0));
90
- return { totalDeposits, totalPayments };
91
- };
92
- const computeStatementClosingBalance = (openingBalance, totalDeposits, totalPayments, accountType) => roundCurrency(accountType === 'credit_card'
93
- ? openingBalance + totalPayments - totalDeposits
94
- : openingBalance + totalDeposits - totalPayments);
95
- const filterFormTransactionsInPeriod = (transactions, statementStartDate, statementEndDate) => {
96
- if (statementStartDate === '' || statementEndDate === '') {
97
- return transactions;
98
- }
99
- const periodStart = (0, zeniDayJS_1.date)(statementStartDate).startOf('day');
100
- const periodEnd = (0, zeniDayJS_1.date)(statementEndDate).startOf('day');
101
- return transactions.filter((txn) => {
102
- if (txn.transactionDate === '') {
103
- return true;
104
- }
105
- const txnDate = (0, zeniDayJS_1.date)(txn.transactionDate).startOf('day');
106
- return (txnDate.isSameOrAfter(periodStart, 'day') &&
107
- txnDate.isSameOrBefore(periodEnd, 'day'));
108
- });
109
- };
110
- const isStatementParseTransactionFieldChange = (changedField) => changedField === 'transactions' ||
111
- changedField?.startsWith('transactions.') === true;
112
- exports.isStatementParseTransactionFieldChange = isStatementParseTransactionFieldChange;
113
- const isStatementParseBalanceAffectingTransactionFieldChange = (changedField) => {
114
- if (changedField === 'transactions') {
115
- return true;
116
- }
117
- if (changedField?.startsWith('transactions.') !== true) {
118
- return false;
119
- }
120
- return !changedField.endsWith('.transactionMemo');
121
- };
122
- const isStatementParseManualBalanceField = (field) => field === 'totalDeposits' ||
123
- field === 'totalPayments' ||
124
- field === 'closingBalance';
125
- exports.isStatementParseManualBalanceField = isStatementParseManualBalanceField;
126
- const shouldRecalculateStatementParseBalances = (changedField) => isStatementParseBalanceAffectingTransactionFieldChange(changedField) ||
127
- changedField === 'statementStartDate' ||
128
- changedField === 'statementEndDate';
129
- exports.shouldRecalculateStatementParseBalances = shouldRecalculateStatementParseBalances;
130
- const resolveStatementParseBalanceFields = (formView, changedField, accountType) => {
131
- const inPeriodTransactions = filterFormTransactionsInPeriod(formView.transactions, formView.statementStartDate, formView.statementEndDate);
132
- const { totalDeposits, totalPayments } = computeStatementTransactionTotals(inPeriodTransactions);
133
- if (!(0, exports.shouldRecalculateStatementParseBalances)(changedField)) {
134
- return {
135
- openingBalance: formView.openingBalance,
136
- closingBalance: formView.closingBalance,
137
- totalDeposits: formView.totalDeposits,
138
- totalPayments: formView.totalPayments,
139
- };
140
- }
141
- return {
142
- openingBalance: formView.openingBalance,
143
- totalDeposits,
144
- totalPayments,
145
- closingBalance: computeStatementClosingBalance(formView.openingBalance, totalDeposits, totalPayments, accountType),
146
- };
147
- };
148
- exports.resolveStatementParseBalanceFields = resolveStatementParseBalanceFields;
149
- const areStatementParseBalancesConsistent = (formView, accountType) => {
150
- // Validate the four balance fields against each other (what the user sees),
151
- // not against recomputed transaction totals. Manual deposits/payments edits
152
- // are allowed as long as closing matches the account-type formula.
153
- return (roundCurrency(formView.closingBalance) ===
154
- computeStatementClosingBalance(formView.openingBalance, formView.totalDeposits, formView.totalPayments, accountType));
155
- };
156
- exports.areStatementParseBalancesConsistent = areStatementParseBalancesConsistent;
157
- const mapParsedTransactionToFormRow = (transaction, index) => ({
158
- id: transaction.statementTransactionId || `parsed-${index}`,
159
- transactionDate: toApiDateString(transaction.transactionDate),
160
- transactionMemo: transaction.transactionMemo,
161
- amount: transaction.amount,
162
- transactionDirection: transaction.transactionDirection,
163
- statementTransactionId: transaction.statementTransactionId,
164
- });
165
- const mapLocalTransactionToFormRow = (transaction, id) => ({
166
- id,
167
- transactionDate: toApiDateString(transaction.transactionDate),
168
- transactionMemo: transaction.transactionMemo,
169
- amount: transaction.amount,
170
- transactionDirection: transaction.transactionDirection,
171
- statementTransactionId: transaction.statementTransactionId,
172
- });
173
- const buildStatementParseFormTransactions = (originalTransactions, localData) => {
174
- const deletedIdSet = new Set(localData.statementTransactions?.deletedIds ?? []);
175
- const updatedById = new Map((localData.statementTransactions?.updated ?? [])
176
- .filter((transaction) => transaction.statementTransactionId != null)
177
- .map((transaction) => [
178
- transaction.statementTransactionId,
179
- transaction,
180
- ]));
181
- const persistedTransactions = originalTransactions
182
- .filter((transaction) => !deletedIdSet.has(transaction.statementTransactionId))
183
- .map((transaction, index) => {
184
- const updated = updatedById.get(transaction.statementTransactionId);
185
- return updated != null
186
- ? mapLocalTransactionToFormRow(updated, transaction.statementTransactionId)
187
- : mapParsedTransactionToFormRow(transaction, index);
188
- });
189
- const addedTransactions = (localData.statementTransactions?.added ?? []).map((transaction, index) => mapLocalTransactionToFormRow(transaction, `added-${index}`));
190
- return [...persistedTransactions, ...addedTransactions];
191
- };
192
76
  const mergeStatementParseDeletedTransactionIds = (originalTransactions, statementPeriod, currentTransactionIds) => {
193
77
  const dateFilteredDeletedIds = (0, exports.getDateFilteredDeletedTransactionIds)(originalTransactions, statementPeriod);
194
78
  const dateFilteredDeletedIdSet = new Set(dateFilteredDeletedIds);
@@ -200,119 +84,3 @@ const mergeStatementParseDeletedTransactionIds = (originalTransactions, statemen
200
84
  return [...new Set([...userDeletedIds, ...dateFilteredDeletedIds])];
201
85
  };
202
86
  exports.mergeStatementParseDeletedTransactionIds = mergeStatementParseDeletedTransactionIds;
203
- const resolveStatementMetaDates = (parsedStatementMeta, localMeta) => ({
204
- statementStartDate: localMeta?.statementStartDate != null && localMeta.statementStartDate !== ''
205
- ? localMeta.statementStartDate
206
- : (parsedStatementMeta?.statementStartDate ?? ''),
207
- statementEndDate: localMeta?.statementEndDate != null && localMeta.statementEndDate !== ''
208
- ? localMeta.statementEndDate
209
- : (parsedStatementMeta?.statementEndDate ?? ''),
210
- });
211
- const getStatementParseFormView = ({ accountType, parsedStatementMeta, originalTransactions, statementUpdateLocalData, }) => {
212
- const transactions = statementUpdateLocalData != null
213
- ? buildStatementParseFormTransactions(originalTransactions, statementUpdateLocalData)
214
- : originalTransactions.map(mapParsedTransactionToFormRow);
215
- const localMeta = statementUpdateLocalData?.statementMeta;
216
- const { statementStartDate, statementEndDate } = resolveStatementMetaDates(parsedStatementMeta, localMeta);
217
- const { totalDeposits, totalPayments } = computeStatementTransactionTotals(filterFormTransactionsInPeriod(transactions, statementStartDate, statementEndDate));
218
- const openingBalance = localMeta?.openingBalance ?? parsedStatementMeta?.openingBalance ?? 0;
219
- const computedClosingBalance = computeStatementClosingBalance(openingBalance, totalDeposits, totalPayments, accountType);
220
- return {
221
- statementStartDate,
222
- statementEndDate,
223
- openingBalance,
224
- closingBalance: localMeta?.closingBalance ?? computedClosingBalance,
225
- totalDeposits: localMeta?.totalDeposits ?? totalDeposits,
226
- totalPayments: localMeta?.totalPayments ?? totalPayments,
227
- transactions: filterFormTransactionsInPeriod(transactions, statementStartDate, statementEndDate),
228
- };
229
- };
230
- exports.getStatementParseFormView = getStatementParseFormView;
231
- const toStatementUpdateLocalDataFromFormView = ({ accountType, formView, parsedStatementMeta, originalTransactions, changedField, }) => {
232
- const balanceFields = (0, exports.resolveStatementParseBalanceFields)(formView, changedField, accountType);
233
- const currentTransactionIds = new Set(formView.transactions
234
- .map((transaction) => transaction.statementTransactionId)
235
- .filter((id) => id != null && id !== ''));
236
- const added = [];
237
- const updated = [];
238
- formView.transactions.forEach((transaction) => {
239
- const transactionDate = toApiDateString(transaction.transactionDate);
240
- if (transaction.statementTransactionId == null) {
241
- if (transaction.transactionDirection != null) {
242
- added.push({
243
- amount: transaction.amount,
244
- transactionDate,
245
- transactionDirection: transaction.transactionDirection,
246
- transactionMemo: transaction.transactionMemo,
247
- });
248
- }
249
- return;
250
- }
251
- const original = originalTransactions.find((candidate) => candidate.statementTransactionId === transaction.statementTransactionId);
252
- if (original != null &&
253
- (original.amount !== transaction.amount ||
254
- toApiDateString(original.transactionDate) !== transactionDate ||
255
- original.transactionMemo !== transaction.transactionMemo)) {
256
- updated.push({
257
- amount: transaction.amount,
258
- transactionDate,
259
- transactionDirection: transaction.transactionDirection,
260
- transactionMemo: transaction.transactionMemo,
261
- statementTransactionId: transaction.statementTransactionId,
262
- });
263
- }
264
- });
265
- const statementPeriod = {
266
- statementStartDate: toApiDateString(formView.statementStartDate),
267
- statementEndDate: toApiDateString(formView.statementEndDate),
268
- };
269
- const deletedIds = (0, exports.mergeStatementParseDeletedTransactionIds)(originalTransactions, statementPeriod, currentTransactionIds);
270
- const deletedIdSet = new Set(deletedIds);
271
- const inPeriodAdded = filterFormTransactionsInPeriod(added, statementPeriod.statementStartDate, statementPeriod.statementEndDate);
272
- const inPeriodUpdated = updated.filter((transaction) => transaction.statementTransactionId != null &&
273
- !deletedIdSet.has(transaction.statementTransactionId));
274
- return {
275
- statementMeta: {
276
- closingBalance: balanceFields.closingBalance,
277
- openingBalance: balanceFields.openingBalance,
278
- statementDataStatus: parsedStatementMeta?.statementDataStatus ?? {
279
- code: '',
280
- label: '',
281
- },
282
- statementEndDate: statementPeriod.statementEndDate,
283
- statementStartDate: statementPeriod.statementStartDate,
284
- statementStatus: parsedStatementMeta?.statementStatus ?? {
285
- code: '',
286
- label: '',
287
- },
288
- statementUploadId: parsedStatementMeta?.statementUploadId ?? '',
289
- totalDeposits: balanceFields.totalDeposits,
290
- totalPayments: balanceFields.totalPayments,
291
- },
292
- statementTransactions: {
293
- added: inPeriodAdded,
294
- deletedIds,
295
- updated: inPeriodUpdated,
296
- },
297
- };
298
- };
299
- exports.toStatementUpdateLocalDataFromFormView = toStatementUpdateLocalDataFromFormView;
300
- const applyStatementParseFormChange = ({ accountType, formView, changedField, parsedStatementMeta, originalTransactions, }) => {
301
- const balanceFields = (0, exports.resolveStatementParseBalanceFields)(formView, changedField, accountType);
302
- const shouldRecalculate = (0, exports.shouldRecalculateStatementParseBalances)(changedField);
303
- const mergedFormView = shouldRecalculate
304
- ? { ...formView, ...balanceFields }
305
- : formView;
306
- return {
307
- didRecalculateBalances: shouldRecalculate,
308
- formView: mergedFormView,
309
- localData: (0, exports.toStatementUpdateLocalDataFromFormView)({
310
- accountType,
311
- formView: mergedFormView,
312
- parsedStatementMeta,
313
- originalTransactions,
314
- changedField,
315
- }),
316
- };
317
- };
318
- exports.applyStatementParseFormChange = applyStatementParseFormChange;
@@ -2,7 +2,7 @@ import { Amount } from '../../../commonStateTypes/amount';
2
2
  import { FetchStateAndError, ID } from '../../../commonStateTypes/common';
3
3
  import { SortOrder } from '../../../commonStateTypes/selectorTypes/sortOrderTypes';
4
4
  import { MonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
5
- import { AccountType, ReconciliationAccountSourceType } from '../../../entity/account/accountState';
5
+ import { ReconciliationAccountSourceType } from '../../../entity/account/accountState';
6
6
  import { BankStatusCodeType } from '../../../entity/accountRecon/accountReconState';
7
7
  import { CustomerBase } from '../../../entity/customer/customerState';
8
8
  import { VendorBase } from '../../../entity/vendor/vendorState';
@@ -251,6 +251,12 @@ export interface StatementTransactionForUpdate {
251
251
  transactionDate: string;
252
252
  transactionDirection: 'debit' | 'credit';
253
253
  transactionMemo: string;
254
+ /**
255
+ * Client-only stable id for unsaved added rows. Preserved across local-data
256
+ * rebuilds so form rows survive add/delete without index remapping. Not sent
257
+ * to the API.
258
+ */
259
+ id?: string;
254
260
  statementTransactionId?: string;
255
261
  }
256
262
  export interface StatementTransactionsUpdate {
@@ -258,49 +264,6 @@ export interface StatementTransactionsUpdate {
258
264
  deletedIds: string[];
259
265
  updated: StatementTransactionForUpdate[];
260
266
  }
261
- export type StatementParseManualBalanceField = 'totalDeposits' | 'totalPayments' | 'closingBalance';
262
- export interface StatementParseFormTransaction {
263
- amount: number;
264
- id: string;
265
- transactionDate: string;
266
- transactionDirection: 'debit' | 'credit';
267
- transactionMemo: string;
268
- statementTransactionId?: string;
269
- }
270
- export interface StatementParseFormView {
271
- closingBalance: number;
272
- openingBalance: number;
273
- statementEndDate: string;
274
- statementStartDate: string;
275
- totalDeposits: number;
276
- totalPayments: number;
277
- transactions: StatementParseFormTransaction[];
278
- }
279
- export interface GetStatementParseFormViewInput {
280
- originalTransactions: StatementTransaction[];
281
- parsedStatementMeta: StatementMeta | undefined;
282
- statementUpdateLocalData: StatementUpdateLocalData | undefined;
283
- accountType?: AccountType;
284
- }
285
- export interface ToStatementUpdateLocalDataFromFormViewInput {
286
- formView: StatementParseFormView;
287
- originalTransactions: StatementTransaction[];
288
- parsedStatementMeta: StatementMeta | undefined;
289
- accountType?: AccountType;
290
- changedField?: string;
291
- }
292
- export interface ApplyStatementParseFormChangeInput {
293
- formView: StatementParseFormView;
294
- originalTransactions: StatementTransaction[];
295
- parsedStatementMeta: StatementMeta | undefined;
296
- accountType?: AccountType;
297
- changedField?: string;
298
- }
299
- export interface ApplyStatementParseFormChangeResult {
300
- didRecalculateBalances: boolean;
301
- formView: StatementParseFormView;
302
- localData: StatementUpdateLocalData;
303
- }
304
267
  export interface StatementUpdateLocalData {
305
268
  statementMeta: StatementMeta;
306
269
  statementTransactions: StatementTransactionsUpdate;
@@ -0,0 +1,34 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
3
+ import { RootState } from '../../../reducer';
4
+ import { ZeniAPI } from '../../../zeniAPI';
5
+ import { saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess, setGroupFrequency, toggleEventChannel } from '../notificationPreferencesViewReducer';
6
+ export type ActionType = ReturnType<typeof saveNotificationPreferences> | ReturnType<typeof saveNotificationPreferencesFailure> | ReturnType<typeof saveNotificationPreferencesSuccess> | ReturnType<typeof setGroupFrequency> | ReturnType<typeof toggleEventChannel> | ReturnType<typeof openSnackbar>;
7
+ export declare const saveNotificationPreferencesEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => import("rxjs").Observable<{
8
+ payload: {
9
+ messageSection: import("../../..").SnackbarMessageSections;
10
+ messageText: import("../../..").SnackbarMessageSectionTexts;
11
+ type: import("../../..").SnackbarMessageType;
12
+ showStatusIcon?: boolean;
13
+ variables?: {
14
+ variableName: string;
15
+ variableValue: string;
16
+ }[];
17
+ };
18
+ type: "snackbar/openSnackbar";
19
+ } | {
20
+ payload: undefined;
21
+ type: "notificationPreferencesView/saveNotificationPreferences";
22
+ } | {
23
+ payload: import("../notificationPreferencesViewReducer").SaveNotificationPreferencesFailurePayload;
24
+ type: "notificationPreferencesView/saveNotificationPreferencesFailure";
25
+ } | {
26
+ payload: import("../notificationPreferencesViewReducer").SaveNotificationPreferencesSuccessPayload;
27
+ type: "notificationPreferencesView/saveNotificationPreferencesSuccess";
28
+ } | {
29
+ payload: import("../notificationPreferencesViewReducer").SetFrequencyPayload;
30
+ type: "notificationPreferencesView/setGroupFrequency";
31
+ } | {
32
+ payload: import("../notificationPreferencesViewReducer").TogglePreferencePayload;
33
+ type: "notificationPreferencesView/toggleEventChannel";
34
+ }>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveNotificationPreferencesEpic = void 0;
4
+ const rxjs_1 = require("rxjs");
5
+ const operators_1 = require("rxjs/operators");
6
+ const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
7
+ const responsePayload_1 = require("../../../responsePayload");
8
+ const notificationPreferencesEndpoint_1 = require("../notificationPreferencesEndpoint");
9
+ const notificationPreferencesViewPayload_1 = require("../notificationPreferencesViewPayload");
10
+ const notificationPreferencesViewReducer_1 = require("../notificationPreferencesViewReducer");
11
+ const notificationPreferencesViewSelector_1 = require("../notificationPreferencesViewSelector");
12
+ const DEBOUNCE_MS = 300;
13
+ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)((action) => notificationPreferencesViewReducer_1.toggleEventChannel.match(action) || notificationPreferencesViewReducer_1.setGroupFrequency.match(action)),
14
+ // Coalesce bursts of toggles into a single PUT. Trade-off: rapid clickers
15
+ // defer saves until they pause. Consider a max-wait wrapper if this
16
+ // becomes a UX problem.
17
+ (0, operators_1.debounceTime)(DEBOUNCE_MS), (0, operators_1.withLatestFrom)(state$),
18
+ // `switchMap` unsubscribes from any in-flight PUT observable when a new
19
+ // debounced batch fires. The prior HTTP request has already left the
20
+ // client — the network call is NOT cancelled — but its response is
21
+ // ignored, so stale success/failure actions cannot clobber the newer
22
+ // batch's state. Server-side ordering is arrival-order LWW.
23
+ (0, operators_1.switchMap)(([, state]) => {
24
+ const overrides = (0, notificationPreferencesViewSelector_1.getNotificationLocalOverrides)(state);
25
+ const preferencesPatch = (0, notificationPreferencesViewPayload_1.mapPreferencesToPayload)(overrides);
26
+ if (Object.keys(preferencesPatch).length === 0) {
27
+ // No diff → do not dispatch a synthetic success (that would wipe
28
+ // stored preferences via the reducer). Simply no-op.
29
+ return rxjs_1.EMPTY;
30
+ }
31
+ // Snapshot the Cancel epoch at PUT-dispatch time. Every Cancel bumps
32
+ // `cancelEpoch`; the reducer compares the captured value on Success/
33
+ // Failure and skips the merge when the value has advanced (user
34
+ // pressed Cancel since this save started). See Cursor Bugbot 4aa021f7
35
+ // — the earlier counter model failed when `switchMap` abandoned an
36
+ // in-flight save's response (Success/Failure never dispatched, so
37
+ // the counter never drained and future saves got their merge dropped).
38
+ const cancelEpochAtDispatch = state.notificationPreferencesViewState.cancelEpoch;
39
+ const body = { task_checklist_preferences: preferencesPatch };
40
+ // Suppress the error snackbar when the response lands AFTER the user
41
+ // pressed Cancel — the Failure reducer already swallows the state
42
+ // update, so the toast would be a stale error for a save the user
43
+ // abandoned. `StateObservable#value` reads the current cancelEpoch at
44
+ // response time (not at dispatch time — that's what
45
+ // `cancelEpochAtDispatch` captures). Cursor Bugbot da2f4fe2 + Nikunj.
46
+ const errorSnackbar = (0, snackbarReducer_1.openSnackbar)({
47
+ messageSection: 'notification_settings_saved',
48
+ messageText: 'failed',
49
+ type: 'error',
50
+ });
51
+ const isCancelledSince = () => cancelEpochAtDispatch <
52
+ state$.value.notificationPreferencesViewState.cancelEpoch;
53
+ // Abort the in-flight HTTP request when `switchMap` disposes this
54
+ // observable (new debounced batch fires, tenant switch, unmount).
55
+ // Without an AbortSignal, `switchMap` would drop the response
56
+ // client-side but the older PUT would still land on the server —
57
+ // under patch-semantic LWW, a slower older PUT arriving after a
58
+ // newer one silently reverts the user's latest choice on the
59
+ // server. Cursor Bugbot e3861a76. `AbortController.abort()` is
60
+ // idempotent so it's safe if the request already completed.
61
+ const controller = new AbortController();
62
+ const request$ = zeniAPI
63
+ .putAndGetJSON((0, notificationPreferencesEndpoint_1.notificationPreferencesUrl)(zeniAPI), body, undefined, controller.signal)
64
+ .pipe((0, operators_1.mergeMap)((response) => {
65
+ if ((0, responsePayload_1.isSuccessResponse)(response)) {
66
+ return (0, rxjs_1.of)((0, notificationPreferencesViewReducer_1.saveNotificationPreferencesSuccess)({
67
+ cancelEpochAtDispatch,
68
+ savedOverrides: overrides,
69
+ serverEcho: response.data?.task_checklist_preferences,
70
+ }));
71
+ }
72
+ const actions = [
73
+ (0, notificationPreferencesViewReducer_1.saveNotificationPreferencesFailure)({
74
+ cancelEpochAtDispatch,
75
+ error: response.status,
76
+ }),
77
+ ];
78
+ if (!isCancelledSince()) {
79
+ // Surface a user-visible error — without it the UI reverts to
80
+ // server truth after debounce and the failed toggle looks like
81
+ // a mystery UI bug.
82
+ actions.push(errorSnackbar);
83
+ }
84
+ return (0, rxjs_1.of)(...actions);
85
+ }), (0, operators_1.catchError)((error) => {
86
+ const actions = [
87
+ (0, notificationPreferencesViewReducer_1.saveNotificationPreferencesFailure)({
88
+ cancelEpochAtDispatch,
89
+ error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Save notification preferences failed: ' +
90
+ JSON.stringify(error)),
91
+ }),
92
+ ];
93
+ if (!isCancelledSince()) {
94
+ actions.push(errorSnackbar);
95
+ }
96
+ return (0, rxjs_1.of)(...actions);
97
+ }), (0, operators_1.finalize)(() => {
98
+ // switchMap disposal (new batch fires, cancel, unmount) → abort
99
+ // the still-in-flight HTTP request. No-op if the request has
100
+ // already completed successfully or errored.
101
+ controller.abort();
102
+ }));
103
+ // Flip savePreferencesState to In-Progress BEFORE the network call so
104
+ // any "saving…" UI can render.
105
+ return (0, rxjs_1.concat)((0, rxjs_1.of)((0, notificationPreferencesViewReducer_1.saveNotificationPreferences)()), request$);
106
+ }));
107
+ exports.saveNotificationPreferencesEpic = saveNotificationPreferencesEpic;
@@ -0,0 +1,2 @@
1
+ import { ZeniAPI } from '../../zeniAPI';
2
+ export declare const notificationPreferencesUrl: (zeniAPI: ZeniAPI) => string;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.notificationPreferencesUrl = void 0;
4
+ // Shared endpoint used by both the GET (envelope refresh in
5
+ // `fetchNotificationSettingsEpic`) and the PUT (patch in
6
+ // `saveNotificationPreferencesEpic`). Kept as a helper so an endpoint rename
7
+ // can't drift between the two callers.
8
+ const notificationPreferencesUrl = (zeniAPI) => `${zeniAPI.apiEndPoints.notificationMicroServiceBaseUrl}/1.0/notification-preferences`;
9
+ exports.notificationPreferencesUrl = notificationPreferencesUrl;
@@ -0,0 +1,7 @@
1
+ import { NotificationPreferences } from './notificationPreferencesViewState';
2
+ export interface NotificationPreferencesPayload {
3
+ event_enabled_by_channel?: Record<string, Record<string, boolean>>;
4
+ group_frequency_by_group_id?: Record<string, string>;
5
+ }
6
+ export declare const mapPayloadToPreferences: (payload?: NotificationPreferencesPayload) => NotificationPreferences;
7
+ export declare const mapPreferencesToPayload: (preferences: NotificationPreferences) => Record<string, unknown>;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapPreferencesToPayload = exports.mapPayloadToPreferences = void 0;
4
+ const notificationRegistryState_1 = require("../../entity/notificationRegistry/notificationRegistryState");
5
+ const mapEventEnabledByChannel = (raw) => {
6
+ if (raw == null) {
7
+ return {};
8
+ }
9
+ const out = {};
10
+ Object.entries(raw).forEach(([eventId, channelMap]) => {
11
+ const mapped = {};
12
+ Object.entries(channelMap).forEach(([channel, enabled]) => {
13
+ mapped[(0, notificationRegistryState_1.toNotificationChannel)(channel)] = enabled;
14
+ });
15
+ out[eventId] = mapped;
16
+ });
17
+ return out;
18
+ };
19
+ const mapPayloadToPreferences = (payload) => {
20
+ const groupFrequencyRaw = payload?.group_frequency_by_group_id ?? {};
21
+ const groupFrequencyByGroupId = Object.fromEntries(Object.entries(groupFrequencyRaw).map(([groupId, frequency]) => [
22
+ groupId,
23
+ (0, notificationRegistryState_1.toNotificationFrequency)(frequency),
24
+ ]));
25
+ return {
26
+ eventEnabledByChannel: mapEventEnabledByChannel(payload?.event_enabled_by_channel),
27
+ groupFrequencyByGroupId,
28
+ };
29
+ };
30
+ exports.mapPayloadToPreferences = mapPayloadToPreferences;
31
+ const mapPreferencesToPayload = (preferences) => {
32
+ const payload = {};
33
+ if (Object.keys(preferences.eventEnabledByChannel).length > 0) {
34
+ payload.event_enabled_by_channel = preferences.eventEnabledByChannel;
35
+ }
36
+ if (Object.keys(preferences.groupFrequencyByGroupId).length > 0) {
37
+ payload.group_frequency_by_group_id = preferences.groupFrequencyByGroupId;
38
+ }
39
+ return payload;
40
+ };
41
+ exports.mapPreferencesToPayload = mapPreferencesToPayload;
@@ -0,0 +1,30 @@
1
+ import { NotificationChannel, NotificationFrequency } from '../../entity/notificationRegistry/notificationRegistryState';
2
+ import { ZeniAPIStatus } from '../../responsePayload';
3
+ import { NotificationPreferencesPayload } from './notificationPreferencesViewPayload';
4
+ import { NotificationPreferences, NotificationPreferencesViewState } from './notificationPreferencesViewState';
5
+ export declare const initialState: NotificationPreferencesViewState;
6
+ export interface SetFrequencyPayload {
7
+ frequency: NotificationFrequency;
8
+ groupId: string;
9
+ }
10
+ export interface TogglePreferencePayload {
11
+ channel: NotificationChannel;
12
+ enabled: boolean;
13
+ eventId: string;
14
+ }
15
+ export interface FetchNotificationPreferencesSuccessPayload {
16
+ preferences: NotificationPreferencesPayload | undefined;
17
+ saveEpochAtDispatch: number;
18
+ }
19
+ export interface SaveNotificationPreferencesSuccessPayload {
20
+ cancelEpochAtDispatch: number;
21
+ savedOverrides: NotificationPreferences;
22
+ serverEcho: NotificationPreferencesPayload | undefined;
23
+ }
24
+ export interface SaveNotificationPreferencesFailurePayload {
25
+ cancelEpochAtDispatch: number;
26
+ error?: ZeniAPIStatus;
27
+ }
28
+ export declare const clearAllNotificationPreferencesView: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"notificationPreferencesView/clearAllNotificationPreferencesView">, clearNotificationPreferencesLocalOverrides: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"notificationPreferencesView/clearNotificationPreferencesLocalOverrides">, fetchNotificationPreferencesSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<FetchNotificationPreferencesSuccessPayload, "notificationPreferencesView/fetchNotificationPreferencesSuccess">, saveNotificationPreferences: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"notificationPreferencesView/saveNotificationPreferences">, saveNotificationPreferencesFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<SaveNotificationPreferencesFailurePayload, "notificationPreferencesView/saveNotificationPreferencesFailure">, saveNotificationPreferencesSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<SaveNotificationPreferencesSuccessPayload, "notificationPreferencesView/saveNotificationPreferencesSuccess">, setGroupFrequency: import("@reduxjs/toolkit").ActionCreatorWithPayload<SetFrequencyPayload, "notificationPreferencesView/setGroupFrequency">, toggleEventChannel: import("@reduxjs/toolkit").ActionCreatorWithPayload<TogglePreferencePayload, "notificationPreferencesView/toggleEventChannel">;
29
+ declare const _default: import("redux").Reducer<NotificationPreferencesViewState>;
30
+ export default _default;