@zeniai/client-epic-state 5.2.93 → 5.2.94-beta0MM

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 (86) hide show
  1. package/lib/commonStateTypes/timeframeTick.d.ts +1 -0
  2. package/lib/commonStateTypes/timeframeTick.js +1 -0
  3. package/lib/commonStateTypes/viewAndReport/reportUIState.d.ts +2 -0
  4. package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -0
  5. package/lib/esm/commonStateTypes/timeframeTick.js +1 -0
  6. package/lib/esm/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
  7. package/lib/esm/view/balanceSheet/balanceSheetReducer.js +8 -2
  8. package/lib/esm/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
  9. package/lib/esm/view/cashFlow/cashFlowReducer.js +2 -2
  10. package/lib/esm/view/cashInCashOut/cashInCashOutEpic.js +22 -14
  11. package/lib/esm/view/cashInCashOut/cashInCashOutReducer.js +161 -41
  12. package/lib/esm/view/cashInCashOut/cashInCashOutSelector.js +42 -15
  13. package/lib/esm/view/cashPosition/cashPositionEpic.js +21 -13
  14. package/lib/esm/view/cashPosition/cashPositionReducer.js +155 -44
  15. package/lib/esm/view/cashPosition/cashPositionSelector.js +28 -9
  16. package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +5 -0
  17. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  18. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +25 -10
  19. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  20. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  21. package/lib/esm/view/opEx/opExEpic.js +7 -7
  22. package/lib/esm/view/opEx/opExForTimeFrameEpic.js +21 -6
  23. package/lib/esm/view/opEx/opExReducer.js +169 -32
  24. package/lib/esm/view/opEx/opExSelector.js +28 -10
  25. package/lib/esm/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  26. package/lib/esm/view/profitAndLoss/profitAndLossReducer.js +9 -2
  27. package/lib/esm/view/revenue/revenueEpic.js +7 -7
  28. package/lib/esm/view/revenue/revenueForTimeframeEpic.js +23 -8
  29. package/lib/esm/view/revenue/revenueReducer.js +183 -40
  30. package/lib/esm/view/revenue/revenueSelector.js +30 -10
  31. package/lib/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
  32. package/lib/view/balanceSheet/balanceSheetReducer.d.ts +3 -1
  33. package/lib/view/balanceSheet/balanceSheetReducer.js +8 -2
  34. package/lib/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
  35. package/lib/view/cashFlow/cashFlowReducer.d.ts +3 -1
  36. package/lib/view/cashFlow/cashFlowReducer.js +2 -2
  37. package/lib/view/cashInCashOut/cashInCashOutEpic.d.ts +8 -2
  38. package/lib/view/cashInCashOut/cashInCashOutEpic.js +22 -14
  39. package/lib/view/cashInCashOut/cashInCashOutReducer.d.ts +13 -1
  40. package/lib/view/cashInCashOut/cashInCashOutReducer.js +161 -41
  41. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +2 -1
  42. package/lib/view/cashInCashOut/cashInCashOutSelector.js +42 -15
  43. package/lib/view/cashInCashOut/cashInCashOutState.d.ts +5 -1
  44. package/lib/view/cashPosition/cashPositionEpic.d.ts +8 -2
  45. package/lib/view/cashPosition/cashPositionEpic.js +21 -13
  46. package/lib/view/cashPosition/cashPositionReducer.d.ts +13 -2
  47. package/lib/view/cashPosition/cashPositionReducer.js +155 -44
  48. package/lib/view/cashPosition/cashPositionSelector.d.ts +2 -1
  49. package/lib/view/cashPosition/cashPositionSelector.js +28 -9
  50. package/lib/view/cashPosition/cashPositionState.d.ts +5 -1
  51. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +2 -0
  52. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +5 -0
  53. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +3 -0
  54. package/lib/view/financeStatement/financeStatementEpic.d.ts +6 -0
  55. package/lib/view/netBurnOrIncome/fetchNetBurnOrIncomeWithForecastEpic.d.ts +1 -0
  56. package/lib/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  57. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.d.ts +2 -1
  58. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +26 -11
  59. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.d.ts +12 -3
  60. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  61. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.d.ts +1 -1
  62. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  63. package/lib/view/netBurnOrIncome/netBurnOrIncomeState.d.ts +8 -0
  64. package/lib/view/opEx/fetchOpExWithForecastEpic.d.ts +1 -0
  65. package/lib/view/opEx/opExEpic.js +7 -7
  66. package/lib/view/opEx/opExForTimeFrameEpic.d.ts +2 -1
  67. package/lib/view/opEx/opExForTimeFrameEpic.js +22 -7
  68. package/lib/view/opEx/opExReducer.d.ts +12 -3
  69. package/lib/view/opEx/opExReducer.js +168 -31
  70. package/lib/view/opEx/opExSelector.d.ts +1 -1
  71. package/lib/view/opEx/opExSelector.js +28 -10
  72. package/lib/view/opEx/opExState.d.ts +6 -0
  73. package/lib/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  74. package/lib/view/profitAndLoss/profitAndLossReducer.d.ts +3 -1
  75. package/lib/view/profitAndLoss/profitAndLossReducer.js +9 -2
  76. package/lib/view/reportUIOptions/updateReportUIOptionCOABalancesRangeEpic.d.ts +15 -0
  77. package/lib/view/revenue/fetchRevenueWithForecastEpic.d.ts +1 -0
  78. package/lib/view/revenue/revenueEpic.js +7 -7
  79. package/lib/view/revenue/revenueForTimeframeEpic.d.ts +2 -1
  80. package/lib/view/revenue/revenueForTimeframeEpic.js +24 -9
  81. package/lib/view/revenue/revenueReducer.d.ts +12 -3
  82. package/lib/view/revenue/revenueReducer.js +182 -39
  83. package/lib/view/revenue/revenueSelector.d.ts +1 -1
  84. package/lib/view/revenue/revenueSelector.js +30 -10
  85. package/lib/view/revenue/revenueState.d.ts +8 -0
  86. package/package.json +1 -1
