@zeniai/client-epic-state 5.1.71 → 5.1.72-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 (71) 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 +11 -1
  5. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +2 -0
  6. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksPayload.d.ts +1 -0
  7. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksPayload.js +1 -0
  8. package/lib/entity/monthEndCloseChecks/monthEndCloseChecksState.d.ts +1 -0
  9. package/lib/entity/transaction/payloadTypes/scheduleTransactionPayload.d.ts +2 -0
  10. package/lib/entity/transaction/payloadTypes/scheduleTransactionPayload.js +2 -0
  11. package/lib/entity/transaction/stateTypes/scheduleTransaction.d.ts +2 -0
  12. package/lib/epic.d.ts +5 -1
  13. package/lib/epic.js +5 -1
  14. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +2 -0
  15. package/lib/esm/entity/jeSchedules/jeSchedulesSelector.js +11 -1
  16. package/lib/esm/entity/monthEndCloseChecks/monthEndCloseChecksPayload.js +1 -0
  17. package/lib/esm/entity/transaction/payloadTypes/scheduleTransactionPayload.js +2 -0
  18. package/lib/esm/epic.js +5 -1
  19. package/lib/esm/index.js +12 -6
  20. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +23 -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 +133 -8
  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 +110 -15
  30. package/lib/esm/view/expenseAutomationView/types/jeSchedulesViewState.js +3 -6
  31. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +13 -16
  32. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewPayload.js +7 -2
  33. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +55 -10
  34. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +23 -13
  35. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewState.js +1 -0
  36. package/lib/index.d.ts +13 -9
  37. package/lib/index.js +72 -48
  38. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.d.ts +2 -2
  39. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulesEpic.js +21 -1
  40. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/initializeNewScheduleEpic.d.ts +8 -0
  41. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/initializeNewScheduleEpic.js +25 -0
  42. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/saveNewScheduleEpic.d.ts +45 -0
  43. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/saveNewScheduleEpic.js +114 -0
  44. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/searchTransactionsForNewScheduleEpic.d.ts +8 -0
  45. package/lib/view/expenseAutomationView/epics/jeSchedule/newSchedule/searchTransactionsForNewScheduleEpic.js +77 -0
  46. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.d.ts +6 -0
  47. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +26 -0
  48. package/lib/view/expenseAutomationView/helpers/newScheduleLocalDataHelper.d.ts +36 -0
  49. package/lib/view/expenseAutomationView/helpers/newScheduleLocalDataHelper.js +145 -0
  50. package/lib/view/expenseAutomationView/payload/jeSchedulesPayload.d.ts +7 -1
  51. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +40 -9
  52. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +134 -8
  53. package/lib/view/expenseAutomationView/selectorTypes/jeNewScheduleViewSelectorTypes.d.ts +25 -0
  54. package/lib/view/expenseAutomationView/selectorTypes/jeNewScheduleViewSelectorTypes.js +2 -0
  55. package/lib/view/expenseAutomationView/selectorTypes/jeSchedulesSelectorTypes.d.ts +31 -7
  56. package/lib/view/expenseAutomationView/selectors/jeNewScheduleViewSelector.d.ts +10 -0
  57. package/lib/view/expenseAutomationView/selectors/jeNewScheduleViewSelector.js +75 -0
  58. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.d.ts +4 -1
  59. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +112 -14
  60. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +53 -6
  61. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.js +5 -8
  62. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.d.ts +2 -8
  63. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +10 -13
  64. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.d.ts +2 -1
  65. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.js +7 -2
  66. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +6 -1
  67. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +56 -11
  68. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +23 -15
  69. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +1 -0
  70. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.js +1 -0
  71. package/package.json +1 -1
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isAiAccountantSchedule = void 0;
7
+ exports.isAccruedScheduleRow = isAccruedScheduleRow;
6
8
  exports.getExpenseAutomationJESchedulesView = getExpenseAutomationJESchedulesView;
7
9
  const omit_1 = __importDefault(require("lodash/omit"));
8
10
  const orderBy_1 = __importDefault(require("lodash/orderBy"));
@@ -10,6 +12,7 @@ const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState")
10
12
  const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
11
13
  const accountSelector_1 = require("../../../entity/account/accountSelector");
12
14
  const jeSchedulesSelector_1 = require("../../../entity/jeSchedules/jeSchedulesSelector");
15
+ const jeSchedulesTypes_1 = require("../../../entity/jeSchedules/jeSchedulesTypes");
13
16
  const tenantSelector_1 = require("../../../entity/tenant/tenantSelector");
14
17
  const accountListSelector_1 = require("../../accountList/accountListSelector");
15
18
  const classListSelector_1 = require("../../classList/classListSelector");
