@zeniai/client-epic-state 5.2.93 → 5.2.94

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 (82) 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/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  17. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +25 -10
  18. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  19. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  20. package/lib/esm/view/opEx/opExEpic.js +7 -7
  21. package/lib/esm/view/opEx/opExForTimeFrameEpic.js +21 -6
  22. package/lib/esm/view/opEx/opExReducer.js +169 -32
  23. package/lib/esm/view/opEx/opExSelector.js +28 -10
  24. package/lib/esm/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  25. package/lib/esm/view/profitAndLoss/profitAndLossReducer.js +9 -2
  26. package/lib/esm/view/revenue/revenueEpic.js +7 -7
  27. package/lib/esm/view/revenue/revenueForTimeframeEpic.js +23 -8
  28. package/lib/esm/view/revenue/revenueReducer.js +183 -40
  29. package/lib/esm/view/revenue/revenueSelector.js +30 -10
  30. package/lib/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
  31. package/lib/view/balanceSheet/balanceSheetReducer.d.ts +3 -1
  32. package/lib/view/balanceSheet/balanceSheetReducer.js +8 -2
  33. package/lib/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
  34. package/lib/view/cashFlow/cashFlowReducer.d.ts +3 -1
  35. package/lib/view/cashFlow/cashFlowReducer.js +2 -2
  36. package/lib/view/cashInCashOut/cashInCashOutEpic.d.ts +8 -2
  37. package/lib/view/cashInCashOut/cashInCashOutEpic.js +22 -14
  38. package/lib/view/cashInCashOut/cashInCashOutReducer.d.ts +13 -1
  39. package/lib/view/cashInCashOut/cashInCashOutReducer.js +161 -41
  40. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +2 -1
  41. package/lib/view/cashInCashOut/cashInCashOutSelector.js +42 -15
  42. package/lib/view/cashInCashOut/cashInCashOutState.d.ts +5 -1
  43. package/lib/view/cashPosition/cashPositionEpic.d.ts +8 -2
  44. package/lib/view/cashPosition/cashPositionEpic.js +21 -13
  45. package/lib/view/cashPosition/cashPositionReducer.d.ts +13 -2
  46. package/lib/view/cashPosition/cashPositionReducer.js +155 -44
  47. package/lib/view/cashPosition/cashPositionSelector.d.ts +2 -1
  48. package/lib/view/cashPosition/cashPositionSelector.js +28 -9
  49. package/lib/view/cashPosition/cashPositionState.d.ts +5 -1
  50. package/lib/view/financeStatement/financeStatementEpic.d.ts +6 -0
  51. package/lib/view/netBurnOrIncome/fetchNetBurnOrIncomeWithForecastEpic.d.ts +1 -0
  52. package/lib/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  53. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.d.ts +2 -1
  54. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +26 -11
  55. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.d.ts +12 -3
  56. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  57. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.d.ts +1 -1
  58. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  59. package/lib/view/netBurnOrIncome/netBurnOrIncomeState.d.ts +8 -0
  60. package/lib/view/opEx/fetchOpExWithForecastEpic.d.ts +1 -0
  61. package/lib/view/opEx/opExEpic.js +7 -7
  62. package/lib/view/opEx/opExForTimeFrameEpic.d.ts +2 -1
  63. package/lib/view/opEx/opExForTimeFrameEpic.js +22 -7
  64. package/lib/view/opEx/opExReducer.d.ts +12 -3
  65. package/lib/view/opEx/opExReducer.js +168 -31
  66. package/lib/view/opEx/opExSelector.d.ts +1 -1
  67. package/lib/view/opEx/opExSelector.js +28 -10
  68. package/lib/view/opEx/opExState.d.ts +6 -0
  69. package/lib/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  70. package/lib/view/profitAndLoss/profitAndLossReducer.d.ts +3 -1
  71. package/lib/view/profitAndLoss/profitAndLossReducer.js +9 -2
  72. package/lib/view/reportUIOptions/updateReportUIOptionCOABalancesRangeEpic.d.ts +15 -0
  73. package/lib/view/revenue/fetchRevenueWithForecastEpic.d.ts +1 -0
  74. package/lib/view/revenue/revenueEpic.js +7 -7
  75. package/lib/view/revenue/revenueForTimeframeEpic.d.ts +2 -1
  76. package/lib/view/revenue/revenueForTimeframeEpic.js +24 -9
  77. package/lib/view/revenue/revenueReducer.d.ts +12 -3
  78. package/lib/view/revenue/revenueReducer.js +182 -39
  79. package/lib/view/revenue/revenueSelector.d.ts +1 -1
  80. package/lib/view/revenue/revenueSelector.js +30 -10
  81. package/lib/view/revenue/revenueState.d.ts +8 -0
  82. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { getDataAvailableDateRange, getNumberOfPeriods, } from '../../commonStat
