@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
@@ -5,7 +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 { dateNow } from '../../zeniDayJS';
8
+ import { date, dateNow } from '../../zeniDayJS';
9
9
  import { getAvailableCOABalancesWithMostRecentNonZeroBalance, getInitialSelectedAndHighlightedRanges, getSelectedAndHighlightedRangesForNewSelection, getSelectedAndHighlightedRangesForThisPeriod, } from '../common/getSelectedAndHighlightedRanges';
10
10
  import { isCOABalanceGroupedByPeriodEmptyForTimeframe } from '../common/reportsHelpers';
11
11
  export const defaultPeriods = {
@@ -68,8 +68,16 @@ const revenue = createSlice({
68
68
  initialState,
69
69
  reducers: {
70
70
  fetchRevenue: {
71
- prepare(fetchFullReport = false, forecastId, cacheOverride = false) {
72
- return { payload: { fetchFullReport, forecastId, cacheOverride } };
71
+ prepare(fetchFullReport = false, forecastId, cacheOverride = false, startDate, endDate) {
72
+ return {
73
+ payload: {
74
+ fetchFullReport,
75
+ forecastId,
76
+ cacheOverride,
77
+ startDate,
78
+ endDate,
79
+ },
80
+ };
73
81
  },
74
82
  reducer(
75
83
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -80,22 +88,49 @@ const revenue = createSlice({
80
88
  },
81
89
  },
82
90
  fetchRevenueForTimeframe: {
83
- prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false) {
91
+ prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
84
92
  return {
85
- payload: { forecastId, timeframe, fetchFullReport, cacheOverride },
93
+ payload: {
94
+ forecastId,
95
+ timeframe,
96
+ fetchFullReport,
97
+ cacheOverride,
98
+ startDate,
99
+ endDate,
100
+ },
86
101
  };
87
102
  },
88
103
  reducer(draft, action) {
89
104
  const timeframe = action.payload.timeframe;
90
105
  const revState = getState(draft, draft.uiState.isForecastModeOn, action.payload.forecastId);
91
- const fetchState = {
92
- ...revState.fetchState,
93
- };
94
- fetchState[timeframe] = {
95
- fetchState: 'In-Progress',
96
- error: undefined,
97
- };
98
- updateState(draft, action.payload.forecastId, { fetchState });
106
+ // Route by the fetch's own dates, not the global flag: a custom fetch
107
+ // (carries start/end) marks customFetchState so the shared fetchState the
108
+ // dashboard reads is never left In-Progress by a detail-screen fetch.
109
+ const isCustomDateRange = action.payload.startDate != null && action.payload.endDate != null;
110
+ if (isCustomDateRange) {
111
+ const customFetchState = {
112
+ ...(revState.customFetchState ?? {
113
+ month: { fetchState: 'Not-Started', error: undefined },
114
+ quarter: { fetchState: 'Not-Started', error: undefined },
115
+ year: { fetchState: 'Not-Started', error: undefined },
116
+ }),
117
+ };
118
+ customFetchState[timeframe] = {
119
+ fetchState: 'In-Progress',
120
+ error: undefined,
121
+ };
122
+ updateState(draft, action.payload.forecastId, { customFetchState });
123
+ }
124
+ else {
125
+ const fetchState = {
126
+ ...revState.fetchState,
127
+ };
128
+ fetchState[timeframe] = {
129
+ fetchState: 'In-Progress',
130
+ error: undefined,
131
+ };
132
+ updateState(draft, action.payload.forecastId, { fetchState });
133
+ }
99
134
  },
100
135
  },
101
136
  fetchRevenueWithForecast: {
@@ -125,28 +160,47 @@ const revenue = createSlice({
125
160
  },
126
161
  },
127
162
  updateRevenueForTimeframe(draft, action) {
128
- doUpdateRevenue(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report);
163
+ doUpdateRevenue(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report, action.payload.isCustomDateRange === true);
129
164
  },
130
165
  updateRevenueForTimeframeFailure: {
131
- prepare(error, forecastId, timeframe) {
166
+ prepare(error, forecastId, timeframe, isCustomDateRange = false) {
132
167
  return {
133
168
  payload: {
134
169
  error,
135
170
  forecastId,
136
171
  timeframe,
172
+ isCustomDateRange,
137
173
  },
138
174
  };
139
175
  },
140
176
  reducer(draft, action) {
141
177
  const revState = getState(draft, draft.uiState.isForecastModeOn, action.payload.forecastId);
142
- const fetchState = {
143
- ...revState.fetchState,
144
- };
145
- fetchState[action.payload.timeframe] = {
146
- fetchState: 'Error',
147
- error: action.payload.error,
148
- };
149
- updateState(draft, action.payload.forecastId, { fetchState });
178
+ // Route the error like the request/success: a custom (dated) fetch's
179
+ // failure lands in customFetchState so the shared default the dashboard
180
+ // reads isn't marked Error, and the detail's customFetchState isn't left
181
+ // stuck In-Progress.
182
+ if (action.payload.isCustomDateRange === true) {
183
+ const customFetchState = revState.customFetchState ?? {
184
+ month: { fetchState: 'Not-Started', error: undefined },
185
+ quarter: { fetchState: 'Not-Started', error: undefined },
186
+ year: { fetchState: 'Not-Started', error: undefined },
187
+ };
188
+ customFetchState[action.payload.timeframe] = {
189
+ fetchState: 'Error',
190
+ error: action.payload.error,
191
+ };
192
+ updateState(draft, action.payload.forecastId, { customFetchState });
193
+ }
194
+ else {
195
+ const fetchState = {
196
+ ...revState.fetchState,
197
+ };
198
+ fetchState[action.payload.timeframe] = {
199
+ fetchState: 'Error',
200
+ error: action.payload.error,
201
+ };
202
+ updateState(draft, action.payload.forecastId, { fetchState });
203
+ }
150
204
  },
151
205
  },
152
206
  updateRevenueAdditionalBalancesSelection(draft, action) {
@@ -165,7 +219,9 @@ const revenue = createSlice({
165
219
  ? true
166
220
  : false;
167
221
  // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
168
- if (dataAvailable != null && draft.uiState.isForecastModeOn === false) {
222
+ if (dataAvailable != null &&
223
+ draft.uiState.isForecastModeOn === false &&
224
+ draft.uiState.customTimeframe == null) {
169
225
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(revenueReport.firstMonthOfFY, timeframe, [income, cogs, grossMargin], dataAvailable);
170
226
  const thisPeriod = extractThisPeriod(revenueReport.firstMonthOfFY, timeframe, selectedCOABalancesRange, coaBalances);
171
227
  if (thisPeriod != null &&
@@ -194,6 +250,48 @@ const revenue = createSlice({
194
250
  }
195
251
  },
196
252
  updateRevenueCOABalancesRange(draft, action) {
253
+ if (action.payload.isCustomDateRange != null) {
254
+ if (action.payload.isCustomDateRange === true) {
255
+ // Convert the string dates the action carries to ZeniDate values and
256
+ // store as a single customTimeframe. Presence signals custom-mode active.
257
+ draft.uiState.customTimeframe =
258
+ action.payload.customStartDate != null &&
259
+ action.payload.customEndDate != null
260
+ ? {
261
+ startDate: date(action.payload.customStartDate),
262
+ endDate: date(action.payload.customEndDate),
263
+ }
264
+ : undefined;
265
+ // Invalidate the shared default-window fetchState so dashboardEpic
266
+ // refetches the default data on the next dashboard visit — the custom
267
+ // fetch writes into the shared section store, and this lets the card
268
+ // self-heal without a reload. The detail screen reads customFetchState.
269
+ draft.revenue.fetchState = {
270
+ month: { fetchState: 'Not-Started', error: undefined },
271
+ quarter: { fetchState: 'Not-Started', error: undefined },
272
+ year: { fetchState: 'Not-Started', error: undefined },
273
+ };
274
+ }
275
+ else {
276
+ draft.uiState.customTimeframe = undefined;
277
+ }
278
+ }
279
+ // A custom date range drives the visible window directly: honor the exact
280
+ // numberOfPeriods + thisPeriod from the payload instead of re-deriving
281
+ // (which clamps the range to the available/non-zero periods).
282
+ if (draft.uiState.customTimeframe != null) {
283
+ draft.uiState.selectedCOABalancesRange =
284
+ action.payload.selectedCOABalancesRange;
285
+ if (action.payload.highlightedCOABalancesRange != null) {
286
+ // Clamp the highlight to the same cap the non-custom path enforces so
287
+ // a wide custom range doesn't highlight all N periods at once.
288
+ draft.uiState.highlightedCOABalancesRange = {
289
+ ...action.payload.highlightedCOABalancesRange,
290
+ numberOfPeriods: Math.min(action.payload.highlightedCOABalancesRange.numberOfPeriods, draft.uiState.maxNumOfPeriodsToHighlight),
291
+ };
292
+ }
293
+ return;
294
+ }
197
295
  // Note: When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
198
296
  const revenueReport = getState(draft, undefined, undefined);
199
297
  const { income, cogs, grossMargin, dataAvailable } = revenueReport;
@@ -237,6 +335,13 @@ const revenue = createSlice({
237
335
  }
238
336
  },
239
337
  updateRevenueThisPeriod(draft, action) {
338
+ // In custom-date mode the picker drives the window; skip re-deriving the
339
+ // selected/highlighted ranges from the default dataAvailable (the epic
340
+ // re-dispatches the default thisPeriod on every month response, which
341
+ // would otherwise clamp the custom window back to the default).
342
+ if (draft.uiState.customTimeframe != null) {
343
+ return;
344
+ }
240
345
  // Note: When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
241
346
  const revenueReport = getState(draft, undefined, undefined);
242
347
  const { income, cogs, grossMargin, dataAvailable } = revenueReport;
@@ -286,7 +391,7 @@ const revenue = createSlice({
286
391
  draft.uiState.maxNumOfPeriodsToHighlight =
287
392
  initialState.uiState.maxNumOfPeriodsToHighlight;
288
393
  }
289
- if (dataAvailable != null) {
394
+ if (dataAvailable != null && draft.uiState.customTimeframe == null) {
290
395
  const timeframe = draft.uiState.timeframe;
291
396
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(revenueReport.firstMonthOfFY, timeframe, [income, cogs, grossMargin], dataAvailable);
292
397
  const selectionRanges = getInitialSelectedAndHighlightedRanges({
@@ -341,37 +446,75 @@ const revenue = createSlice({
341
446
  });
342
447
  export const { fetchRevenue, fetchRevenueWithForecast, fetchRevenueForTimeframe, waitForForecastListThenFetchRevenueWithForecast, updateRevenueForTimeframe, updateRevenueForTimeframeFailure, updateRevenueAdditionalBalancesSelection, updateRevenueCOABalancesRange, updateRevenueThisPeriod, updateRevenueDownloadState, updateRevenueTimeFrame, updateRevenueUIState, revenueToggleForecastMode, } = revenue.actions;
343
448
  export default revenue.reducer;
344
- const doUpdateRevenue = (draft, forecastId, timeframe, revReport) => {
449
+ const doUpdateRevenue = (draft, forecastId, timeframe, revReport, isCustomDateRange) => {
345
450
  const revenue = getState(draft, draft.uiState.isForecastModeOn, forecastId);
346
451
  const report = mapReportPayloadV2ToReportV2(revReport.report);
347
452
  const { income, cogs, insights, currency } = revReport.report;
348
453
  const incomeCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(income, currency, timeframe);
349
454
  const cogsCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(cogs, currency, timeframe);
350
455
  const grossMarginCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(revReport.report.gross_profit, currency, timeframe);
351
- revenue.income = Object.assign({}, revenue.income, incomeCOABalance);
352
- revenue.cogs = Object.assign({}, revenue.cogs, cogsCOABalance);
353
- revenue.grossMargin = Object.assign({}, revenue.grossMargin, grossMarginCOABalance);
354
- if (timeframe === 'month' && insights != null) {
456
+ // Route custom-date results (identified by the fetch's own dates, not the
457
+ // global flag) to the separate slots so the dashboard card (which reads
458
+ // income/cogs/grossMargin) keeps the default window.
459
+ if (isCustomDateRange) {
460
+ revenue.customIncome = Object.assign({}, revenue.customIncome, incomeCOABalance);
461
+ revenue.customCogs = Object.assign({}, revenue.customCogs, cogsCOABalance);
462
+ revenue.customGrossMargin = Object.assign({}, revenue.customGrossMargin, grossMarginCOABalance);
463
+ }
464
+ else {
465
+ revenue.income = Object.assign({}, revenue.income, incomeCOABalance);
466
+ revenue.cogs = Object.assign({}, revenue.cogs, cogsCOABalance);
467
+ revenue.grossMargin = Object.assign({}, revenue.grossMargin, grossMarginCOABalance);
468
+ }
469
+ // Insights are shared (read by the dashboard card); a custom slice must not
470
+ // overwrite them.
471
+ if (timeframe === 'month' && insights != null && !isCustomDateRange) {
355
472
  // we will get insights only in the month timeframe api
356
473
  revenue.insights = mapInsightsPayloadtoInsightsState(insights);
357
474
  }
358
475
  const isIncomeEmpty = isCOABalanceGroupedByPeriodEmptyForTimeframe(incomeCOABalance, timeframe);
359
476
  const isCogsEmpty = isCOABalanceGroupedByPeriodEmptyForTimeframe(cogsCOABalance, timeframe);
477
+ // Derive dataAvailable from the slot this fetch actually populated.
360
478
  const cogsOrIncome = isIncomeEmpty
361
479
  ? isCogsEmpty
362
- ? revenue.grossMargin
363
- : revenue.cogs
364
- : revenue.income;
365
- revenue.dataAvailable = report.dataAvailable
366
- ? report.dataAvailable
367
- : getDataAvailableDateRange(cogsOrIncome);
480
+ ? isCustomDateRange
481
+ ? revenue.customGrossMargin
482
+ : revenue.grossMargin
483
+ : isCustomDateRange
484
+ ? revenue.customCogs
485
+ : revenue.cogs
486
+ : isCustomDateRange
487
+ ? revenue.customIncome
488
+ : revenue.income;
489
+ // Don't let a custom slice shrink the shared dataAvailable the dashboard uses
490
+ // for its period count; a default fetch always updates it, a custom one only
491
+ // populates it on first load (when nothing has set it yet).
492
+ if (!isCustomDateRange || revenue.dataAvailable == null) {
493
+ revenue.dataAvailable =
494
+ report.dataAvailable ?? getDataAvailableDateRange(cogsOrIncome ?? {});
495
+ }
368
496
  revenue.firstMonthOfFY = report.firstMonthOfFY;
369
497
  revenue.bookCloseDate = report.bookCloseDate;
370
- revenue.fetchState[timeframe].fetchState = 'Completed';
371
- revenue.fetchState[timeframe].error = undefined;
498
+ // Mark the matching fetchState Completed. A custom fetch uses customFetchState so
499
+ // dashboardEpic's Completed check on the default fetchState is untouched.
500
+ if (isCustomDateRange) {
501
+ const customFetchState = revenue.customFetchState ?? {
502
+ month: { fetchState: 'Not-Started', error: undefined },
503
+ quarter: { fetchState: 'Not-Started', error: undefined },
504
+ year: { fetchState: 'Not-Started', error: undefined },
505
+ };
506
+ customFetchState[timeframe] = { fetchState: 'Completed', error: undefined };
507
+ revenue.customFetchState = customFetchState;
508
+ }
509
+ else {
510
+ revenue.fetchState[timeframe].fetchState = 'Completed';
511
+ revenue.fetchState[timeframe].error = undefined;
512
+ }
372
513
  if (revenue.dataAvailable != null &&
373
- forecastId == null // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
374
- ) {
514
+ forecastId == null && // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
515
+ // A custom date range drives the window; don't reset it to the default on
516
+ // every report receipt (this is what clamped wide ranges back to 8).
517
+ draft.uiState.customTimeframe == null) {
375
518
  const timeframe = draft.uiState.timeframe;
376
519
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(revenue.firstMonthOfFY, timeframe, [revenue.income, revenue.cogs, revenue.grossMargin], revenue.dataAvailable);
377
520
  const selectionRanges = getInitialSelectedAndHighlightedRanges({
@@ -13,14 +13,26 @@ import { ALL_REVENUE_CALCULATED_SECTION_IDS, ALL_REVENUE_SECTION_IDS, revenueIds
13
13
  import { reportIdUniqueKey } from './revenueForTimeframeEpic';
14
14
  import { getGrossMarginWithPercent } from './revenueHelper';
15
15
  import { getState } from './revenueReducer';
16
- export const getRevenueReport = (filter, accountState, accountGroupState, sectionsState, forecastListState, revenueWithForecastState, forecastState, forecastId) => {
16
+ export const getRevenueReport = (filter, accountState, accountGroupState, sectionsState, forecastListState, revenueWithForecastState, forecastState, forecastId, readCustomRangeSlot) => {
17
17
  const revState = getState(revenueWithForecastState, revenueWithForecastState.uiState.isForecastModeOn, forecastId);
18
+ // The detail screen opts into customFetchState so its Completed check fires
19
+ // off the custom fetch; the dashboard (no flag) reads the shared fetchState,
20
+ // which custom-mode activation reset to Not-Started so dashboardEpic refetches.
21
+ const activeFetchState = readCustomRangeSlot === true &&
22
+ revenueWithForecastState.uiState.customTimeframe != null &&
23
+ revState.customFetchState != null
24
+ ? revState.customFetchState
25
+ : revState.fetchState;
18
26
  const fetchState = reduceAnyFetchState([
19
- revState.fetchState.month,
20
- revState.fetchState.quarter,
21
- revState.fetchState.year,
27
+ activeFetchState.month,
28
+ activeFetchState.quarter,
29
+ activeFetchState.year,
22
30
  ]);
23
- const reportId = reportIdUniqueKey(forecastId);
31
+ // Custom detail reads the isolated custom section-accounts slot; dashboard
32
+ // (no flag) reads the default slot, so a default fetch can't clobber it.
33
+ const useCustomRange = readCustomRangeSlot === true &&
34
+ revenueWithForecastState.uiState.customTimeframe != null;
35
+ const reportId = reportIdUniqueKey(forecastId, useCustomRange);
24
36
  const sections = {};
25
37
  const calculatedSections = {};
26
38
  let allTimeframeTicks = [];
@@ -46,16 +58,16 @@ export const getRevenueReport = (filter, accountState, accountGroupState, sectio
46
58
  const timeframe = revenueWithForecastState.uiState.timeframe;
47
59
  // To get incomeBalances without forecast init.
48
60
  const incomeBalance = getSectionBalances(sectionsState, {
49
- reportId: reportIdUniqueKey(), // reportId without forecastId init to fetch Accouts incomeBalances
61
+ reportId: reportIdUniqueKey(undefined, useCustomRange), // reportId without forecastId init to fetch Accouts incomeBalances
50
62
  sectionId: 'income',
51
63
  });
52
64
  // To get cogsBalance without forecast init.
53
65
  const cogsBalance = getSectionBalances(sectionsState, {
54
- reportId: reportIdUniqueKey(), // reportId without forecastId init to fetch Accouts cogsBalance
66
+ reportId: reportIdUniqueKey(undefined, useCustomRange), // reportId without forecastId init to fetch Accouts cogsBalance
55
67
  sectionId: 'cogs',
56
68
  });
57
69
  const grossMarginBalances = getSectionBalances(sectionsState, {
58
- reportId: reportIdUniqueKey(), // reportId without forecastId init to fetch Accouts cogsBalance
70
+ reportId: reportIdUniqueKey(undefined, useCustomRange), // reportId without forecastId init to fetch Accouts cogsBalance
59
71
  sectionId: 'grossMargin',
60
72
  });
61
73
  const incomeOrCogs = incomeBalance != null
@@ -97,6 +109,8 @@ export const getRevenueReport = (filter, accountState, accountGroupState, sectio
97
109
  allTimeframeTicks,
98
110
  insights: revenueWithForecastState.revenue.insights ?? [], // Picking insights from RevenueState in both foregast mode on and off
99
111
  availableNumberOfPeriods: getNumberOfPeriods(revState.firstMonthOfFY, filter.timeframe, revState.dataAvailable),
112
+ // Surface the saved custom timeframe so the detail picker can restore them.
113
+ customTimeframe: revenueWithForecastState.uiState.customTimeframe,
100
114
  };
101
115
  };
102
116
  export const getRevenueUIState = createSelector((state) => state.revenueState, (state) => state.forecastListState, (revenueWithForecastState, forecastListState) => {
@@ -153,7 +167,10 @@ export const getRevenueForSelectedRange = createSelector((state) => state.accoun
153
167
  selectedForecast?.id != null &&
154
168
  getRevenueReportFetchState(selectedForecast.id, revenueWithForecastState).fetchState === 'Completed'
155
169
  ? getRevenueWithForecastOnDataAvailableDateRange(noFilter, selectedForecast?.id, accountState, accountGroupState, sectionAccountsViewState, forecastListState, revenueWithForecastState, forecastState)
156
- : getRevenueReport(noFilter, accountState, accountGroupState, sectionAccountsViewState, forecastListState, revenueWithForecastState, forecastState, selectedForecast?.id);
170
+ : getRevenueReport(noFilter, accountState, accountGroupState, sectionAccountsViewState, forecastListState, revenueWithForecastState, forecastState, selectedForecast?.id,
171
+ // Detail selectors read the custom slot when the detail is in custom
172
+ // mode; the dashboard's getRevenueForLast4Periods does not.
173
+ revenueWithForecastState.uiState.customTimeframe != null);
157
174
  });
158
175
  export const getRevenueForHighlightedRange = createSelector((state) => state.accountGroupState, (state) => state.accountState, (state) => state.revenueState, (state) => state.forecastState, (state) => state.forecastListState, (state) => state.sectionAccountsViewState, (accountGroupState, accountState, revenueWithForecastState, forecastState, forecastListState, sectionsState) => {
159
176
  const { timeframe, highlightedCOABalancesRange, selectedAdditionalBalances, maxNumOfPeriodsToHighlight, isForecastModeOn, selectedForecast, } = revenueWithForecastState.uiState;
@@ -164,7 +181,10 @@ export const getRevenueForHighlightedRange = createSelector((state) => state.acc
164
181
  getRevenueReportFetchState(selectedForecast.id, revenueWithForecastState)
165
182
  .fetchState === 'Completed'
166
183
  ? getRevenueWithForecastOnDataAvailableDateRange(filter, selectedForecast?.id, accountState, accountGroupState, sectionsState, forecastListState, revenueWithForecastState, forecastState)
167
- : getRevenueReport(filter, accountState, accountGroupState, sectionsState, forecastListState, revenueWithForecastState, forecastState, selectedForecast?.id);
184
+ : getRevenueReport(filter, accountState, accountGroupState, sectionsState, forecastListState, revenueWithForecastState, forecastState, selectedForecast?.id,
185
+ // Detail selectors read the custom slot when the detail is in custom
186
+ // mode; the dashboard's getRevenueForLast4Periods does not.
187
+ revenueWithForecastState.uiState.customTimeframe != null);
168
188
  });
169
189
  /**
170
190
  * Get Revenue with forecast data overlaid on actuals.
@@ -24,6 +24,10 @@ const fetchBalanceSheetForTimeframeEpic = (actions$, _, zeniAPI) => actions$.pip
24
24
  is_full_response: action.payload.fetchFullReport,
25
25
  time_series_groups: [getTimeSeriesGroup(action.payload.timeframe)],
26
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
+ }
27
31
  return zeniAPI
28
32
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/balance_sheet?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
29
33
  .pipe((0, operators_1.mergeMap)((response) => {
@@ -5,10 +5,12 @@ import { BalanceSheetState } from './balanceSheetState';
5
5
  export declare const initialState: BalanceSheetState;
6
6
  export declare const fetchBalanceSheet: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[cacheOverride?: boolean | undefined], {
7
7
  cacheOverride: boolean;
8
- }, "balanceSheet/fetchBalanceSheet", never, never>, fetchBalanceSheetForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[timeframe: "month" | "year" | "quarter", fetchFullReport?: boolean | undefined, cacheOverride?: boolean | undefined], {
8
+ }, "balanceSheet/fetchBalanceSheet", never, never>, fetchBalanceSheetForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[timeframe: "month" | "year" | "quarter", fetchFullReport?: boolean | undefined, cacheOverride?: boolean | undefined, startDate?: string | undefined, endDate?: string | undefined], {
9
9
  timeframe: "month" | "year" | "quarter";
10
10
  fetchFullReport: boolean;
11
11
  cacheOverride: boolean;
12
+ startDate: string | undefined;
13
+ endDate: string | undefined;
12
14
  }, "balanceSheet/fetchBalanceSheetForTimeframe", never, never>, updateBalanceSheetForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
13
15
  report: ZeniReportData<BalanceSheetReportPayload>;
14
16
  timeframe: "month" | "quarter" | "year";
@@ -49,9 +49,15 @@ const balanceSheet = (0, toolkit_1.createSlice)({
49
49
  },
50
50
  },
51
51
  fetchBalanceSheetForTimeframe: {
52
- prepare(timeframe, fetchFullReport = true, cacheOverride = false) {
52
+ prepare(timeframe, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
53
53
  return {
54
- payload: { timeframe, fetchFullReport, cacheOverride },
54
+ payload: {
55
+ timeframe,
56
+ fetchFullReport,
57
+ cacheOverride,
58
+ startDate,
59
+ endDate,
60
+ },
55
61
  };
56
62
  },
57
63
  reducer(draft, action) {
@@ -24,6 +24,10 @@ const fetchCashFlowForTimeframeEpic = (actions$, _, zeniAPI) => actions$.pipe((0
24
24
  is_full_response: action.payload.fetchFullReport,
25
25
  time_series_groups: [getTimeSeriesGroup(action.payload.timeframe)],
26
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
+ }
27
31
  return zeniAPI
28
32
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/cash_flow?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
29
33
  .pipe((0, operators_1.mergeMap)((response) => {
@@ -6,9 +6,11 @@ export declare const initialState: CashFlowState;
6
6
  export declare const fetchCashFlow: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[fetchFullReport?: boolean | undefined, cacheOverride?: boolean | undefined], {
7
7
  fetchFullReport: boolean;
8
8
  cacheOverride: boolean;
9
- }, "cashFlow/fetchCashFlow", never, never>, fetchCashFlowForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[timeframe: "month" | "year" | "quarter", fetchFullReport?: boolean | undefined], {
9
+ }, "cashFlow/fetchCashFlow", never, never>, fetchCashFlowForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[timeframe: "month" | "year" | "quarter", fetchFullReport?: boolean | undefined, startDate?: string | undefined, endDate?: string | undefined], {
10
10
  timeframe: "month" | "year" | "quarter";
11
11
  fetchFullReport: boolean;
12
+ startDate: string | undefined;
13
+ endDate: string | undefined;
12
14
  }, "cashFlow/fetchCashFlowForTimeframe", never, never>, updateCashFlowForTimeframe: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
13
15
  report: ZeniReportData<CashFlowPayload>;
14
16
  timeframe: "month" | "quarter" | "year";
@@ -47,9 +47,9 @@ const cashFlow = (0, toolkit_1.createSlice)({
47
47
  },
48
48
  },
49
49
  fetchCashFlowForTimeframe: {
50
- prepare(timeframe, fetchFullReport = true) {
50
+ prepare(timeframe, fetchFullReport = true, startDate, endDate) {
51
51
  return {
52
- payload: { timeframe, fetchFullReport },
52
+ payload: { timeframe, fetchFullReport, startDate, endDate },
53
53
  };
54
54
  },
55
55
  reducer(draft, action) {
@@ -10,10 +10,16 @@ export declare const fetchCashInCashOutEpic: (actions$: ActionsObservable<Action
10
10
  classesViewParentId: import("../../commonStateTypes/classesView/nestedClassID").ClassesViewParentID | undefined;
11
11
  updateType: import("../..").UpdateType;
12
12
  }, never> | {
13
- payload: import("../../responsePayload").ZeniReportData<import("./cashInCashOutPayload").CashInCashOutPayload>;
13
+ payload: {
14
+ data: import("../../responsePayload").ZeniReportData<import("./cashInCashOutPayload").CashInCashOutPayload>;
15
+ isCustomDateRange: boolean;
16
+ };
14
17
  type: "cashInCashOut/updateCashInCashOut";
15
18
  } | {
16
- payload: import("../../zeniAPIStatus").ZeniAPIStatus<Record<string, unknown>>;
19
+ payload: {
20
+ error: import("../../zeniAPIStatus").ZeniAPIStatus<Record<string, unknown>>;
21
+ isCustomDateRange: boolean;
22
+ };
17
23
  type: "cashInCashOut/updateCashInCashOutFailure";
18
24
  } | import("@reduxjs/toolkit").PayloadAction<import("../../entity/accountGroup/accountGroupPayload").AccountGroupPayload[], "accountGroup/updateAccountGroups", {
19
25
  reportId: import("../..").ReportIDPlusForecastID;
@@ -7,18 +7,26 @@ const accountReducer_1 = require("../../entity/account/accountReducer");
7
7
  const accountGroupReducer_1 = require("../../entity/accountGroup/accountGroupReducer");
8
8
  const responsePayload_1 = require("../../responsePayload");
9
9
  const cashInCashOutReducer_1 = require("./cashInCashOutReducer");
10
- const fetchCashInCashOutEpic = (actions$, state, zeniAPI) => actions$.pipe((0, operators_1.filter)(cashInCashOutReducer_1.fetchCashInCashOut.match), (0, operators_1.switchMap)(() => zeniAPI
11
- .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_in_cash_out`)
12
- .pipe((0, operators_1.mergeMap)((response) => {
13
- if ((0, responsePayload_1.isSuccessResponse)(response)) {
14
- return (0, rxjs_1.of)((0, accountReducer_1.updateAccounts)(response.data?.report.accounts ?? [], 'cash_in_cash_out'), (0, accountGroupReducer_1.updateAccountGroups)(response.data?.report.account_groups ?? [], 'cash_in_cash_out'), (0, cashInCashOutReducer_1.updateCashInCashOut)(response.data), // eslint-disable-line @typescript-eslint/no-non-null-assertion
15
- (0, cashInCashOutReducer_1.updateCashInCashOutThisPeriod)({
16
- thisPeriod: state.value.reportUIOptionsState.thisPeriod,
17
- }));
18
- }
19
- else {
20
- return (0, rxjs_1.of)((0, cashInCashOutReducer_1.updateCashInCashOutFailure)(response.status));
21
- }
22
- }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, cashInCashOutReducer_1.updateCashInCashOutFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected error', 'CashInCashOut REST API call errored out' +
23
- JSON.stringify(error))))))));
10
+ const fetchCashInCashOutEpic = (actions$, state, zeniAPI) => actions$.pipe((0, operators_1.filter)(cashInCashOutReducer_1.fetchCashInCashOut.match), (0, operators_1.switchMap)((action) => {
11
+ const { startDate, endDate } = action.payload;
12
+ // Route by the fetch's own dates, not a global flag: a custom fetch carries
13
+ // start/end, so its results land in the isolated custom slots and the
14
+ // dashboard's default window is untouched.
15
+ const isCustomDateRange = startDate != null && endDate != null;
16
+ const url = `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_in_cash_out${isCustomDateRange
17
+ ? `?query=${encodeURIComponent(JSON.stringify({ custom_end_date: endDate, custom_start_date: startDate }))}`
18
+ : ''}`;
19
+ return zeniAPI.getJSON(url).pipe((0, operators_1.mergeMap)((response) => {
20
+ if ((0, responsePayload_1.isSuccessResponse)(response)) {
21
+ return (0, rxjs_1.of)((0, accountReducer_1.updateAccounts)(response.data?.report.accounts ?? [], 'cash_in_cash_out'), (0, accountGroupReducer_1.updateAccountGroups)(response.data?.report.account_groups ?? [], 'cash_in_cash_out'), (0, cashInCashOutReducer_1.updateCashInCashOut)(response.data, isCustomDateRange), // eslint-disable-line @typescript-eslint/no-non-null-assertion
22
+ (0, cashInCashOutReducer_1.updateCashInCashOutThisPeriod)({
23
+ thisPeriod: state.value.reportUIOptionsState.thisPeriod,
24
+ }));
25
+ }
26
+ else {
27
+ return (0, rxjs_1.of)((0, cashInCashOutReducer_1.updateCashInCashOutFailure)(response.status, isCustomDateRange));
28
+ }
29
+ }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, cashInCashOutReducer_1.updateCashInCashOutFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected error', 'CashInCashOut REST API call errored out' +
30
+ JSON.stringify(error)), isCustomDateRange))));
31
+ }));
24
32
  exports.fetchCashInCashOutEpic = fetchCashInCashOutEpic;
@@ -9,9 +9,21 @@ export declare const defaultPeriods: {
9
9
  numberOfPeriods: number;
10
10
  };
11
11
  export declare const initialState: CashInCashOutState;
12
- export declare const fetchCashInCashOut: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"cashInCashOut/fetchCashInCashOut">, updateCashInCashOut: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniReportData<CashInCashOutPayload>, "cashInCashOut/updateCashInCashOut">, updateCashInCashOutFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "cashInCashOut/updateCashInCashOutFailure">, updateCashInCashOutCOABalancesRange: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
12
+ export declare const fetchCashInCashOut: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[startDate?: string | undefined, endDate?: string | undefined], {
13
+ endDate: string | undefined;
14
+ startDate: string | undefined;
15
+ }, "cashInCashOut/fetchCashInCashOut", never, never>, updateCashInCashOut: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[data: ZeniReportData<CashInCashOutPayload>, isCustomDateRange?: boolean | undefined], {
16
+ data: ZeniReportData<CashInCashOutPayload>;
17
+ isCustomDateRange: boolean;
18
+ }, "cashInCashOut/updateCashInCashOut", never, never>, updateCashInCashOutFailure: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[error: ZeniAPIStatus<Record<string, unknown>>, isCustomDateRange?: boolean | undefined], {
19
+ error: ZeniAPIStatus<Record<string, unknown>>;
20
+ isCustomDateRange: boolean;
21
+ }, "cashInCashOut/updateCashInCashOutFailure", never, never>, updateCashInCashOutCOABalancesRange: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
13
22
  selectedCOABalancesRange: COABalancesRange;
23
+ customEndDate?: string;
24
+ customStartDate?: string;
14
25
  highlightedCOABalancesRange?: COABalancesRangeWithThisPeriod;
26
+ isCustomDateRange?: boolean;
15
27
  }, "cashInCashOut/updateCashInCashOutCOABalancesRange">, updateCashInCashOutAdditionalBalancesSelection: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
16
28
  additionalBalances: COABalanceType[];
17
29
  isCompareModeOn: boolean;