@@ -18,6 +21,32 @@ const scheduleDetailSelector_1 = require("../../scheduleView/scheduleDetailView/
18
21
  const scheduleListHelper_1 = require("../../scheduleView/scheduleListView/scheduleListHelper");
19
22
  const expenseAutomationViewSelectorTypes_1 = require("../selectorTypes/expenseAutomationViewSelectorTypes");
20
23
  const jeAccountSettingsViewSelector_1 = require("./jeAccountSettingsViewSelector");
24
+ const jeNewScheduleViewSelector_1 = require("./jeNewScheduleViewSelector");
25
+ // Matches the AI-automation user addresses webc's schedule list row already
26
+ // checks (JEScheduleReadOnlyListRow/JEScheduleDraftListRow "Method" column,
27
+ // strings.ts aiBotEmail/botEmail) to determine whether a schedule was
28
+ // created by the AI accountant vs. a human.
29
+ const AI_BOT_EMAIL = 'ai@zeni.ai';
30
+ const BOT_EMAIL = 'bot@zeni.ai';
31
+ // Exported (rather than kept as a closure inside the selector) so it can be
32
+ // unit-tested directly — confidenceScore is a separate, more narrowly
33
+ // populated field that can be null on a schedule the bot user created,
34
+ // which previously caused these schedules to display as "AI Accountant" in
35
+ // the list but count under "Manual" in the KPI summary.
36
+ const isAiAccountantSchedule = (schedule) => schedule.createdByUser?.email === AI_BOT_EMAIL ||
37
+ schedule.createdByUser?.email === BOT_EMAIL;
38
+ exports.isAiAccountantSchedule = isAiAccountantSchedule;
39
+ // Accrued schedules are structurally distinct — no baseTransaction, carrying recurringAmount and
40
+ // memo in its place — so anything reading baseTransaction has to branch on this first.
41
+ function isAccruedScheduleRow(schedule) {
42
+ return schedule.jeScheduleType === 'accrued_expenses';
43
+ }
44
+ function getScheduleAmount(schedule) {
45
+ if (isAccruedScheduleRow(schedule)) {
46
+ return schedule.recurringAmount.amount;
47
+ }
48
+ return schedule.baseTransaction.amount.amount;
49
+ }
21
50
  function getJEScheduleSortAccessor(sortKey) {
22
51
  switch (sortKey) {
23
52
  case 'vendor':
@@ -29,7 +58,9 @@ function getJEScheduleSortAccessor(sortKey) {
29
58
  case 'depCategory':
30
59
  return (s) => s.jeCredit.account?.accountName?.toLowerCase();
31
60
  case 'scheduleCategory':
32
- return (s) => s.baseTransaction.account?.accountName?.toLowerCase();
61
+ return (s) => isAccruedScheduleRow(s)
62
+ ? undefined
63
+ : s.baseTransaction.account?.accountName?.toLowerCase();
33
64
  case 'startMonth':
34
65
  return (s) => s.startDate?.valueOf();
35
66
  case 'type':
@@ -57,11 +88,13 @@ function getJEScheduleSortAccessor(sortKey) {
57
88
  return s.balanceAsOfToday?.amount ?? null;
58
89
  };
59
90
  case 'totalAmount':
60
- return (s) => s.baseTransaction.amount.amount;
91
+ return (s) => getScheduleAmount(s);
61
92
  case 'transactionDate':
62
- return (s) => s.baseTransaction.date.valueOf();
93
+ return (s) => s.createTime?.valueOf();
63
94
  case 'memo':
64
- return (s) => s.baseTransaction.memo?.toLowerCase();
95
+ return (s) => isAccruedScheduleRow(s)
96
+ ? s.memo.toLowerCase()
97
+ : s.baseTransaction.memo?.toLowerCase();
65
98
  default:
66
99
  return (s) => s.startDate?.valueOf();
67
100
  }
@@ -73,9 +106,14 @@ function sortJEScheduledTransactions(transactions, sortKey, sortOrder) {
73
106
  function getExpenseAutomationJESchedulesView(state) {
74
107
  const jeScheduledTransaction = [];
75
108
  const failedJeScheduledTransaction = [];
109
+ const accruedSchedules = [];
76
110
  const { accountState, accountListState, classState, classListState, expenseAutomationJESchedulesViewState, expenseAutomationViewState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
77
- const { jeScheduleTransactionKeysByPeriod, failedJeScheduleTransactionKeysByPeriod, jeScheduleLocalDataById, postStatusById, ignoreStatusById, uiState, error, fetchState, refreshStatus, } = expenseAutomationJESchedulesViewState;
111
+ const { jeScheduleTransactionKeysByPeriod, jeScheduleAccruedKeysByPeriod, failedJeScheduleTransactionKeysByPeriod, jeScheduleLocalDataById, postStatusById, ignoreStatusById, uiState, error, fetchState, refreshStatus, newSchedule, } = expenseAutomationJESchedulesViewState;
78
112
  const accountSettingsView = (0, jeAccountSettingsViewSelector_1.getJEAccountSettingsView)(state);
113
+ const newScheduleView = newSchedule.selectedDetailScheduleType === 'fixed_assets' ||
114
+ newSchedule.selectedDetailScheduleType === 'prepaid_expenses'
115
+ ? (0, jeNewScheduleViewSelector_1.getNewScheduleView)(state, newSchedule.selectedDetailScheduleType)
116
+ : undefined;
79
117
  const currentTenant = (0, tenantSelector_1.getCurrentTenant)(state);
80
118
  const { selectedPeriodByTenantId } = expenseAutomationViewState;
81
119
  const selectedPeriod = selectedPeriodByTenantId[currentTenant.tenantId];
@@ -111,19 +149,24 @@ function getExpenseAutomationJESchedulesView(state) {
111
149
  : null;
112
150
  const jeScheduleTransactionKeys = jeScheduleTransactionKeysForSelectedPeriod ?? [];
113
151
  const failedJeScheduleTransactionKeys = failedJeScheduleTransactionKeysForSelectedPeriod ?? [];
152
+ const jeScheduleAccruedKeys = (monthYearPeriodId != null
153
+ ? jeScheduleAccruedKeysByPeriod[monthYearPeriodId]
154
+ : null) ?? [];
114
155
  jeScheduleTransactionKeys.forEach((jeScheduleKey) => {
115
156
  const jeScheduleDetails = (0, jeSchedulesSelector_1.getJEScheduledTransactionByJEScheduleTransactionKey)(jeScheduleKey, state);
116
157
  jeScheduledTransaction.push(jeScheduleDetails);
117
158
  });
159
+ jeScheduleAccruedKeys.forEach((jeAccruedScheduleKey) => {
160
+ const accruedScheduleDetails = (0, jeSchedulesSelector_1.getJEAccruedScheduleByJEScheduleKey)(jeAccruedScheduleKey, state);
161
+ if (accruedScheduleDetails != null) {
162
+ accruedSchedules.push(accruedScheduleDetails);
163
+ }
164
+ });
118
165
  failedJeScheduleTransactionKeys.forEach((jeScheduleKey) => {
119
166
  const jeScheduleDetails = (0, jeSchedulesSelector_1.getJEScheduledTransactionByJEScheduleTransactionKey)(jeScheduleKey, state);
120
167
  failedJeScheduledTransaction.push(jeScheduleDetails);
121
168
  });
122
- const { sortKey, sortOrder } = uiState;
123
- const draftSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'draft'), sortKey, sortOrder);
124
- const ongoingSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'ongoing'), sortKey, sortOrder);
125
- const completedSchedules = sortJEScheduledTransactions(jeScheduledTransaction.filter((schedule) => schedule.status.code === 'completed' ||
126
- schedule.status.code === 'marked_as_completed'), sortKey, sortOrder);
169
+ const { sortKey, sortOrder, selectedJETypeFilter } = uiState;
127
170
  const unsortedResolveSchedules = [];
128
171
  failedJeScheduledTransaction.forEach((schedule) => {
129
172
  schedule.scheduledJournalEntry.forEach((journalEntry) => {
@@ -135,10 +178,46 @@ function getExpenseAutomationJESchedulesView(state) {
135
178
  });
136
179
  const resolveSchedules = sortJEScheduledTransactions(unsortedResolveSchedules, sortKey, sortOrder);
137
180
  const validResolveForPending = unsortedResolveSchedules.filter((r) => jeScheduleLocalDataById[r.scheduledJournalEntry.scheduledJournalEntryID] != null);
181
+ const matchesTypeFilter = (schedule) => selectedJETypeFilter == null ||
182
+ schedule.jeScheduleType === selectedJETypeFilter;
183
+ const isCreatedStatus = (schedule) => schedule.status.code === 'ongoing' ||
184
+ schedule.status.code === 'completed' ||
185
+ schedule.status.code === 'marked_as_completed';
186
+ const allScheduleRows = [
187
+ ...jeScheduledTransaction,
188
+ ...accruedSchedules,
189
+ ];
138
190
  const pendingReviewSchedules = sortJEScheduledTransactions([
139
- ...jeScheduledTransaction.filter((schedule) => schedule.status.code === 'draft'),
191
+ ...allScheduleRows.filter((schedule) => schedule.status.code === 'draft' && matchesTypeFilter(schedule)),
140
192
  ...validResolveForPending,
141
193
  ], sortKey, sortOrder);
194
+ const createdSchedules = sortJEScheduledTransactions(allScheduleRows.filter((schedule) => isCreatedStatus(schedule) && matchesTypeFilter(schedule)), sortKey, sortOrder);
195
+ // KPI summary — derived from unfiltered schedules so counts are always accurate
196
+ const allSchedules = allScheduleRows;
197
+ const sumAmount = (schedules) => schedules.reduce((total, schedule) => total + (getScheduleAmount(schedule) ?? 0), 0);
198
+ const pendingReviewTotal = sumAmount(pendingReviewSchedules);
199
+ const createdTotal = sumAmount(createdSchedules);
200
+ const aiAccountantSchedules = allSchedules.filter(exports.isAiAccountantSchedule);
201
+ const manualSchedules = allSchedules.filter((schedule) => !(0, exports.isAiAccountantSchedule)(schedule));
202
+ const pendingUnfiltered = allSchedules.filter((schedule) => schedule.status.code === 'draft');
203
+ const kpiSummary = {
204
+ aiAccountant: {
205
+ count: aiAccountantSchedules.length,
206
+ amount: sumAmount(aiAccountantSchedules),
207
+ },
208
+ manual: {
209
+ count: manualSchedules.length,
210
+ amount: sumAmount(manualSchedules),
211
+ },
212
+ needsReview: {
213
+ count: pendingUnfiltered.length,
214
+ amount: sumAmount(pendingUnfiltered),
215
+ },
216
+ total: {
217
+ count: allSchedules.length,
218
+ amount: sumAmount(allSchedules),
219
+ },
220
+ };
142
221
  const allSteps = monthYearPeriodId != null
143
222
  ? (0, expenseAutomationViewSelectorTypes_1.getAllSteps)(monthEndCloseChecksState, monthYearPeriodId, currentTenant.tenantId)
144
223
  : [];
@@ -151,6 +230,22 @@ function getExpenseAutomationJESchedulesView(state) {
151
230
  { fetchState, error },
152
231
  monthEndFetchState,
153
232
  ]);
233
+ const countByType = (schedules) => {
234
+ const counts = jeSchedulesTypes_1.ALL_SCHEDULES_TYPES.reduce((acc, type) => ({ ...acc, [type]: 0 }), {});
235
+ schedules.forEach((schedule) => {
236
+ if (schedule.jeScheduleType != null) {
237
+ counts[schedule.jeScheduleType] += 1;
238
+ }
239
+ });
240
+ return counts;
241
+ };
242
+ const jeScheduleTypeCounts = {
243
+ active: countByType(allSchedules.filter((schedule) => schedule.status.code === 'draft' ||
244
+ schedule.status.code === 'ongoing' ||
245
+ schedule.status.code === 'completed' ||
246
+ schedule.status.code === 'marked_as_completed')),
247
+ ongoing: countByType(allSchedules.filter((schedule) => schedule.status.code === 'ongoing')),
248
+ };
154
249
  return {
155
250
  uncategorizedAccounts,
156
251
  allDepreciationAccountList: filteredDepAccounts,
@@ -159,12 +254,15 @@ function getExpenseAutomationJESchedulesView(state) {
159
254
  allAccountList: filteredAccounts,
160
255
  accountListNestedAccountHierarchy,
161
256
  allClassList: allClasses,
162
- completedSchedules,
163
- draftSchedules,
164
- ongoingSchedules,
257
+ createdSchedules,
258
+ createdTotal,
259
+ jeScheduleTypeCounts,
260
+ kpiSummary,
165
261
  pendingReviewSchedules,
262
+ pendingReviewTotal,
166
263
  resolveSchedules: resolveSchedules,
167
264
  accountSettingsView,
265
+ newScheduleView,
168
266
  postStatusById: postStatusById,
169
267
  ignoreStatusById: ignoreStatusById,
170
268
  fetchState: reducedFetchStatus.fetchState,
@@ -1,22 +1,28 @@
1
1
  import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
+ import { Amount } from '../../../commonStateTypes/amount';
2
3
  import { FetchStateAndError, FetchedState, ID } from '../../../commonStateTypes/common';
3
4
  import { Recommendation } from '../../../commonStateTypes/recommendationBase';
4
5
  import { SortOrder } from '../../../commonStateTypes/selectorTypes/sortOrderTypes';
5
- import { MonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
6
+ import { MonthYearPeriodId, ScheduleDaysOfMonth } from '../../../commonStateTypes/timePeriod';
6
7
  import { AccountBase } from '../../../entity/account/accountState';
7
- import { JEScheduleTransactionKey, ScheduleTypes } from '../../../entity/jeSchedules/jeSchedulesTypes';
8
+ import { JEScheduleKey, JEScheduleTransactionKey, ScheduleJournalEntryStatusCodeType, ScheduleTransactionID, ScheduleTypes } from '../../../entity/jeSchedules/jeSchedulesTypes';
8
9
  import { VendorBase } from '../../../entity/vendor/vendorState';
9
- export declare const toJEScheduleSortKey: (v: string) => "vendor" | "type" | "category" | "class" | "memo" | "transactionDate" | "totalAmount" | "runningBalance" | "depCategory" | "scheduleCategory" | "startMonth" | "amortizationPeriod" | "jePostingDate" | "remainingMonths";
10
+ import { ZeniDate } from '../../../zeniDayJS';
11
+ import { JETransactionLink } from '../../scheduleView/scheduleDetailView/scheduleDetailState';
12
+ export declare const toJEScheduleSortKey: (v: string) => "vendor" | "type" | "category" | "class" | "memo" | "transactionDate" | "totalAmount" | "runningBalance" | "jePostingDate" | "depCategory" | "scheduleCategory" | "startMonth" | "amortizationPeriod" | "remainingMonths";
10
13
  export type JEScheduleSortKey = ReturnType<typeof toJEScheduleSortKey>;
11
- export declare const toJEScheduleMainTab: (v: string) => "completed" | "pending_review" | "ongoing";
12
- export type JEScheduleMainTab = ReturnType<typeof toJEScheduleMainTab>;
14
+ export declare const toJEPageTab: (v: string) => "schedules" | "existing_schedules";
15
+ export type JEPageTab = ReturnType<typeof toJEPageTab>;
13
16
  export interface JESchedulesViewUIState {
17
+ createdGroupCollapsed: boolean;
18
+ pendingGroupCollapsed: boolean;
14
19
  scrollPosition: {
15
20
  scrollTop: number;
16
21
  scrollLeft?: number;
17
22
  } | undefined;
18
23
  searchString: string;
19
- selectedMainTab: JEScheduleMainTab;
24
+ selectedJETypeFilter: ScheduleTypes | null;
25
+ selectedPageTab: JEPageTab;
20
26
  sortKey: JEScheduleSortKey;
21
27
  sortOrder: SortOrder;
22
28
  totalCount: number;
@@ -35,12 +41,53 @@ export interface JESchedulesViewState extends FetchedState {
35
41
  accountSettings: AccountSettingsState;
36
42
  failedJeScheduleTransactionKeysByPeriod: Record<MonthYearPeriodId, JEScheduleTransactionKey[]>;
37
43
  ignoreStatusById: Record<JEScheduleTransactionKey, FetchStateAndError>;
44
+ jeScheduleAccruedKeysByPeriod: Record<MonthYearPeriodId, JEScheduleKey[]>;
38
45
  jeScheduleLocalDataById: Record<ID, JEScheduleLocalData>;
39
46
  jeScheduleTransactionKeysByPeriod: Record<MonthYearPeriodId, JEScheduleTransactionKey[]>;
47
+ newSchedule: NewJeScheduleState;
40
48
  postStatusById: Record<ID, FetchStateAndError>;
41
49
  refreshStatus: FetchStateAndError;
42
50
  uiState: JESchedulesViewUIState;
43
51
  }
52
+ export interface NewJeDetailLocalData {
53
+ amount: Amount;
54
+ jeLink: JETransactionLink;
55
+ memo: string;
56
+ postingDate: ZeniDate;
57
+ postingStatus: ScheduleJournalEntryStatusCodeType;
58
+ runningBalance: Amount;
59
+ }
60
+ export interface NewScheduleLocalData {
61
+ jeScheduleDetails: NewJeDetailLocalData[];
62
+ scheduleType: ScheduleTypes;
63
+ lastDayOfMonth?: boolean;
64
+ selectedAccount?: ID;
65
+ selectedBaseTransactionId?: ScheduleTransactionID;
66
+ selectedClass?: ID;
67
+ selectedDate?: ZeniDate;
68
+ selectedDayOfMonth?: ScheduleDaysOfMonth;
69
+ selectedJEScheduleKey?: JEScheduleTransactionKey;
70
+ selectedMonths?: number;
71
+ selectedVendor?: VendorBase;
72
+ }
73
+ export interface NewScheduleLocalDataFixedAssets extends NewScheduleLocalData {
74
+ assetId?: string;
75
+ selectedAccumulatedDepreciationCategory?: ID;
76
+ }
77
+ export interface SearchTransactionsState extends FetchStateAndError {
78
+ searchResults: JEScheduleTransactionKey[];
79
+ searchString?: string;
80
+ }
81
+ export declare const MIN_NEW_SCHEDULE_SEARCH_LENGTH = 2;
82
+ export interface NewJeScheduleState extends FetchStateAndError {
83
+ newScheduleState: {
84
+ fixed_assets: NewScheduleLocalDataFixedAssets;
85
+ prepaid_expenses: NewScheduleLocalData;
86
+ };
87
+ saveStatus: FetchStateAndError;
88
+ searchTransactions: SearchTransactionsState;
89
+ selectedDetailScheduleType?: ScheduleTypes;
90
+ }
44
91
  export interface AccountSettingsLocalData {
45
92
  previousAccruedExpensesAccount: AccountBase[];
46
93
  previousFixedAssetsAccount: AccountBase[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toJEScheduleMainTab = exports.toJEScheduleSortKey = void 0;
3
+ exports.MIN_NEW_SCHEDULE_SEARCH_LENGTH = exports.toJEPageTab = exports.toJEScheduleSortKey = void 0;
4
4
  const stringToUnion_1 = require("../../../commonStateTypes/stringToUnion");
5
5
  const JE_SCHEDULE_SORT_KEYS = [
6
6
  'vendor',
@@ -20,10 +20,7 @@ const JE_SCHEDULE_SORT_KEYS = [
20
20
  ];
21
21
  const toJEScheduleSortKey = (v) => (0, stringToUnion_1.stringToUnion)(v, JE_SCHEDULE_SORT_KEYS);
22
22
  exports.toJEScheduleSortKey = toJEScheduleSortKey;
23
- const JE_SCHEDULE_MAIN_TABS = [
24
- 'pending_review',
25
- 'ongoing',
26
- 'completed',
27
- ];
28
- const toJEScheduleMainTab = (v) => (0, stringToUnion_1.stringToUnion)(v, JE_SCHEDULE_MAIN_TABS);
29
- exports.toJEScheduleMainTab = toJEScheduleMainTab;
23
+ const JE_PAGE_TABS = ['schedules', 'existing_schedules'];
24
+ const toJEPageTab = (v) => (0, stringToUnion_1.stringToUnion)(v, JE_PAGE_TABS);
25
+ exports.toJEPageTab = toJEPageTab;
26
+ exports.MIN_NEW_SCHEDULE_SEARCH_LENGTH = 2;
@@ -2,8 +2,8 @@ import { ActionsObservable, StateObservable } from 'redux-observable';
2
2
  import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
3
3
  import { RootState } from '../../../reducer';
4
4
  import { ZeniAPI } from '../../../zeniAPI';
5
- import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess, setGroupFrequency, toggleEventChannel } from '../notificationPreferencesViewReducer';
6
- export type ActionType = ReturnType<typeof clearAllNotificationPreferencesView> | ReturnType<typeof clearNotificationPreferencesLocalOverrides> | ReturnType<typeof saveNotificationPreferences> | ReturnType<typeof saveNotificationPreferencesFailure> | ReturnType<typeof saveNotificationPreferencesSuccess> | ReturnType<typeof setGroupFrequency> | ReturnType<typeof toggleEventChannel> | ReturnType<typeof openSnackbar>;
5
+ import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess } from '../notificationPreferencesViewReducer';
6
+ export type ActionType = ReturnType<typeof clearAllNotificationPreferencesView> | ReturnType<typeof clearNotificationPreferencesLocalOverrides> | ReturnType<typeof saveNotificationPreferences> | ReturnType<typeof saveNotificationPreferencesFailure> | ReturnType<typeof saveNotificationPreferencesSuccess> | ReturnType<typeof openSnackbar>;
7
7
  export declare const saveNotificationPreferencesEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => import("rxjs").Observable<{
8
8
  payload: undefined;
9
9
  type: "notificationPreferencesView/clearAllNotificationPreferencesView";
@@ -31,10 +31,4 @@ export declare const saveNotificationPreferencesEpic: (actions$: ActionsObservab
31
31
  } | {
32
32
  payload: import("../notificationPreferencesViewReducer").SaveNotificationPreferencesSuccessPayload;
33
33
  type: "notificationPreferencesView/saveNotificationPreferencesSuccess";
34
- } | {
35
- payload: import("../notificationPreferencesViewReducer").SetFrequencyPayload;
36
- type: "notificationPreferencesView/setGroupFrequency";
37
- } | {
38
- payload: import("../notificationPreferencesViewReducer").TogglePreferencePayload;
39
- type: "notificationPreferencesView/toggleEventChannel";
40
34
  }>;
@@ -9,14 +9,9 @@ const notificationPreferencesEndpoint_1 = require("../notificationPreferencesEnd
9
9
  const notificationPreferencesViewPayload_1 = require("../notificationPreferencesViewPayload");
10
10
  const notificationPreferencesViewReducer_1 = require("../notificationPreferencesViewReducer");
11
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
12
+ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)((action) => notificationPreferencesViewReducer_1.saveNotificationPreferences.match(action)), (0, operators_1.withLatestFrom)(state$),
13
+ // `switchMap` unsubscribes from any in-flight PUT observable when a newer
14
+ // Save fires. The prior HTTP request has already left the
20
15
  // client — the network call is NOT cancelled — but its response is
21
16
  // ignored, so stale success/failure actions cannot clobber the newer
22
17
  // batch's state. Server-side ordering is arrival-order LWW.
@@ -51,7 +46,7 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
51
46
  const isCancelledSince = () => cancelEpochAtDispatch <
52
47
  state$.value.notificationPreferencesViewState.cancelEpoch;
53
48
  // Abort the in-flight HTTP request when `switchMap` disposes this
54
- // observable (new debounced batch fires, tenant switch, unmount).
49
+ // observable (a newer Save fires, tenant switch, unmount).
55
50
  // Without an AbortSignal, `switchMap` would drop the response
56
51
  // client-side but the older PUT would still land on the server —
57
52
  // under patch-semantic LWW, a slower older PUT arriving after a
@@ -77,7 +72,7 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
77
72
  ];
78
73
  if (!isCancelledSince()) {
79
74
  // Surface a user-visible error — without it the UI reverts to
80
- // server truth after debounce and the failed toggle looks like
75
+ // server truth after the save and the failed toggle looks like
81
76
  // a mystery UI bug.
82
77
  actions.push(errorSnackbar);
83
78
  }
@@ -113,8 +108,10 @@ const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.
113
108
  // under patch-semantic LWW. Cursor Bugbot 3637969823 + 3641372002.
114
109
  (0, operators_1.takeUntil)(actions$.pipe((0, operators_1.filter)((action) => notificationPreferencesViewReducer_1.clearNotificationPreferencesLocalOverrides.match(action) ||
115
110
  notificationPreferencesViewReducer_1.clearAllNotificationPreferencesView.match(action)))));
116
- // Flip savePreferencesState to In-Progress BEFORE the network call so
117
- // any "saving…" UI can render.
118
- return (0, rxjs_1.concat)((0, rxjs_1.of)((0, notificationPreferencesViewReducer_1.saveNotificationPreferences)()), request$);
111
+ // `saveNotificationPreferences` (the trigger) already flipped
112
+ // savePreferencesState to In-Progress via its reducer case, so the PUT
113
+ // observable is returned directly — re-emitting the trigger here would
114
+ // loop the epic on itself.
115
+ return request$;
119
116
  }));
120
117
  exports.saveNotificationPreferencesEpic = saveNotificationPreferencesEpic;
@@ -1,7 +1,8 @@
1
1
  import { NotificationPreferences } from './notificationPreferencesViewState';
2
2
  export interface NotificationPreferencesPayload {
3
3
  event_enabled_by_channel?: Record<string, Record<string, boolean>>;
4
+ group_channel_enabled_by_group_id?: Record<string, Record<string, boolean>>;
4
5
  group_frequency_by_group_id?: Record<string, string>;
5
6
  }
6
7
  export declare const mapPayloadToPreferences: (payload?: NotificationPreferencesPayload) => NotificationPreferences;
7
- export declare const mapPreferencesToPayload: (preferences: NotificationPreferences) => Record<string, unknown>;
8
+ export declare const mapPreferencesToPayload: (preferences: NotificationPreferences) => NotificationPreferencesPayload;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mapPreferencesToPayload = exports.mapPayloadToPreferences = void 0;
4
4
  const notificationRegistryState_1 = require("../../entity/notificationRegistry/notificationRegistryState");
5
- const mapEventEnabledByChannel = (raw) => {
5
+ const mapChannelEnabledMap = (raw) => {
6
6
  if (raw == null) {
7
7
  return {};
8
8
  }
@@ -23,7 +23,8 @@ const mapPayloadToPreferences = (payload) => {
23
23
  (0, notificationRegistryState_1.toNotificationFrequency)(frequency),
24
24
  ]));
25
25
  return {
26
- eventEnabledByChannel: mapEventEnabledByChannel(payload?.event_enabled_by_channel),
26
+ eventEnabledByChannel: mapChannelEnabledMap(payload?.event_enabled_by_channel),
27
+ groupChannelEnabledByGroupId: mapChannelEnabledMap(payload?.group_channel_enabled_by_group_id),
27
28
  groupFrequencyByGroupId,
28
29
  };
29
30
  };
@@ -33,6 +34,10 @@ const mapPreferencesToPayload = (preferences) => {
33
34
  if (Object.keys(preferences.eventEnabledByChannel).length > 0) {
34
35
  payload.event_enabled_by_channel = preferences.eventEnabledByChannel;
35
36
  }
37
+ if (Object.keys(preferences.groupChannelEnabledByGroupId).length > 0) {
38
+ payload.group_channel_enabled_by_group_id =
39
+ preferences.groupChannelEnabledByGroupId;
40
+ }
36
41
  if (Object.keys(preferences.groupFrequencyByGroupId).length > 0) {
37
42
  payload.group_frequency_by_group_id = preferences.groupFrequencyByGroupId;
38
43
  }
@@ -12,6 +12,11 @@ export interface TogglePreferencePayload {
12
12
  enabled: boolean;
13
13
  eventId: string;
14
14
  }
15
+ export interface SetGroupChannelMasterPayload {
16
+ channel: NotificationChannel;
17
+ enabled: boolean;
18
+ groupId: string;
19
+ }
15
20
  export interface FetchNotificationPreferencesSuccessPayload {
16
21
  preferences: NotificationPreferencesPayload | undefined;
17
22
  saveEpochAtDispatch: number;
@@ -25,6 +30,6 @@ export interface SaveNotificationPreferencesFailurePayload {
25
30
  cancelEpochAtDispatch: number;
26
31
  error?: ZeniAPIStatus;
27
32
  }
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">;
33
+ 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">, setGroupChannelMaster: import("@reduxjs/toolkit").ActionCreatorWithPayload<SetGroupChannelMasterPayload, "notificationPreferencesView/setGroupChannelMaster">, setGroupFrequency: import("@reduxjs/toolkit").ActionCreatorWithPayload<SetFrequencyPayload, "notificationPreferencesView/setGroupFrequency">, toggleEventChannel: import("@reduxjs/toolkit").ActionCreatorWithPayload<TogglePreferencePayload, "notificationPreferencesView/toggleEventChannel">;
29
34
  declare const _default: import("redux").Reducer<NotificationPreferencesViewState>;
30
35
  export default _default;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.toggleEventChannel = exports.setGroupFrequency = exports.saveNotificationPreferencesSuccess = exports.saveNotificationPreferencesFailure = exports.saveNotificationPreferences = exports.fetchNotificationPreferencesSuccess = exports.clearNotificationPreferencesLocalOverrides = exports.clearAllNotificationPreferencesView = exports.initialState = void 0;
4
+ exports.toggleEventChannel = exports.setGroupFrequency = exports.setGroupChannelMaster = exports.saveNotificationPreferencesSuccess = exports.saveNotificationPreferencesFailure = exports.saveNotificationPreferences = exports.fetchNotificationPreferencesSuccess = exports.clearNotificationPreferencesLocalOverrides = exports.clearAllNotificationPreferencesView = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const notificationPreferencesViewPayload_1 = require("./notificationPreferencesViewPayload");
7
7
  const notificationPreferencesViewState_1 = require("./notificationPreferencesViewState");
@@ -22,8 +22,18 @@ const mergePreferences = (base, patch) => {
22
22
  ...channels,
23
23
  };
24
24
  });
25
+ const groupChannelEnabledByGroupId = {
26
+ ...base.groupChannelEnabledByGroupId,
27
+ };
28
+ Object.entries(patch.groupChannelEnabledByGroupId).forEach(([groupId, channels]) => {
29
+ groupChannelEnabledByGroupId[groupId] = {
30
+ ...(base.groupChannelEnabledByGroupId[groupId] ?? {}),
31
+ ...channels,
32
+ };
33
+ });
25
34
  return {
26
35
  eventEnabledByChannel,
36
+ groupChannelEnabledByGroupId,
27
37
  groupFrequencyByGroupId: {
28
38
  ...base.groupFrequencyByGroupId,
29
39
  ...patch.groupFrequencyByGroupId,
@@ -34,16 +44,16 @@ const mergePreferences = (base, patch) => {
34
44
  // to the server from `localOverrides` — AND whose current value still equals
35
45
  // what was saved. Any new edits the user added while the PUT was in flight
36
46
  // (either new keys not present in `saved`, OR same keys the user flipped back
37
- // to a different value) are preserved so the next debounce cycle picks them up.
47
+ // to a different value) are preserved so the next Save picks them up.
38
48
  //
39
49
  // Presence-only subtraction (the prior implementation) silently dropped a
40
- // same-key re-edit that landed AFTER debounce fired but BEFORE the response
50
+ // same-key re-edit that landed AFTER a Save fired but BEFORE the response
41
51
  // returned. Concrete case:
42
52
  // 1. toggle evt1.email = false → localOverrides = {evt1:{email:false}}
43
- // 2. debounce PUT #1 fires with savedOverrides = {evt1:{email:false}}
53
+ // 2. Save → PUT #1 fires with savedOverrides = {evt1:{email:false}}
44
54
  // 3. user re-toggles evt1.email = true → localOverrides = {evt1:{email:true}}
45
55
  // 4. PUT #1 success → subtract dropped `email` on presence, so the true
46
- // re-edit disappeared and next-debounce found nothing to send.
56
+ // re-edit disappeared and the next Save found nothing to send.
47
57
  const subtractSavedOverrides = (current, saved) => {
48
58
  const eventEnabledByChannel = {};
49
59
  Object.entries(current.eventEnabledByChannel).forEach(([eventId, channels]) => {
@@ -65,13 +75,38 @@ const subtractSavedOverrides = (current, saved) => {
65
75
  eventEnabledByChannel[eventId] = remainingChannels;
66
76
  }
67
77
  });
78
+ // Same value-equality subtraction as eventEnabledByChannel above — a
79
+ // group-channel master the user re-flipped while a PUT was in flight must
80
+ // survive so the next Save re-sends it.
81
+ const groupChannelEnabledByGroupId = {};
82
+ Object.entries(current.groupChannelEnabledByGroupId).forEach(([groupId, channels]) => {
83
+ const savedChannels = saved.groupChannelEnabledByGroupId[groupId];
84
+ if (savedChannels == null) {
85
+ groupChannelEnabledByGroupId[groupId] = channels;
86
+ return;
87
+ }
88
+ const remainingChannels = {};
89
+ Object.keys(channels).forEach((channel) => {
90
+ if (!(channel in savedChannels) ||
91
+ channels[channel] !== savedChannels[channel]) {
92
+ remainingChannels[channel] = channels[channel];
93
+ }
94
+ });
95
+ if (Object.keys(remainingChannels).length > 0) {
96
+ groupChannelEnabledByGroupId[groupId] = remainingChannels;
97
+ }
98
+ });
68
99
  const groupFrequencyByGroupId = {};
69
100
  Object.entries(current.groupFrequencyByGroupId).forEach(([groupId, frequency]) => {
70
101
  if (saved.groupFrequencyByGroupId[groupId] !== frequency) {
71
102
  groupFrequencyByGroupId[groupId] = frequency;
72
103
  }
73
104
  });
74
- return { eventEnabledByChannel, groupFrequencyByGroupId };
105
+ return {
106
+ eventEnabledByChannel,
107
+ groupChannelEnabledByGroupId,
108
+ groupFrequencyByGroupId,
109
+ };
75
110
  };
76
111
  const notificationPreferencesView = (0, toolkit_1.createSlice)({
77
112
  name: 'notificationPreferencesView',
@@ -127,8 +162,8 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
127
162
  // snapshot of the server-known preferences (empty maps included), so
128
163
  // REPLACE `preferences` rather than merging — merging would leak keys
129
164
  // the server has since removed. DO NOT touch `localOverrides`: a refetch
130
- // that fires while the user has in-flight edits (pre-debounce) must not
131
- // silently drop them; the save-success path is the only clearer.
165
+ // that fires while the user has unsaved edits must not silently drop
166
+ // them; the save-success path is the only clearer.
132
167
  // A `null`/`undefined` payload isn't a snapshot at all (e.g. the
133
168
  // envelope key was omitted); no-op instead of wiping preferences.
134
169
  if (action.payload.preferences == null) {
@@ -181,8 +216,8 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
181
216
  // In-flight-edit invariant: `savedOverrides` is the exact snapshot the
182
217
  // epic PUT to the server. Subtracting those keys from `localOverrides`
183
218
  // (instead of clearing it wholesale) preserves any edits the user
184
- // added AFTER the debounce fired but BEFORE the response landed. The
185
- // next debounce cycle will pick them up. Clearing wholesale drops
219
+ // added AFTER the Save fired but BEFORE the response landed. The
220
+ // next Save will pick them up. Clearing wholesale drops
186
221
  // in-flight edits from both UI (once server echo lands) and server.
187
222
  // Cancel/in-flight-save race: `cancelEpoch` monotonically increases
188
223
  // on every Cancel. If the save captured a lower epoch at dispatch,
@@ -208,6 +243,16 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
208
243
  draft.preferences = mergePreferences(draft.preferences, patch);
209
244
  draft.localOverrides = subtractSavedOverrides(draft.localOverrides, action.payload.savedOverrides);
210
245
  },
246
+ setGroupChannelMaster(draft, action) {
247
+ // Mirror of `toggleEventChannel` but scoped to the group-channel master
248
+ // held under `groupChannelEnabledByGroupId[groupId][channel]`.
249
+ const { channel, enabled, groupId } = action.payload;
250
+ const groupMap = {
251
+ ...(draft.localOverrides.groupChannelEnabledByGroupId[groupId] ?? {}),
252
+ };
253
+ groupMap[channel] = enabled;
254
+ draft.localOverrides.groupChannelEnabledByGroupId[groupId] = groupMap;
255
+ },
211
256
  setGroupFrequency(draft, action) {
212
257
  draft.localOverrides.groupFrequencyByGroupId[action.payload.groupId] =
213
258
  action.payload.frequency;
@@ -222,5 +267,5 @@ const notificationPreferencesView = (0, toolkit_1.createSlice)({
222
267
  },
223
268
  },
224
269
  });
225
- _a = notificationPreferencesView.actions, exports.clearAllNotificationPreferencesView = _a.clearAllNotificationPreferencesView, exports.clearNotificationPreferencesLocalOverrides = _a.clearNotificationPreferencesLocalOverrides, exports.fetchNotificationPreferencesSuccess = _a.fetchNotificationPreferencesSuccess, exports.saveNotificationPreferences = _a.saveNotificationPreferences, exports.saveNotificationPreferencesFailure = _a.saveNotificationPreferencesFailure, exports.saveNotificationPreferencesSuccess = _a.saveNotificationPreferencesSuccess, exports.setGroupFrequency = _a.setGroupFrequency, exports.toggleEventChannel = _a.toggleEventChannel;
270
+ _a = notificationPreferencesView.actions, exports.clearAllNotificationPreferencesView = _a.clearAllNotificationPreferencesView, exports.clearNotificationPreferencesLocalOverrides = _a.clearNotificationPreferencesLocalOverrides, exports.fetchNotificationPreferencesSuccess = _a.fetchNotificationPreferencesSuccess, exports.saveNotificationPreferences = _a.saveNotificationPreferences, exports.saveNotificationPreferencesFailure = _a.saveNotificationPreferencesFailure, exports.saveNotificationPreferencesSuccess = _a.saveNotificationPreferencesSuccess, exports.setGroupChannelMaster = _a.setGroupChannelMaster, exports.setGroupFrequency = _a.setGroupFrequency, exports.toggleEventChannel = _a.toggleEventChannel;
226
271
  exports.default = notificationPreferencesView.reducer;