5
5
  import { extractThisPeriod, getMatchingThisPeriod, } from '../../commonStateTypes/viewAndReport/thisPeriodHelpers';
6
6
  import { mapInsightsPayloadtoInsightsState } from '../../entity/insights/insightSummaryPayload';
7
7
  import { clearAll } from '../../rootActions';
8
+ import { date } from '../../zeniDayJS';
8
9
  import { getAvailableCOABalancesWithMostRecentNonZeroBalance, getInitialSelectedAndHighlightedRanges, getSelectedAndHighlightedRangesForNewSelection, getSelectedAndHighlightedRangesForThisPeriod, } from '../common/getSelectedAndHighlightedRanges';
9
10
  export const defaultPeriods = {
10
11
  numberOfPeriods: 8,
@@ -14,6 +15,12 @@ export const initialState = {
14
15
  error: undefined,
15
16
  cashPosition: undefined,
16
17
  dataAvailable: undefined,
18
+ // Custom-date slots must be explicit in initialState so clearAll's
19
+ // Object.assign(draft, initialState) resets them on tenant switch (absent
20
+ // keys would otherwise survive). Mirrors cashInCashOut.
21
+ customCashPosition: undefined,
22
+ customDataAvailable: undefined,
23
+ customFetchState: undefined,
17
24
  firstMonthOfFY: 1,
18
25
  reportId: 'cash_position',
19
26
  reportTitle: '',
@@ -45,52 +52,54 @@ const cashPosition = createSlice({
45
52
  initialState,
46
53
  reducers: {
47
54
  fetchCashPosition: {
48
- prepare(fetchFullReport = false) {
49
- return { payload: { fetchFullReport } };
55
+ prepare(fetchFullReport = false, startDate, endDate) {
56
+ return { payload: { fetchFullReport, startDate, endDate } };
50
57
  },
51
- reducer(draft) {
52
- draft.fetchState = 'In-Progress';
53
- draft.error = undefined;
58
+ reducer(draft, action) {
59
+ // Route by the fetch's own dates, not the global flag: a custom fetch
60
+ // (carries start/end) marks customFetchState so the shared fetchState the
61
+ // dashboard reads is never left In-Progress by a detail-screen fetch.
62
+ const isCustomDateRange = action.payload.startDate != null && action.payload.endDate != null;
63
+ if (isCustomDateRange) {
64
+ draft.customFetchState = {
65
+ fetchState: 'In-Progress',
66
+ error: undefined,
67
+ };
68
+ }
69
+ else {
70
+ draft.fetchState = 'In-Progress';
71
+ draft.error = undefined;
72
+ }
54
73
  },
55
74
  },
56
- updateCashPosition(draft, action) {
57
- const reportDataPayload = action.payload;
58
- const report = mapReportPayloadToReport(reportDataPayload.report);
59
- Object.assign(draft, report);
60
- const { cash_position, insights } = reportDataPayload.report;
61
- draft.fetchState = 'Completed';
62
- draft.error = undefined;
63
- draft.cashPosition =
64
- mapCOABalanceGroupedWithAccountIDsPayloadToCOABalanceGroupedWithAccountIDs({
65
- ...cash_position,
66
- account_ids: reportDataPayload.report.accounts.map((payload) => payload.account_id),
67
- });
68
- draft.insights = mapInsightsPayloadtoInsightsState(insights);
69
- draft.dataAvailable = report.dataAvailable
70
- ? report.dataAvailable
71
- : getDataAvailableDateRange(draft.cashPosition);
72
- if (draft.dataAvailable != null) {
73
- const timeframe = draft.uiState.timeframe;
74
- const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [draft.cashPosition], draft.dataAvailable);
75
- const selectionRanges = getInitialSelectedAndHighlightedRanges({
76
- firstMonthOfFY: draft.firstMonthOfFY,
77
- timeframe,
78
- coaBalances,
79
- orderBy: 'ascending_date',
80
- maxNumOfPeriodsToSelect: defaultPeriods.numberOfPeriods,
81
- maxNumOfPeriodsToHighlight: initialState.uiState.maxNumOfPeriodsToHighlight,
82
- });
83
- if (selectionRanges?.selectedCOABalancesRange != null) {
84
- draft.uiState.selectedCOABalancesRange =
85
- selectionRanges?.selectedCOABalancesRange;
86
- }
87
- draft.uiState.highlightedCOABalancesRange =
88
- selectionRanges?.highlightedCOABalancesRange;
89
- }
75
+ updateCashPosition: {
76
+ prepare(reportData, isCustomDateRange = false) {
77
+ return { payload: { reportData, isCustomDateRange } };
78
+ },
79
+ reducer(draft, action) {
80
+ doUpdateCashPosition(draft, action.payload.reportData, action.payload.isCustomDateRange);
81
+ },
90
82
  },
91
- updateCashPositionFailure(draft, action) {
92
- draft.fetchState = 'Error';
93
- draft.error = action.payload;
83
+ updateCashPositionFailure: {
84
+ prepare(error, isCustomDateRange = false) {
85
+ return { payload: { error, isCustomDateRange } };
86
+ },
87
+ reducer(draft, action) {
88
+ // Route the error like the request/success: a custom (dated) fetch's
89
+ // failure lands in customFetchState so the shared default the dashboard
90
+ // reads isn't marked Error, and the detail's customFetchState isn't left
91
+ // stuck In-Progress.
92
+ if (action.payload.isCustomDateRange === true) {
93
+ draft.customFetchState = {
94
+ fetchState: 'Error',
95
+ error: action.payload.error,
96
+ };
97
+ }
98
+ else {
99
+ draft.fetchState = 'Error';
100
+ draft.error = action.payload.error;
101
+ }
102
+ },
94
103
  },
95
104
  updateCashPositionAdditionalBalancesSelection(draft, action) {
96
105
  const { cashPosition, dataAvailable, uiState } = draft;
@@ -105,7 +114,7 @@ const cashPosition = createSlice({
105
114
  action.payload.additionalBalances.length != 0
106
115
  ? true
107
116
  : false;
108
- if (dataAvailable != null) {
117
+ if (dataAvailable != null && draft.uiState.customTimeframe == null) {
109
118
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, uiState.timeframe, [cashPosition], dataAvailable);
110
119
  const thisPeriod = extractThisPeriod(draft.firstMonthOfFY, timeframe, selectedCOABalancesRange, coaBalances);
111
120
  if (thisPeriod != null &&
@@ -134,6 +143,46 @@ const cashPosition = createSlice({
134
143
  }
135
144
  },
136
145
  updateCashPositionCOABalancesRange(draft, action) {
146
+ if (action.payload.isCustomDateRange != null) {
147
+ if (action.payload.isCustomDateRange === true) {
148
+ // Collapse the two raw date strings into a single customTimeframe object.
149
+ // Presence of customTimeframe signals custom-mode active (replaces the
150
+ // old isCustomDateRangeActive boolean + customStartDate/customEndDate).
151
+ draft.uiState.customTimeframe =
152
+ action.payload.customStartDate != null &&
153
+ action.payload.customEndDate != null
154
+ ? {
155
+ startDate: date(action.payload.customStartDate),
156
+ endDate: date(action.payload.customEndDate),
157
+ }
158
+ : undefined;
159
+ // Invalidate the shared default-window fetchState so dashboardEpic
160
+ // refetches the default data on the next dashboard visit — the custom
161
+ // fetch writes into the shared report store, and this lets the card
162
+ // self-heal without a reload. The detail screen reads customFetchState.
163
+ draft.fetchState = 'Not-Started';
164
+ draft.error = undefined;
165
+ }
166
+ else {
167
+ draft.uiState.customTimeframe = undefined;
168
+ }
169
+ }
170
+ // A custom date range drives the visible window directly: honor the exact
171
+ // numberOfPeriods + thisPeriod from the payload instead of re-deriving
172
+ // (which clamps the range to the available/non-zero periods).
173
+ if (draft.uiState.customTimeframe != null) {
174
+ draft.uiState.selectedCOABalancesRange =
175
+ action.payload.selectedCOABalancesRange;
176
+ if (action.payload.highlightedCOABalancesRange != null) {
177
+ // Clamp the highlight to the same cap the non-custom path enforces so
178
+ // a wide custom range doesn't highlight all N periods at once.
179
+ draft.uiState.highlightedCOABalancesRange = {
180
+ ...action.payload.highlightedCOABalancesRange,
181
+ numberOfPeriods: Math.min(action.payload.highlightedCOABalancesRange.numberOfPeriods, draft.uiState.maxNumOfPeriodsToHighlight),
182
+ };
183
+ }
184
+ return;
185
+ }
137
186
  const { cashPosition, dataAvailable, uiState } = draft;
138
187
  if (dataAvailable != null &&
139
188
  action.payload.selectedCOABalancesRange.thisPeriod != null) {
@@ -174,6 +223,13 @@ const cashPosition = createSlice({
174
223
  }
175
224
  },
176
225
  updateCashPositionThisPeriod(draft, action) {
226
+ // In custom-date mode the picker drives the window; skip re-deriving the
227
+ // selected/highlighted ranges from the default dataAvailable (the epic
228
+ // re-dispatches the default thisPeriod on every response, which would
229
+ // otherwise clamp the custom window back to the default).
230
+ if (draft.uiState.customTimeframe != null) {
231
+ return;
232
+ }
177
233
  const { cashPosition, dataAvailable, uiState } = draft;
178
234
  if (dataAvailable != null && action.payload.thisPeriod != null) {
179
235
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, uiState.timeframe, [cashPosition], dataAvailable);
@@ -218,7 +274,7 @@ const cashPosition = createSlice({
218
274
  draft.uiState.maxNumOfPeriodsToHighlight =
219
275
  initialState.uiState.maxNumOfPeriodsToHighlight;
220
276
  }
221
- if (dataAvailable != null) {
277
+ if (dataAvailable != null && draft.uiState.customTimeframe == null) {
222
278
  const timeframe = draft.uiState.timeframe;
223
279
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [cashPosition], dataAvailable);
224
280
  const selectionRanges = getInitialSelectedAndHighlightedRanges({
@@ -258,5 +314,60 @@ const cashPosition = createSlice({
258
314
  });
259
315
  },
260
316
  });
317
+ const doUpdateCashPosition = (draft, reportDataPayload, isCustomDateRange) => {
318
+ const report = mapReportPayloadToReport(reportDataPayload.report);
319
+ const { cash_position, insights } = reportDataPayload.report;
320
+ const cashPosition = mapCOABalanceGroupedWithAccountIDsPayloadToCOABalanceGroupedWithAccountIDs({
321
+ ...cash_position,
322
+ account_ids: reportDataPayload.report.accounts.map((payload) => payload.account_id),
323
+ });
324
+ const dataAvailable = report.dataAvailable
325
+ ? report.dataAvailable
326
+ : getDataAvailableDateRange(cashPosition);
327
+ // Route custom-date results (identified by the fetch's own dates, not the
328
+ // global flag) to the separate slots so the dashboard card (which reads
329
+ // cashPosition / dataAvailable) keeps the default window.
330
+ if (isCustomDateRange) {
331
+ draft.customCashPosition = cashPosition;
332
+ // Don't let a custom slice shrink the shared dataAvailable the dashboard uses;
333
+ // a default fetch always updates it, a custom one only populates it on first
334
+ // load (when nothing has set it yet).
335
+ draft.customDataAvailable = dataAvailable;
336
+ if (draft.dataAvailable == null) {
337
+ draft.dataAvailable = dataAvailable;
338
+ }
339
+ draft.customFetchState = { fetchState: 'Completed', error: undefined };
340
+ // Skip insights on a custom fetch so the dashboard card keeps the
341
+ // default-window copy (matches cashInCashOut / opEx / revenue / netBurn).
342
+ return;
343
+ }
344
+ Object.assign(draft, report);
345
+ draft.fetchState = 'Completed';
346
+ draft.error = undefined;
347
+ draft.cashPosition = cashPosition;
348
+ draft.insights = mapInsightsPayloadtoInsightsState(insights);
349
+ draft.dataAvailable = dataAvailable;
350
+ if (draft.dataAvailable != null &&
351
+ // A custom date range drives the window; don't reset it to the default on
352
+ // every report receipt (this is what clamped wide ranges back to 8).
353
+ draft.uiState.customTimeframe == null) {
354
+ const timeframe = draft.uiState.timeframe;
355
+ const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [draft.cashPosition], draft.dataAvailable);
356
+ const selectionRanges = getInitialSelectedAndHighlightedRanges({
357
+ firstMonthOfFY: draft.firstMonthOfFY,
358
+ timeframe,
359
+ coaBalances,
360
+ orderBy: 'ascending_date',
361
+ maxNumOfPeriodsToSelect: defaultPeriods.numberOfPeriods,
362
+ maxNumOfPeriodsToHighlight: initialState.uiState.maxNumOfPeriodsToHighlight,
363
+ });
364
+ if (selectionRanges?.selectedCOABalancesRange != null) {
365
+ draft.uiState.selectedCOABalancesRange =
366
+ selectionRanges?.selectedCOABalancesRange;
367
+ }
368
+ draft.uiState.highlightedCOABalancesRange =
369
+ selectionRanges?.highlightedCOABalancesRange;
370
+ }
371
+ };
261
372
  export const { fetchCashPosition, updateCashPosition, updateCashPositionFailure, updateCashPositionTimeFrame, updateCashPositionCOABalancesRange, updateCashPositionAdditionalBalancesSelection, updateCashPositionThisPeriod, updateCashPositionDownloadState, updateCashPositionUIState, } = cashPosition.actions;
262
373
  export default cashPosition.reducer;
@@ -4,20 +4,30 @@ import { getCOABalances } from '../../commonStateTypes/coaBalance/getCOABalances
4
4
  import { getNumberOfPeriods } from '../../commonStateTypes/dataAvailable';
5
5
  import { toTimeframeTick, } from '../../commonStateTypes/timeframeTick';
6
6
  import { getAccountsOrdered } from '../../entity/account/accountSelector';
7
- export const getCashPosition = (accountState, cashPositionState, filter) => {
7
+ export const getCashPosition = (accountState, cashPositionState, filter, readCustomRangeSlot) => {
8
+ // The detail screen opts into the isolated custom slot when custom mode is
9
+ // active; the dashboard (no flag) reads the default slot, so a default fetch
10
+ // can't clobber the custom partial-period data the detail is showing.
11
+ const useCustomRange = readCustomRangeSlot === true &&
12
+ cashPositionState.uiState.customTimeframe != null;
13
+ const activeCashPosition = useCustomRange
14
+ ? cashPositionState.customCashPosition
15
+ : cashPositionState.cashPosition;
16
+ const dataAvailable = useCustomRange
17
+ ? cashPositionState.customDataAvailable
18
+ : cashPositionState.dataAvailable;
8
19
  const cashPosition = {
9
20
  itemOrder: 'last_balance_descending',
10
21
  accountsOrdered: getAccountsOrdered(filter, accountState, {
11
- accountIDs: cashPositionState.cashPosition?.accountIDs ?? [],
22
+ accountIDs: activeCashPosition?.accountIDs ?? [],
12
23
  reportId: 'cash_position',
13
24
  }, 'cashPosition'),
14
- ...getCOABalances(cashPositionState.cashPosition, filter),
25
+ ...getCOABalances(activeCashPosition, filter),
15
26
  };
16
27
  let allTimeframeTicks = [];
17
- const dataAvailable = cashPositionState.dataAvailable;
18
28
  if (dataAvailable != null) {
19
29
  const noFilter = newBalancesFilterForDateRange(cashPositionState.firstMonthOfFY, cashPositionState.uiState.timeframe, dataAvailable, 'ascending_date', []);
20
- const cashPositionBalances = getCOABalances(cashPositionState.cashPosition, noFilter);
30
+ const cashPositionBalances = getCOABalances(activeCashPosition, noFilter);
21
31
  allTimeframeTicks = cashPositionBalances.balances.map((balance) => toTimeframeTick(balance));
22
32
  }
23
33
  return {
@@ -31,10 +41,19 @@ export const getCashPosition = (accountState, cashPositionState, filter) => {
31
41
  reportTitle: cashPositionState.reportTitle,
32
42
  version: cashPositionState.version,
33
43
  status: cashPositionState.status,
34
- fetchState: cashPositionState.fetchState,
35
- error: cashPositionState.error,
44
+ // The detail screen opts into customFetchState so its loading/complete state
45
+ // tracks the custom fetch; the dashboard (no flag) reads the shared
46
+ // fetchState, which custom-mode activation reset so dashboardEpic refetches.
47
+ fetchState: useCustomRange && cashPositionState.customFetchState != null
48
+ ? cashPositionState.customFetchState.fetchState
49
+ : cashPositionState.fetchState,
50
+ error: useCustomRange && cashPositionState.customFetchState != null
51
+ ? cashPositionState.customFetchState.error
52
+ : cashPositionState.error,
36
53
  insights: cashPositionState.insights ?? [],
37
54
  downloadState: cashPositionState.uiState.downloadState,
55
+ // Surface the customTimeframe so the detail picker can restore the selection.
56
+ customTimeframe: cashPositionState.uiState.customTimeframe,
38
57
  };
39
58
  };
40
59
  export const getCashPositionUIState = createSelector((state) => state.cashPositionState, (state) => {
@@ -60,11 +79,11 @@ export const getCashPositionUIState = createSelector((state) => state.cashPositi
60
79
  export const getCashPositionForSelectedRange = createSelector((state) => state.accountState, (state) => state.cashPositionState, (accountState, cashPositionState) => {
61
80
  const { timeframe, selectedCOABalancesRange, selectedAdditionalBalances } = cashPositionState.uiState;
62
81
  const noFilter = newBalancesFilter(cashPositionState.firstMonthOfFY, timeframe, selectedCOABalancesRange.numberOfPeriods, selectedCOABalancesRange.thisPeriod, 'ascending_date', selectedAdditionalBalances);
63
- return getCashPosition(accountState, cashPositionState, noFilter);
82
+ return getCashPosition(accountState, cashPositionState, noFilter, true);
64
83
  });
65
84
  export const getCashPositionForHighlightedRange = createSelector((state) => state.accountState, (state) => state.cashPositionState, (accountState, cashPositionState) => {
66
85
  const { timeframe, highlightedCOABalancesRange, selectedAdditionalBalances, maxNumOfPeriodsToHighlight, } = cashPositionState.uiState;
67
86
  const filter = newBalancesFilter(cashPositionState.firstMonthOfFY, timeframe, highlightedCOABalancesRange?.numberOfPeriods ??
68
87
  maxNumOfPeriodsToHighlight, highlightedCOABalancesRange?.thisPeriod, 'ascending_date', selectedAdditionalBalances);
69
- return getCashPosition(accountState, cashPositionState, filter);
88
+ return getCashPosition(accountState, cashPositionState, filter, true);
70
89
  });
@@ -2,12 +2,12 @@ import { from } from 'rxjs';
2
2
  import { filter, mergeMap } from 'rxjs/operators';
3
3
  import { fetchNetBurnOrIncome, fetchNetBurnOrIncomeForTimeframe, getState, } from './netBurnOrIncomeReducer';
4
4
  export const fetchNetBurnOrIncomeEpic = (actions$, state) => actions$.pipe(filter(fetchNetBurnOrIncome.match), mergeMap((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
- fetchNetBurnOrIncomeForTimeframe('month', forecastId),
9
- fetchNetBurnOrIncomeForTimeframe('quarter', forecastId),
10
- fetchNetBurnOrIncomeForTimeframe('year', forecastId),
8
+ fetchNetBurnOrIncomeForTimeframe('month', forecastId, true, false, startDate, endDate),
9
+ fetchNetBurnOrIncomeForTimeframe('quarter', forecastId, true, false, startDate, endDate),
10
+ fetchNetBurnOrIncomeForTimeframe('year', forecastId, true, false, startDate, endDate),
11
11
  ]);
12
12
  }
13
13
  else {
@@ -16,15 +16,15 @@ export const fetchNetBurnOrIncomeEpic = (actions$, state) => actions$.pipe(filte
16
16
  const timeframeFetchState = netBurnOrIncomeState.fetchState;
17
17
  if (timeframeFetchState['month'].fetchState !== 'In-Progress' &&
18
18
  timeframeFetchState['month'].fetchState !== 'Completed') {
19
- fetchActions.push(fetchNetBurnOrIncomeForTimeframe('month', forecastId));
19
+ fetchActions.push(fetchNetBurnOrIncomeForTimeframe('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(fetchNetBurnOrIncomeForTimeframe('quarter', forecastId));
23
+ fetchActions.push(fetchNetBurnOrIncomeForTimeframe('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(fetchNetBurnOrIncomeForTimeframe('year', forecastId));
27
+ fetchActions.push(fetchNetBurnOrIncomeForTimeframe('year', forecastId, true, false, startDate, endDate));
28
28
  }
29
29
  return from(fetchActions);
30
30
  }
@@ -5,9 +5,15 @@ import { updateCalculatedSectionAccountsView, updateSectionAccountsView, } from
5
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
6
6
  import { getTimeSeriesGroup } from '../common/reportsHelpers';
7
7
  import { fetchNetBurnOrIncomeForTimeframe, updateNetBurnOrIncomeForTimeframe, updateNetBurnOrIncomeForTimeframeFailure, updateNetBurnOrIncomeThisPeriod, } from './netBurnOrIncomeReducer';
8
- export const reportIdUniqueKey = (forecastId) => forecastId != null
9
- ? `net_burn_or_income+${forecastId}`
10
- : 'net_burn_or_income';
8
+ // Reserved forecast-slot marker so custom-date section-accounts data lives under
9
+ // its own key (net_burn_or_income+<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
+ ? `net_burn_or_income+${CUSTOM_DATE_RANGE_SECTION_MARKER}`
14
+ : forecastId != null
15
+ ? `net_burn_or_income+${forecastId}`
16
+ : 'net_burn_or_income';
11
17
  export const fetchNetBurnOrIncomeForTimeframeEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchNetBurnOrIncomeForTimeframe.match), mergeMap((action) => {
12
18
  const queryValue = {
13
19
  is_full_response: action.payload.fetchFullReport,
@@ -18,20 +24,27 @@ export const fetchNetBurnOrIncomeForTimeframeEpic = (actions$, state, zeniAPI) =
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/net_burn_or_income?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, 'cogs', reportIdUniqueKey(action.payload.forecastId), response.data.report.cogs, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
27
- 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));
28
- actions.push(updateSectionAccountsView(action.payload.timeframe, 'income', reportIdUniqueKey(action.payload.forecastId), response.data.report.income, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
29
- actions.push(updateCalculatedSectionAccountsView(action.payload.timeframe, 'grossBurn', reportIdUniqueKey(action.payload.forecastId), response.data.report.gross_burn, response.data.report.currency));
30
- actions.push(updateCalculatedSectionAccountsView(action.payload.timeframe, 'netBurnOrIncome', reportIdUniqueKey(action.payload.forecastId), response.data.report.net_burn_or_income, response.data.report.currency));
36
+ const isCustomDateRange = action.payload.startDate != null &&
37
+ action.payload.endDate != null;
38
+ actions.push(updateSectionAccountsView(action.payload.timeframe, 'cogs', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.cogs, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
39
+ actions.push(updateSectionAccountsView(action.payload.timeframe, 'expenses', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.expenses, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
40
+ actions.push(updateSectionAccountsView(action.payload.timeframe, 'income', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.income, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
41
+ actions.push(updateCalculatedSectionAccountsView(action.payload.timeframe, 'grossBurn', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.gross_burn, response.data.report.currency));
42
+ actions.push(updateCalculatedSectionAccountsView(action.payload.timeframe, 'netBurnOrIncome', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.net_burn_or_income, response.data.report.currency));
31
43
  actions.push(updateNetBurnOrIncomeForTimeframe({
32
44
  timeframe: action.payload.timeframe,
33
45
  forecastId: action.payload.forecastId,
34
46
  report: response.data,
47
+ isCustomDateRange,
35
48
  }));
36
49
  if (action.payload.forecastId == null &&
37
50
  action.payload.timeframe === 'month') {
@@ -46,8 +59,10 @@ export const fetchNetBurnOrIncomeForTimeframeEpic = (actions$, state, zeniAPI) =
46
59
  return from(actions);
47
60
  }
48
61
  else {
49
- return of(updateNetBurnOrIncomeForTimeframeFailure(response.status, action.payload.forecastId, action.payload.timeframe));
62
+ return of(updateNetBurnOrIncomeForTimeframeFailure(response.status, action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
63
+ action.payload.endDate != null));
50
64
  }
51
65
  }), catchError((error) => of(updateNetBurnOrIncomeForTimeframeFailure(createZeniAPIStatus('Unexpected Error', 'Net burn or income REST API call errored out' +
52
- JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe))));
66
+ JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
67
+ action.payload.endDate != null))));
53
68
  }));