@@ -91,8 +91,16 @@ const netBurnOrIncome = createSlice({
91
91
  },
92
92
  },
93
93
  fetchNetBurnOrIncome: {
94
- prepare(fetchFullReport = false, forecastId, cacheOverride = false) {
95
- return { payload: { fetchFullReport, forecastId, cacheOverride } };
94
+ prepare(fetchFullReport = false, forecastId, cacheOverride = false, startDate, endDate) {
95
+ return {
96
+ payload: {
97
+ fetchFullReport,
98
+ forecastId,
99
+ cacheOverride,
100
+ startDate,
101
+ endDate,
102
+ },
103
+ };
96
104
  },
97
105
  reducer(
98
106
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -103,50 +111,138 @@ const netBurnOrIncome = createSlice({
103
111
  },
104
112
  },
105
113
  fetchNetBurnOrIncomeForTimeframe: {
106
- prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false) {
114
+ prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
107
115
  return {
108
- payload: { forecastId, timeframe, fetchFullReport, cacheOverride },
116
+ payload: {
117
+ forecastId,
118
+ timeframe,
119
+ fetchFullReport,
120
+ cacheOverride,
121
+ startDate,
122
+ endDate,
123
+ },
109
124
  };
110
125
  },
111
126
  reducer(draft, action) {
112
127
  const timeframe = action.payload.timeframe;
113
128
  const netBurnOrIncomeState = getState(draft, draft.uiState.isForecastModeOn, action.payload.forecastId);
114
- const fetchState = {
115
- ...netBurnOrIncomeState.fetchState,
116
- };
117
- fetchState[timeframe] = {
118
- fetchState: 'In-Progress',
119
- error: undefined,
120
- };
121
- updateState(draft, action.payload.forecastId, { fetchState });
129
+ // Route by the fetch's own dates, not the global flag: a custom fetch
130
+ // (carries start/end) marks customFetchState so the shared fetchState the
131
+ // dashboard reads is never left In-Progress by a detail-screen fetch.
132
+ const isCustomDateRange = action.payload.startDate != null && action.payload.endDate != null;
133
+ if (isCustomDateRange) {
134
+ const customFetchState = {
135
+ ...(netBurnOrIncomeState.customFetchState ?? {
136
+ month: { fetchState: 'Not-Started', error: undefined },
137
+ quarter: { fetchState: 'Not-Started', error: undefined },
138
+ year: { fetchState: 'Not-Started', error: undefined },
139
+ }),
140
+ };
141
+ customFetchState[timeframe] = {
142
+ fetchState: 'In-Progress',
143
+ error: undefined,
144
+ };
145
+ updateState(draft, action.payload.forecastId, { customFetchState });
146
+ }
147
+ else {
148
+ const fetchState = {
149
+ ...netBurnOrIncomeState.fetchState,
150
+ };
151
+ fetchState[timeframe] = {
152
+ fetchState: 'In-Progress',
153
+ error: undefined,
154
+ };
155
+ updateState(draft, action.payload.forecastId, { fetchState });
156
+ }
122
157
  },
123
158
  },
124
159
  updateNetBurnOrIncomeForTimeframe(draft, action) {
125
- doUpdateNetBurnOrIncome(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report);
160
+ doUpdateNetBurnOrIncome(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report, action.payload.isCustomDateRange === true);
126
161
  },
127
162
  updateNetBurnOrIncomeForTimeframeFailure: {
128
- prepare(error, forecastId, timeframe) {
163
+ prepare(error, forecastId, timeframe, isCustomDateRange = false) {
129
164
  return {
130
165
  payload: {
131
166
  error,
132
167
  forecastId,
133
168
  timeframe,
169
+ isCustomDateRange,
134
170
  },
135
171
  };
136
172
  },
137
173
  reducer(draft, action) {
138
174
  const netBurnOrIncome = getState(draft, draft.uiState.isForecastModeOn, action.payload.forecastId);
139
- const fetchState = {
140
- ...netBurnOrIncome.fetchState,
141
- };
142
- fetchState[action.payload.timeframe] = {
143
- fetchState: 'Error',
144
- error: action.payload.error,
145
- };
146
- updateState(draft, action.payload.forecastId, { fetchState });
175
+ // Route the error like the request/success: a custom (dated) fetch's
176
+ // failure lands in customFetchState so the shared default the dashboard
177
+ // reads isn't marked Error, and the detail's customFetchState isn't left
178
+ // stuck In-Progress.
179
+ if (action.payload.isCustomDateRange === true) {
180
+ const customFetchState = netBurnOrIncome.customFetchState ?? {
181
+ month: { fetchState: 'Not-Started', error: undefined },
182
+ quarter: { fetchState: 'Not-Started', error: undefined },
183
+ year: { fetchState: 'Not-Started', error: undefined },
184
+ };
185
+ customFetchState[action.payload.timeframe] = {
186
+ fetchState: 'Error',
187
+ error: action.payload.error,
188
+ };
189
+ updateState(draft, action.payload.forecastId, { customFetchState });
190
+ }
191
+ else {
192
+ const fetchState = {
193
+ ...netBurnOrIncome.fetchState,
194
+ };
195
+ fetchState[action.payload.timeframe] = {
196
+ fetchState: 'Error',
197
+ error: action.payload.error,
198
+ };
199
+ updateState(draft, action.payload.forecastId, { fetchState });
200
+ }
147
201
  },
148
202
  },
149
203
  updateNetBurnOrIncomeCOABalancesRange(draft, action) {
204
+ if (action.payload.isCustomDateRange != null) {
205
+ if (action.payload.isCustomDateRange === true) {
206
+ // Convert the string dates the action carries to ZeniDate values and
207
+ // store as a single customTimeframe. Presence signals custom-mode active.
208
+ draft.uiState.customTimeframe =
209
+ action.payload.customStartDate != null &&
210
+ action.payload.customEndDate != null
211
+ ? {
212
+ startDate: date(action.payload.customStartDate),
213
+ endDate: date(action.payload.customEndDate),
214
+ }
215
+ : undefined;
216
+ // Invalidate the shared default-window fetchState so dashboardEpic
217
+ // refetches the default data on the next dashboard visit — the custom
218
+ // fetch writes into the shared section store, and this lets the card
219
+ // self-heal without a reload. The detail screen reads customFetchState.
220
+ draft.netBurnOrIncome.fetchState = {
221
+ month: { fetchState: 'Not-Started', error: undefined },
222
+ quarter: { fetchState: 'Not-Started', error: undefined },
223
+ year: { fetchState: 'Not-Started', error: undefined },
224
+ };
225
+ }
226
+ else {
227
+ draft.uiState.customTimeframe = undefined;
228
+ }
229
+ }
230
+ // A custom date range drives the visible window directly: honor the exact
231
+ // numberOfPeriods + thisPeriod from the payload instead of re-deriving
232
+ // (which clamps the range to the available/non-zero periods).
233
+ if (draft.uiState.customTimeframe != null) {
234
+ draft.uiState.selectedCOABalancesRange =
235
+ action.payload.selectedCOABalancesRange;
236
+ if (action.payload.highlightedCOABalancesRange != null) {
237
+ // Clamp the highlight to the same cap the non-custom path enforces so
238
+ // a wide custom range doesn't highlight all N periods at once.
239
+ draft.uiState.highlightedCOABalancesRange = {
240
+ ...action.payload.highlightedCOABalancesRange,
241
+ numberOfPeriods: Math.min(action.payload.highlightedCOABalancesRange.numberOfPeriods, draft.uiState.maxNumOfPeriodsToHighlight),
242
+ };
243
+ }
244
+ return;
245
+ }
150
246
  // When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
151
247
  const netBurnOrIncomeReport = getState(draft, undefined, undefined);
152
248
  const { income, cogs, expenses, dataAvailable } = netBurnOrIncomeReport;
@@ -190,6 +286,13 @@ const netBurnOrIncome = createSlice({
190
286
  }
191
287
  },
192
288
  updateNetBurnOrIncomeThisPeriod(draft, action) {
289
+ // In custom-date mode the picker drives the window; skip re-deriving the
290
+ // selected/highlighted ranges from the default dataAvailable (the epic
291
+ // re-dispatches the default thisPeriod on every month response, which
292
+ // would otherwise clamp the custom window back to the default).
293
+ if (draft.uiState.customTimeframe != null) {
294
+ return;
295
+ }
193
296
  // When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
194
297
  const netBurnOrIncomeReport = getState(draft, undefined, undefined);
195
298
  const { income, cogs, expenses, dataAvailable } = netBurnOrIncomeReport;
@@ -241,7 +344,9 @@ const netBurnOrIncome = createSlice({
241
344
  ? true
242
345
  : false;
243
346
  // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
244
- if (dataAvailable != null && draft.uiState.isForecastModeOn === false) {
347
+ if (dataAvailable != null &&
348
+ draft.uiState.isForecastModeOn === false &&
349
+ draft.uiState.customTimeframe == null) {
245
350
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(netBurnOrIncomeReport.firstMonthOfFY, timeframe, [expenses, income, cogs], dataAvailable);
246
351
  const thisPeriod = extractThisPeriod(netBurnOrIncomeReport.firstMonthOfFY, timeframe, selectedCOABalancesRange, coaBalances);
247
352
  if (thisPeriod != null &&
@@ -284,7 +389,7 @@ const netBurnOrIncome = createSlice({
284
389
  draft.uiState.maxNumOfPeriodsToHighlight =
285
390
  initialState.uiState.maxNumOfPeriodsToHighlight;
286
391
  }
287
- if (dataAvailable != null) {
392
+ if (dataAvailable != null && draft.uiState.customTimeframe == null) {
288
393
  const timeframe = draft.uiState.timeframe;
289
394
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(netBurnOrIncomeReport.firstMonthOfFY, timeframe, [expenses, income, cogs], dataAvailable);
290
395
  const selectionRanges = getInitialSelectedAndHighlightedRanges({
@@ -339,34 +444,73 @@ const netBurnOrIncome = createSlice({
339
444
  });
340
445
  export const { fetchNetBurnOrIncome, fetchNetBurnOrIncomeForTimeframe, updateNetBurnOrIncomeForTimeframe, updateNetBurnOrIncomeForTimeframeFailure, updateNetBurnOrIncomeThisPeriod, updateNetBurnOrIncomeUIState, updateNetBurnOrIncomeAdditionalBalancesSelection, updateNetBurnOrIncomeCOABalancesRange, netBurnOrIncomeToggleForecastMode, updateNetBurnOrIncomeTimeFrame, updateNetBurnOrIncomeDownloadState, fetchNetBurnOrIncomeWithForecast, waitForForecastListThenFetchNetBurnOrIncomeWithForecast, } = netBurnOrIncome.actions;
341
446
  export default netBurnOrIncome.reducer;
342
- const doUpdateNetBurnOrIncome = (draft, forecastId, timeframe, data) => {
447
+ const doUpdateNetBurnOrIncome = (draft, forecastId, timeframe, data, isCustomDateRange) => {
343
448
  const netBurnOrIncome = getState(draft, draft.uiState.isForecastModeOn, forecastId);
344
449
  const report = mapReportPayloadV2ToReportV2(data.report);
345
450
  const { expenses, income, cogs, net_burn_or_income, insights, currency, runway, } = data.report;
346
451
  const expensesCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(expenses, currency, timeframe);
347
452
  const incomeCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(income, currency, timeframe);
348
453
  const cogsCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(cogs, currency, timeframe);
349
- netBurnOrIncome.expenses = Object.assign({}, netBurnOrIncome.expenses, expensesCOABalance);
350
- netBurnOrIncome.income = Object.assign({}, netBurnOrIncome.income, incomeCOABalance);
351
- netBurnOrIncome.cogs = Object.assign({}, netBurnOrIncome.cogs, cogsCOABalance);
352
- if (timeframe === 'month' && insights != null) {
454
+ // Route custom-date results (identified by the fetch's own dates, not the
455
+ // global flag) to the separate slots so the dashboard card (which reads
456
+ // income/cogs/expenses) keeps the default window.
457
+ if (isCustomDateRange) {
458
+ netBurnOrIncome.customExpenses = Object.assign({}, netBurnOrIncome.customExpenses, expensesCOABalance);
459
+ netBurnOrIncome.customIncome = Object.assign({}, netBurnOrIncome.customIncome, incomeCOABalance);
460
+ netBurnOrIncome.customCogs = Object.assign({}, netBurnOrIncome.customCogs, cogsCOABalance);
461
+ }
462
+ else {
463
+ netBurnOrIncome.expenses = Object.assign({}, netBurnOrIncome.expenses, expensesCOABalance);
464
+ netBurnOrIncome.income = Object.assign({}, netBurnOrIncome.income, incomeCOABalance);
465
+ netBurnOrIncome.cogs = Object.assign({}, netBurnOrIncome.cogs, cogsCOABalance);
466
+ }
467
+ // Insights and runway/averages are shared (read by the dashboard card); a
468
+ // custom-date slice must not overwrite them with its partial window.
469
+ if (timeframe === 'month' && insights != null && !isCustomDateRange) {
353
470
  // we will get insights only in the month timeframe api
354
471
  netBurnOrIncome.insights = mapInsightsPayloadtoInsightsState(insights);
355
472
  }
356
- netBurnOrIncome.runwayAndAverages = mapRunwayPayloadtoRunwayState(runway, net_burn_or_income.averages?.months, data.report.currency);
357
- const expensesOrIncome = netBurnOrIncome.expenses != null
358
- ? netBurnOrIncome.expenses
359
- : netBurnOrIncome.income;
360
- netBurnOrIncome.dataAvailable = report.dataAvailable
361
- ? report.dataAvailable
362
- : getDataAvailableDateRange(expensesOrIncome);
473
+ if (!isCustomDateRange) {
474
+ netBurnOrIncome.runwayAndAverages = mapRunwayPayloadtoRunwayState(runway, net_burn_or_income.averages?.months, data.report.currency);
475
+ }
476
+ // Derive dataAvailable from the slot this fetch actually populated.
477
+ const expensesOrIncome = isCustomDateRange
478
+ ? netBurnOrIncome.customExpenses != null
479
+ ? netBurnOrIncome.customExpenses
480
+ : netBurnOrIncome.customIncome
481
+ : netBurnOrIncome.expenses != null
482
+ ? netBurnOrIncome.expenses
483
+ : netBurnOrIncome.income;
484
+ // Don't let a custom slice shrink the shared dataAvailable the dashboard uses
485
+ // for its period count; a default fetch always updates it, a custom one only
486
+ // populates it on first load (when nothing has set it yet).
487
+ if (!isCustomDateRange || netBurnOrIncome.dataAvailable == null) {
488
+ netBurnOrIncome.dataAvailable = report.dataAvailable
489
+ ? report.dataAvailable
490
+ : getDataAvailableDateRange(expensesOrIncome ?? {});
491
+ }
363
492
  netBurnOrIncome.firstMonthOfFY = report.firstMonthOfFY;
364
493
  netBurnOrIncome.bookCloseDate = report.bookCloseDate;
365
- netBurnOrIncome.fetchState[timeframe].fetchState = 'Completed';
366
- netBurnOrIncome.fetchState[timeframe].error = undefined;
494
+ // Mark the matching fetchState Completed. A custom fetch uses customFetchState so
495
+ // dashboardEpic's Completed check on the default fetchState is untouched.
496
+ if (isCustomDateRange) {
497
+ const customFetchState = netBurnOrIncome.customFetchState ?? {
498
+ month: { fetchState: 'Not-Started', error: undefined },
499
+ quarter: { fetchState: 'Not-Started', error: undefined },
500
+ year: { fetchState: 'Not-Started', error: undefined },
501
+ };
502
+ customFetchState[timeframe] = { fetchState: 'Completed', error: undefined };
503
+ netBurnOrIncome.customFetchState = customFetchState;
504
+ }
505
+ else {
506
+ netBurnOrIncome.fetchState[timeframe].fetchState = 'Completed';
507
+ netBurnOrIncome.fetchState[timeframe].error = undefined;
508
+ }
367
509
  if (netBurnOrIncome.dataAvailable != null &&
368
- forecastId == null // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
369
- ) {
510
+ forecastId == null && // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
511
+ // A custom date range drives the window; don't reset it to the default on
512
+ // every report receipt (this is what clamped wide ranges back to 8).
513
+ draft.uiState.customTimeframe == null) {
370
514
  const timeframe = draft.uiState.timeframe;
371
515
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(netBurnOrIncome.firstMonthOfFY, timeframe, [netBurnOrIncome.expenses, netBurnOrIncome.income, netBurnOrIncome.cogs], netBurnOrIncome.dataAvailable);
372
516
  const selectionRanges = getInitialSelectedAndHighlightedRanges({
@@ -60,18 +60,28 @@ export const getNetBurnOrIncomeUIState = createSelector((state) => state.netBurn
60
60
  scrollPosition,
61
61
  };
62
62
  });
63
- export const getNetBurnOrIncomeReport = (accountState, accountGroupState, sectionsState, netBurnOrIncomeState, forecastState, forecastListState, filter, forecastId) => {
63
+ export const getNetBurnOrIncomeReport = (accountState, accountGroupState, sectionsState, netBurnOrIncomeState, forecastState, forecastListState, filter, forecastId, readCustomRangeSlot) => {
64
64
  const netBurnOrIncome = getState(netBurnOrIncomeState, netBurnOrIncomeState.uiState.isForecastModeOn, forecastId);
65
+ // Custom detail reads the isolated custom section-accounts slot; dashboard
66
+ // (no flag) reads the default slot, so a default fetch can't clobber it.
67
+ const useCustomRange = readCustomRangeSlot === true &&
68
+ netBurnOrIncomeState.uiState.customTimeframe != null;
69
+ // The detail screen opts into customFetchState so its Completed check fires
70
+ // off the custom fetch; the dashboard (no flag) reads the shared fetchState,
71
+ // which custom-mode activation reset to Not-Started so dashboardEpic refetches.
72
+ const activeFetchState = useCustomRange && netBurnOrIncome.customFetchState != null
73
+ ? netBurnOrIncome.customFetchState
74
+ : netBurnOrIncome.fetchState;
65
75
  const fetchState = reduceAnyFetchState([
66
- netBurnOrIncome.fetchState.month,
67
- netBurnOrIncome.fetchState.quarter,
68
- netBurnOrIncome.fetchState.year,
76
+ activeFetchState.month,
77
+ activeFetchState.quarter,
78
+ activeFetchState.year,
69
79
  ]);
70
80
  let allTimeframeTicks = [];
71
81
  const dataAvailable = netBurnOrIncomeState.netBurnOrIncome.dataAvailable;
72
82
  if (dataAvailable != null) {
73
83
  const noFilter = newBalancesFilterForDateRange(netBurnOrIncomeState.netBurnOrIncome.firstMonthOfFY, netBurnOrIncomeState.uiState.timeframe, dataAvailable, 'ascending_date', []);
74
- const reportId = reportIdUniqueKey();
84
+ const reportId = reportIdUniqueKey(undefined, useCustomRange);
75
85
  const incomeBalance = getSectionBalances(sectionsState, {
76
86
  reportId,
77
87
  sectionId: 'income',
@@ -88,7 +98,7 @@ export const getNetBurnOrIncomeReport = (accountState, accountGroupState, sectio
88
98
  : getCOABalances(incomeBalance, noFilter);
89
99
  allTimeframeTicks = incomeOrExpensesBalances.balances.map((balance) => toTimeframeTick(balance));
90
100
  }
91
- const reportId = reportIdUniqueKey(forecastId);
101
+ const reportId = reportIdUniqueKey(forecastId, useCustomRange);
92
102
  const sections = {};
93
103
  const calculatedSections = {};
94
104
  if (fetchState.fetchState === 'Completed') {
@@ -135,6 +145,8 @@ export const getNetBurnOrIncomeReport = (accountState, accountGroupState, sectio
135
145
  allTimeframeTicks,
136
146
  forecastList,
137
147
  runwayAndAverages: netBurnOrIncome.runwayAndAverages,
148
+ // Surface the saved custom timeframe so the detail picker can restore them.
149
+ customTimeframe: netBurnOrIncomeState.uiState.customTimeframe,
138
150
  };
139
151
  };
140
152
  export const getNetBurnOrIncomeForSelectedRange = createSelector((state) => state.accountState, (state) => state.accountGroupState, (state) => state.sectionAccountsViewState, (state) => state.netBurnOrIncomeState, (state) => state.forecastState, (state) => state.forecastListState, (accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState) => {
@@ -144,7 +156,10 @@ export const getNetBurnOrIncomeForSelectedRange = createSelector((state) => stat
144
156
  selectedForecast?.id != null &&
145
157
  getNetBurnOrIncomeReportFetchState(selectedForecast.id, netBurnOrIncomeWithForecastState).fetchState === 'Completed'
146
158
  ? getNetBurnOrIncomeWithForecastOnDataAvailableDateRange(noFilter, selectedForecast?.id, accountState, accountGroupState, sectionAccountsViewState, forecastListState, netBurnOrIncomeWithForecastState, forecastState)
147
- : getNetBurnOrIncomeReport(accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState, noFilter, selectedForecast?.id);
159
+ : getNetBurnOrIncomeReport(accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState, noFilter, selectedForecast?.id,
160
+ // Detail selectors read the custom slot when the detail is in custom
161
+ // mode; the dashboard's last-periods selector does not.
162
+ netBurnOrIncomeWithForecastState.uiState.customTimeframe != null);
148
163
  });
149
164
  export const getNetBurnOrIncomeForHighlightedRange = createSelector((state) => state.accountState, (state) => state.accountGroupState, (state) => state.sectionAccountsViewState, (state) => state.netBurnOrIncomeState, (state) => state.forecastState, (state) => state.forecastListState, (accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState) => {
150
165
  const { timeframe, highlightedCOABalancesRange, selectedAdditionalBalances, maxNumOfPeriodsToHighlight, selectedForecast, isForecastModeOn, } = netBurnOrIncomeWithForecastState.uiState;
@@ -154,7 +169,10 @@ export const getNetBurnOrIncomeForHighlightedRange = createSelector((state) => s
154
169
  selectedForecast?.id != null &&
155
170
  getNetBurnOrIncomeReportFetchState(selectedForecast.id, netBurnOrIncomeWithForecastState).fetchState === 'Completed'
156
171
  ? getNetBurnOrIncomeWithForecastOnDataAvailableDateRange(filter, selectedForecast?.id, accountState, accountGroupState, sectionAccountsViewState, forecastListState, netBurnOrIncomeWithForecastState, forecastState)
157
- : getNetBurnOrIncomeReport(accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState, filter, selectedForecast?.id);
172
+ : getNetBurnOrIncomeReport(accountState, accountGroupState, sectionAccountsViewState, netBurnOrIncomeWithForecastState, forecastState, forecastListState, filter, selectedForecast?.id,
173
+ // Detail selectors read the custom slot when the detail is in custom
174
+ // mode; the dashboard's last-periods selector does not.
175
+ netBurnOrIncomeWithForecastState.uiState.customTimeframe != null);
158
176
  });
159
177
  export const getNetBurnOrIncomeReportFetchState = (forecastId, netBurnOrIncomeState) => {
160
178
  const netBurnOrIncome = getState(netBurnOrIncomeState, forecastId != null, forecastId);
@@ -2,12 +2,12 @@ import { from } from 'rxjs';
2
2
  import { filter, switchMap } from 'rxjs/operators';
3
3
  import { fetchOpEx, fetchOpExReportForTimeframe, getState } from './opExReducer';
4
4
  export const fetchOpExEpic = (actions$, state) => actions$.pipe(filter(fetchOpEx.match), switchMap((action) => {
5
- const { cacheOverride, forecastId } = action.payload;
5
+ const { cacheOverride, endDate, forecastId, startDate } = action.payload;
6
6
  if (cacheOverride === true) {
7
7
  return from([
8
- fetchOpExReportForTimeframe('month', forecastId),
9
- fetchOpExReportForTimeframe('quarter', forecastId),
10
- fetchOpExReportForTimeframe('year', forecastId),
8
+ fetchOpExReportForTimeframe('month', forecastId, true, false, startDate, endDate),
9
+ fetchOpExReportForTimeframe('quarter', forecastId, true, false, startDate, endDate),
10
+ fetchOpExReportForTimeframe('year', forecastId, true, false, startDate, endDate),
11
11
  ]);
12
12
  }
13
13
  else {
@@ -16,15 +16,15 @@ export const fetchOpExEpic = (actions$, state) => actions$.pipe(filter(fetchOpEx
16
16
  const timeframeFetchState = opExState.fetchState;
17
17
  if (timeframeFetchState['month'].fetchState !== 'In-Progress' &&
18
18
  timeframeFetchState['month'].fetchState !== 'Completed') {
19
- fetchActions.push(fetchOpExReportForTimeframe('month', forecastId));
19
+ fetchActions.push(fetchOpExReportForTimeframe('month', forecastId, true, false, startDate, endDate));
20
20
  }
21
21
  if (timeframeFetchState['quarter'].fetchState !== 'In-Progress' &&
22
22
  timeframeFetchState['quarter'].fetchState !== 'Completed') {
23
- fetchActions.push(fetchOpExReportForTimeframe('quarter', forecastId));
23
+ fetchActions.push(fetchOpExReportForTimeframe('quarter', forecastId, true, false, startDate, endDate));
24
24
  }
25
25
  if (timeframeFetchState['year'].fetchState !== 'In-Progress' &&
26
26
  timeframeFetchState['year'].fetchState !== 'Completed') {
27
- fetchActions.push(fetchOpExReportForTimeframe('year', forecastId));
27
+ fetchActions.push(fetchOpExReportForTimeframe('year', forecastId, true, false, startDate, endDate));
28
28
  }
29
29
  return from(fetchActions);
30
30
  }
@@ -5,9 +5,15 @@ import { updateSectionAccountsView } from '../../entity/sectionAccountsView/sect
5
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
6
6
  import { getTimeSeriesGroup } from '../common/reportsHelpers';
7
7
  import { fetchOpExReportForTimeframe, updateOpExFailureForTimeframe, updateOpExForTimeframe, updateOpExThisPeriod, } from './opExReducer';
8
- export const reportIdUniqueKey = (forecastId) => forecastId != null
9
- ? `operating_expenses+${forecastId}`
10
- : 'operating_expenses';
8
+ // Reserved forecast-slot marker so custom-date section-accounts data lives under
9
+ // its own key (operating_expenses+<marker>) and a default fetch can never
10
+ // overwrite it. Real forecast ids are UUIDs, so this never collides.
11
+ export const CUSTOM_DATE_RANGE_SECTION_MARKER = '__custom_date_range__';
12
+ export const reportIdUniqueKey = (forecastId, isCustomDateRange) => isCustomDateRange === true
13
+ ? `operating_expenses+${CUSTOM_DATE_RANGE_SECTION_MARKER}`
14
+ : forecastId != null
15
+ ? `operating_expenses+${forecastId}`
16
+ : 'operating_expenses';
11
17
  export const fetchOpExReportForTimeframeEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchOpExReportForTimeframe.match), mergeMap((action) => {
12
18
  const queryValue = {
13
19
  is_full_response: action.payload.fetchFullReport,
@@ -18,16 +24,23 @@ export const fetchOpExReportForTimeframeEpic = (actions$, state, zeniAPI) => act
18
24
  queryValue.forecast_id = action.payload.forecastId;
19
25
  queryValue.is_forecast_vs_actual = true;
20
26
  }
27
+ if (action.payload.startDate != null && action.payload.endDate != null) {
28
+ queryValue.custom_start_date = action.payload.startDate;
29
+ queryValue.custom_end_date = action.payload.endDate;
30
+ }
21
31
  return zeniAPI
22
32
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/operating_expenses?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
23
33
  .pipe(mergeMap((response) => {
24
34
  if (response.data != null && isSuccessResponse(response)) {
25
35
  const actions = [];
26
- actions.push(updateSectionAccountsView(action.payload.timeframe, 'expenses', reportIdUniqueKey(action.payload.forecastId), response.data.report.expenses, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
36
+ actions.push(updateSectionAccountsView(action.payload.timeframe, 'expenses', reportIdUniqueKey(action.payload.forecastId, action.payload.startDate != null &&
37
+ action.payload.endDate != null), response.data.report.expenses, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
27
38
  actions.push(updateOpExForTimeframe({
28
39
  timeframe: action.payload.timeframe,
29
40
  forecastId: action.payload.forecastId,
30
41
  report: response.data,
42
+ isCustomDateRange: action.payload.startDate != null &&
43
+ action.payload.endDate != null,
31
44
  }));
32
45
  if (action.payload.forecastId == null &&
33
46
  action.payload.timeframe === 'month') {
@@ -42,7 +55,9 @@ export const fetchOpExReportForTimeframeEpic = (actions$, state, zeniAPI) => act
42
55
  return from(actions);
43
56
  }
44
57
  else {
45
- return of(updateOpExFailureForTimeframe(response.status, action.payload.forecastId, action.payload.timeframe));
58
+ return of(updateOpExFailureForTimeframe(response.status, action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
59
+ action.payload.endDate != null));
46
60
  }
47
- }), catchError((error) => of(updateOpExFailureForTimeframe(createZeniAPIStatus('Unexpected error', 'OpEx REST API call errored out' + JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe))));
61
+ }), catchError((error) => of(updateOpExFailureForTimeframe(createZeniAPIStatus('Unexpected error', 'OpEx REST API call errored out' + JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
62
+ action.payload.endDate != null))));
48
63
  }));