@zeniai/client-epic-state 5.2.93-betaNB2 → 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.
- package/lib/commonStateTypes/timeframeTick.d.ts +1 -0
- package/lib/commonStateTypes/timeframeTick.js +1 -0
- package/lib/commonStateTypes/viewAndReport/reportUIState.d.ts +2 -0
- package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -0
- package/lib/entity/reconciliationReport/reconciliationReportPayload.d.ts +0 -1
- package/lib/entity/reconciliationReport/reconciliationReportReducer.js +0 -1
- package/lib/entity/reconciliationReport/reconciliationReportState.d.ts +2 -4
- package/lib/entity/reconciliationReport/reconciliationReportState.js +1 -4
- package/lib/esm/commonStateTypes/timeframeTick.js +1 -0
- package/lib/esm/entity/reconciliationReport/reconciliationReportReducer.js +0 -1
- package/lib/esm/entity/reconciliationReport/reconciliationReportState.js +1 -4
- package/lib/esm/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
- package/lib/esm/view/balanceSheet/balanceSheetReducer.js +8 -2
- package/lib/esm/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
- package/lib/esm/view/cashFlow/cashFlowReducer.js +2 -2
- package/lib/esm/view/cashInCashOut/cashInCashOutEpic.js +22 -14
- package/lib/esm/view/cashInCashOut/cashInCashOutReducer.js +161 -41
- package/lib/esm/view/cashInCashOut/cashInCashOutSelector.js +42 -15
- package/lib/esm/view/cashPosition/cashPositionEpic.js +21 -13
- package/lib/esm/view/cashPosition/cashPositionReducer.js +155 -44
- package/lib/esm/view/cashPosition/cashPositionSelector.js +28 -9
- package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
- package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +25 -10
- package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
- package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
- package/lib/esm/view/opEx/opExEpic.js +7 -7
- package/lib/esm/view/opEx/opExForTimeFrameEpic.js +21 -6
- package/lib/esm/view/opEx/opExReducer.js +169 -32
- package/lib/esm/view/opEx/opExSelector.js +28 -10
- package/lib/esm/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
- package/lib/esm/view/profitAndLoss/profitAndLossReducer.js +9 -2
- package/lib/esm/view/revenue/revenueEpic.js +7 -7
- package/lib/esm/view/revenue/revenueForTimeframeEpic.js +23 -8
- package/lib/esm/view/revenue/revenueReducer.js +183 -40
- package/lib/esm/view/revenue/revenueSelector.js +30 -10
- package/lib/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
- package/lib/view/balanceSheet/balanceSheetReducer.d.ts +3 -1
- package/lib/view/balanceSheet/balanceSheetReducer.js +8 -2
- package/lib/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
- package/lib/view/cashFlow/cashFlowReducer.d.ts +3 -1
- package/lib/view/cashFlow/cashFlowReducer.js +2 -2
- package/lib/view/cashInCashOut/cashInCashOutEpic.d.ts +8 -2
- package/lib/view/cashInCashOut/cashInCashOutEpic.js +22 -14
- package/lib/view/cashInCashOut/cashInCashOutReducer.d.ts +13 -1
- package/lib/view/cashInCashOut/cashInCashOutReducer.js +161 -41
- package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +2 -1
- package/lib/view/cashInCashOut/cashInCashOutSelector.js +42 -15
- package/lib/view/cashInCashOut/cashInCashOutState.d.ts +5 -1
- package/lib/view/cashPosition/cashPositionEpic.d.ts +8 -2
- package/lib/view/cashPosition/cashPositionEpic.js +21 -13
- package/lib/view/cashPosition/cashPositionReducer.d.ts +13 -2
- package/lib/view/cashPosition/cashPositionReducer.js +155 -44
- package/lib/view/cashPosition/cashPositionSelector.d.ts +2 -1
- package/lib/view/cashPosition/cashPositionSelector.js +28 -9
- package/lib/view/cashPosition/cashPositionState.d.ts +5 -1
- package/lib/view/financeStatement/financeStatementEpic.d.ts +6 -0
- package/lib/view/netBurnOrIncome/fetchNetBurnOrIncomeWithForecastEpic.d.ts +1 -0
- package/lib/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
- package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.d.ts +2 -1
- package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +26 -11
- package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.d.ts +12 -3
- package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
- package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.d.ts +1 -1
- package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
- package/lib/view/netBurnOrIncome/netBurnOrIncomeState.d.ts +8 -0
- package/lib/view/opEx/fetchOpExWithForecastEpic.d.ts +1 -0
- package/lib/view/opEx/opExEpic.js +7 -7
- package/lib/view/opEx/opExForTimeFrameEpic.d.ts +2 -1
- package/lib/view/opEx/opExForTimeFrameEpic.js +22 -7
- package/lib/view/opEx/opExReducer.d.ts +12 -3
- package/lib/view/opEx/opExReducer.js +168 -31
- package/lib/view/opEx/opExSelector.d.ts +1 -1
- package/lib/view/opEx/opExSelector.js +28 -10
- package/lib/view/opEx/opExState.d.ts +6 -0
- package/lib/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
- package/lib/view/profitAndLoss/profitAndLossReducer.d.ts +3 -1
- package/lib/view/profitAndLoss/profitAndLossReducer.js +9 -2
- package/lib/view/reportUIOptions/updateReportUIOptionCOABalancesRangeEpic.d.ts +15 -0
- package/lib/view/revenue/fetchRevenueWithForecastEpic.d.ts +1 -0
- package/lib/view/revenue/revenueEpic.js +7 -7
- package/lib/view/revenue/revenueForTimeframeEpic.d.ts +2 -1
- package/lib/view/revenue/revenueForTimeframeEpic.js +24 -9
- package/lib/view/revenue/revenueReducer.d.ts +12 -3
- package/lib/view/revenue/revenueReducer.js +182 -39
- package/lib/view/revenue/revenueSelector.d.ts +1 -1
- package/lib/view/revenue/revenueSelector.js +30 -10
- package/lib/view/revenue/revenueState.d.ts +8 -0
- 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
|
export const defaultPeriods = {
|
|
11
11
|
numberOfPeriods: 8,
|
|
@@ -65,8 +65,16 @@ const operatingExpenses = createSlice({
|
|
|
65
65
|
initialState,
|
|
66
66
|
reducers: {
|
|
67
67
|
fetchOpEx: {
|
|
68
|
-
prepare(fetchFullReport = false, forecastId, cacheOverride = false) {
|
|
69
|
-
return {
|
|
68
|
+
prepare(fetchFullReport = false, forecastId, cacheOverride = false, startDate, endDate) {
|
|
69
|
+
return {
|
|
70
|
+
payload: {
|
|
71
|
+
fetchFullReport,
|
|
72
|
+
forecastId,
|
|
73
|
+
cacheOverride,
|
|
74
|
+
startDate,
|
|
75
|
+
endDate,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
70
78
|
},
|
|
71
79
|
reducer(
|
|
72
80
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -77,22 +85,49 @@ const operatingExpenses = createSlice({
|
|
|
77
85
|
},
|
|
78
86
|
},
|
|
79
87
|
fetchOpExReportForTimeframe: {
|
|
80
|
-
prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false) {
|
|
88
|
+
prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
|
|
81
89
|
return {
|
|
82
|
-
payload: {
|
|
90
|
+
payload: {
|
|
91
|
+
forecastId,
|
|
92
|
+
timeframe,
|
|
93
|
+
fetchFullReport,
|
|
94
|
+
cacheOverride,
|
|
95
|
+
startDate,
|
|
96
|
+
endDate,
|
|
97
|
+
},
|
|
83
98
|
};
|
|
84
99
|
},
|
|
85
100
|
reducer(draft, action) {
|
|
86
101
|
const timeframe = action.payload.timeframe;
|
|
87
102
|
const opExReport = getState(draft, draft.uiState.isForecastModeOn, action.payload.forecastId);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
// Route by the fetch's own dates, not the global flag: a custom fetch
|
|
104
|
+
// (carries start/end) marks customFetchState so the shared fetchState the
|
|
105
|
+
// dashboard reads is never left In-Progress by a detail-screen fetch.
|
|
106
|
+
const isCustomDateRange = action.payload.startDate != null && action.payload.endDate != null;
|
|
107
|
+
if (isCustomDateRange) {
|
|
108
|
+
const customFetchState = {
|
|
109
|
+
...(opExReport.customFetchState ?? {
|
|
110
|
+
month: { fetchState: 'Not-Started', error: undefined },
|
|
111
|
+
quarter: { fetchState: 'Not-Started', error: undefined },
|
|
112
|
+
year: { fetchState: 'Not-Started', error: undefined },
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
customFetchState[timeframe] = {
|
|
116
|
+
fetchState: 'In-Progress',
|
|
117
|
+
error: undefined,
|
|
118
|
+
};
|
|
119
|
+
updateState(draft, action.payload.forecastId, { customFetchState });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const fetchState = {
|
|
123
|
+
...opExReport.fetchState,
|
|
124
|
+
};
|
|
125
|
+
fetchState[timeframe] = {
|
|
126
|
+
fetchState: 'In-Progress',
|
|
127
|
+
error: undefined,
|
|
128
|
+
};
|
|
129
|
+
updateState(draft, action.payload.forecastId, { fetchState });
|
|
130
|
+
}
|
|
96
131
|
},
|
|
97
132
|
},
|
|
98
133
|
fetchOpExWithForecast: {
|
|
@@ -122,25 +157,44 @@ const operatingExpenses = createSlice({
|
|
|
122
157
|
},
|
|
123
158
|
},
|
|
124
159
|
updateOpExForTimeframe(draft, action) {
|
|
125
|
-
doUpdatedOpExReport(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report);
|
|
160
|
+
doUpdatedOpExReport(draft, action.payload.forecastId, action.payload.timeframe, action.payload.report, action.payload.isCustomDateRange === true);
|
|
126
161
|
},
|
|
127
162
|
updateOpExFailureForTimeframe: {
|
|
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 timeframe = action.payload.timeframe;
|
|
139
175
|
const opExReport = getState(draft, draft.uiState.isForecastModeOn, draft.uiState.selectedForecast?.id);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
// Route the error like the request/success: a custom (dated) fetch's
|
|
177
|
+
// failure lands in customFetchState so the shared default the dashboard
|
|
178
|
+
// reads isn't marked Error, and the detail's customFetchState isn't left
|
|
179
|
+
// stuck In-Progress.
|
|
180
|
+
if (action.payload.isCustomDateRange === true) {
|
|
181
|
+
const customFetchState = opExReport.customFetchState ?? {
|
|
182
|
+
month: { fetchState: 'Not-Started', error: undefined },
|
|
183
|
+
quarter: { fetchState: 'Not-Started', error: undefined },
|
|
184
|
+
year: { fetchState: 'Not-Started', error: undefined },
|
|
185
|
+
};
|
|
186
|
+
customFetchState[timeframe] = {
|
|
187
|
+
fetchState: 'Error',
|
|
188
|
+
error: action.payload.error,
|
|
189
|
+
};
|
|
190
|
+
opExReport.customFetchState = customFetchState;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
opExReport.fetchState[timeframe] = {
|
|
194
|
+
fetchState: 'Error',
|
|
195
|
+
error: action.payload.error,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
144
198
|
updateState(draft, action.payload.forecastId, opExReport);
|
|
145
199
|
},
|
|
146
200
|
},
|
|
@@ -158,8 +212,12 @@ const operatingExpenses = createSlice({
|
|
|
158
212
|
action.payload.additionalBalances.length != 0
|
|
159
213
|
? true
|
|
160
214
|
: false;
|
|
161
|
-
// Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
|
|
162
|
-
|
|
215
|
+
// Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON.
|
|
216
|
+
// Also skip when a custom date range is driving the window — otherwise the
|
|
217
|
+
// auto-window re-derivation below clamps the user's range to the default.
|
|
218
|
+
if (dataAvailable != null &&
|
|
219
|
+
draft.uiState.isForecastModeOn === false &&
|
|
220
|
+
draft.uiState.customTimeframe == null) {
|
|
163
221
|
const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(opExReport.firstMonthOfFY, draft.uiState.timeframe, [opExBalancesByTimeframe], dataAvailable);
|
|
164
222
|
const thisPeriod = extractThisPeriod(opExReport.firstMonthOfFY, draft.uiState.timeframe, draft.uiState.selectedCOABalancesRange, coaBalances);
|
|
165
223
|
if (thisPeriod != null &&
|
|
@@ -188,6 +246,48 @@ const operatingExpenses = createSlice({
|
|
|
188
246
|
}
|
|
189
247
|
},
|
|
190
248
|
updateOpExCOABalancesRange(draft, action) {
|
|
249
|
+
if (action.payload.isCustomDateRange != null) {
|
|
250
|
+
if (action.payload.isCustomDateRange === true) {
|
|
251
|
+
// Convert the string dates the action carries to ZeniDate values and
|
|
252
|
+
// store as a single customTimeframe. Presence signals custom-mode active.
|
|
253
|
+
draft.uiState.customTimeframe =
|
|
254
|
+
action.payload.customStartDate != null &&
|
|
255
|
+
action.payload.customEndDate != null
|
|
256
|
+
? {
|
|
257
|
+
startDate: date(action.payload.customStartDate),
|
|
258
|
+
endDate: date(action.payload.customEndDate),
|
|
259
|
+
}
|
|
260
|
+
: undefined;
|
|
261
|
+
// Invalidate the shared default-window fetchState so dashboardEpic
|
|
262
|
+
// refetches the default data on the next dashboard visit — the custom
|
|
263
|
+
// fetch writes into the shared section store, and this lets the card
|
|
264
|
+
// self-heal without a reload. The detail screen reads customFetchState.
|
|
265
|
+
draft.opEx.fetchState = {
|
|
266
|
+
month: { fetchState: 'Not-Started', error: undefined },
|
|
267
|
+
quarter: { fetchState: 'Not-Started', error: undefined },
|
|
268
|
+
year: { fetchState: 'Not-Started', error: undefined },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
draft.uiState.customTimeframe = undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// A custom date range drives the visible window directly: honor the exact
|
|
276
|
+
// numberOfPeriods + thisPeriod from the payload instead of re-deriving
|
|
277
|
+
// (which clamps the range to the available/non-zero periods).
|
|
278
|
+
if (draft.uiState.customTimeframe != null) {
|
|
279
|
+
draft.uiState.selectedCOABalancesRange =
|
|
280
|
+
action.payload.selectedCOABalancesRange;
|
|
281
|
+
if (action.payload.highlightedCOABalancesRange != null) {
|
|
282
|
+
// Clamp the highlight to the same cap the non-custom path enforces so
|
|
283
|
+
// a wide custom range doesn't highlight all N periods at once.
|
|
284
|
+
draft.uiState.highlightedCOABalancesRange = {
|
|
285
|
+
...action.payload.highlightedCOABalancesRange,
|
|
286
|
+
numberOfPeriods: Math.min(action.payload.highlightedCOABalancesRange.numberOfPeriods, draft.uiState.maxNumOfPeriodsToHighlight),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
191
291
|
// Note: When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
|
|
192
292
|
const opExReport = getState(draft, undefined, undefined);
|
|
193
293
|
const { opExBalancesByTimeframe, dataAvailable } = opExReport;
|
|
@@ -232,6 +332,13 @@ const operatingExpenses = createSlice({
|
|
|
232
332
|
}
|
|
233
333
|
},
|
|
234
334
|
updateOpExThisPeriod(draft, action) {
|
|
335
|
+
// In custom-date mode the picker drives the window; skip re-deriving the
|
|
336
|
+
// selected/highlighted ranges from the default dataAvailable (the epic
|
|
337
|
+
// re-dispatches the default thisPeriod on every month response, which
|
|
338
|
+
// would otherwise clamp the custom window back to the default).
|
|
339
|
+
if (draft.uiState.customTimeframe != null) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
235
342
|
// Note: When thisPeriod changes the selected and highlighted balances ranges should be calculated based on full report period.
|
|
236
343
|
const opExReport = getState(draft, undefined, undefined);
|
|
237
344
|
const { opExBalancesByTimeframe, dataAvailable } = opExReport;
|
|
@@ -310,7 +417,7 @@ const operatingExpenses = createSlice({
|
|
|
310
417
|
draft.uiState.maxNumOfPeriodsToHighlight =
|
|
311
418
|
initialState.uiState.maxNumOfPeriodsToHighlight;
|
|
312
419
|
}
|
|
313
|
-
if (dataAvailable != null) {
|
|
420
|
+
if (dataAvailable != null && draft.uiState.customTimeframe == null) {
|
|
314
421
|
const timeframe = draft.uiState.timeframe;
|
|
315
422
|
const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(opExReport.firstMonthOfFY, timeframe, [opExBalancesByTimeframe], dataAvailable);
|
|
316
423
|
const selectionRanges = getInitialSelectedAndHighlightedRanges({
|
|
@@ -338,27 +445,57 @@ const operatingExpenses = createSlice({
|
|
|
338
445
|
});
|
|
339
446
|
export const { fetchOpEx, fetchOpExReportForTimeframe, updateOpExForTimeframe, updateOpExFailureForTimeframe, updateOpExCOABalancesRange, updateOpExAdditionalBalancesSelection, updateOpExThisPeriod, updateOpExDownloadState, updateOpExUIState, opExToggleForecastMode, fetchOpExWithForecast, waitForForecastListThenFetchOpExWithForecast, updateOpExTimeFrame, } = operatingExpenses.actions;
|
|
340
447
|
export default operatingExpenses.reducer;
|
|
341
|
-
const doUpdatedOpExReport = (draft, forecastId, timeframe, data) => {
|
|
448
|
+
const doUpdatedOpExReport = (draft, forecastId, timeframe, data, isCustomDateRange) => {
|
|
342
449
|
const report = mapReportPayloadV2ToReportV2(data.report);
|
|
343
450
|
const opExReport = getState(draft, draft.uiState.isForecastModeOn, forecastId);
|
|
344
451
|
const { currency, expenses, insights } = data.report;
|
|
345
452
|
const { firstMonthOfFY, bookCloseDate, dataAvailable } = report;
|
|
346
453
|
const expensesCOABalance = mapCOABalanceGroupedByPeriodPayloadV2ToCOABalanceGroupedByPeriod(expenses, currency, timeframe);
|
|
347
|
-
|
|
348
|
-
|
|
454
|
+
// Route custom-date results (identified by the fetch's own dates, not the
|
|
455
|
+
// global flag) to the separate slot so the dashboard card (which reads
|
|
456
|
+
// opExBalancesByTimeframe) keeps the default window.
|
|
457
|
+
if (isCustomDateRange) {
|
|
458
|
+
opExReport.customOpExBalancesByTimeframe = Object.assign({}, opExReport.customOpExBalancesByTimeframe, expensesCOABalance);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
opExReport.opExBalancesByTimeframe = Object.assign({}, opExReport.opExBalancesByTimeframe, expensesCOABalance);
|
|
462
|
+
}
|
|
463
|
+
// Insights are shared (read by the dashboard card); a custom slice must not
|
|
464
|
+
// overwrite them.
|
|
465
|
+
if (timeframe === 'month' && insights != null && !isCustomDateRange) {
|
|
349
466
|
// we will get insights only in the month timeframe api
|
|
350
467
|
opExReport.insights = mapInsightsPayloadtoInsightsState(insights);
|
|
351
468
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
469
|
+
// Don't let a custom slice shrink the shared dataAvailable the dashboard uses
|
|
470
|
+
// for its period count; a default fetch always updates it, a custom one only
|
|
471
|
+
// populates it on first load (when nothing has set it yet).
|
|
472
|
+
if (!isCustomDateRange || opExReport.dataAvailable == null) {
|
|
473
|
+
opExReport.dataAvailable = dataAvailable
|
|
474
|
+
? dataAvailable
|
|
475
|
+
: getDataAvailableDateRange(expensesCOABalance);
|
|
476
|
+
}
|
|
355
477
|
opExReport.firstMonthOfFY = firstMonthOfFY;
|
|
356
478
|
opExReport.bookCloseDate = bookCloseDate;
|
|
357
|
-
|
|
358
|
-
|
|
479
|
+
// Mark the matching fetchState Completed. A custom fetch uses customFetchState so
|
|
480
|
+
// dashboardEpic's Completed check on the default fetchState is untouched.
|
|
481
|
+
if (isCustomDateRange) {
|
|
482
|
+
const customFetchState = opExReport.customFetchState ?? {
|
|
483
|
+
month: { fetchState: 'Not-Started', error: undefined },
|
|
484
|
+
quarter: { fetchState: 'Not-Started', error: undefined },
|
|
485
|
+
year: { fetchState: 'Not-Started', error: undefined },
|
|
486
|
+
};
|
|
487
|
+
customFetchState[timeframe] = { fetchState: 'Completed', error: undefined };
|
|
488
|
+
opExReport.customFetchState = customFetchState;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
opExReport.fetchState[timeframe].fetchState = 'Completed';
|
|
492
|
+
opExReport.fetchState[timeframe].error = undefined;
|
|
493
|
+
}
|
|
359
494
|
if (opExReport.dataAvailable != null &&
|
|
360
|
-
forecastId == null // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
|
|
361
|
-
|
|
495
|
+
forecastId == null && // Note: We don't want to update selected & highlighted ranges in Ui State when forecast is ON
|
|
496
|
+
// A custom date range drives the window; don't reset it to the default on
|
|
497
|
+
// every report receipt (this is what clamped wide ranges back to 8).
|
|
498
|
+
draft.uiState.customTimeframe == null) {
|
|
362
499
|
const timeframe = draft.uiState.timeframe;
|
|
363
500
|
const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(opExReport.firstMonthOfFY, timeframe, [expensesCOABalance], opExReport.dataAvailable);
|
|
364
501
|
const selectionRanges = getInitialSelectedAndHighlightedRanges({
|
|
@@ -58,8 +58,10 @@ export const getOperatingExpensesUIState = createSelector((state) => state.opExS
|
|
|
58
58
|
reportFormat,
|
|
59
59
|
};
|
|
60
60
|
});
|
|
61
|
-
const getOpExBalancesInTimeFrame = createSelector((state) => state.opExState, (state) => state.sectionAccountsViewState, (opExState, sectionState) => {
|
|
62
|
-
|
|
61
|
+
const getOpExBalancesInTimeFrame = createSelector((state) => state.opExState, (state) => state.sectionAccountsViewState, (_state, readCustomRangeSlot) => readCustomRangeSlot === true, (opExState, sectionState, readCustomRangeSlot) => {
|
|
62
|
+
// Custom detail reads its own section-accounts slot so a default fetch can't
|
|
63
|
+
// overwrite the partial-period data; the dashboard reads the default slot.
|
|
64
|
+
const reportId = reportIdUniqueKey(undefined, readCustomRangeSlot && opExState.uiState.customTimeframe != null);
|
|
63
65
|
const opExStateReport = getState(opExState, false, undefined);
|
|
64
66
|
if (opExStateReport.dataAvailable != null) {
|
|
65
67
|
const noFilter = newBalancesFilterForDateRange(opExStateReport.firstMonthOfFY, opExState.uiState.timeframe, opExStateReport.dataAvailable, 'ascending_date', []);
|
|
@@ -72,14 +74,25 @@ const getOpExBalancesInTimeFrame = createSelector((state) => state.opExState, (s
|
|
|
72
74
|
}
|
|
73
75
|
return [];
|
|
74
76
|
});
|
|
75
|
-
export const getOperatingExpensesReport = createSelector((state) => state, (state) => state.accountState, (state) => state.accountGroupState, (state) => state.sectionAccountsViewState, (state) => state.opExState, (state) => state.forecastState, (state) => state.forecastListState, (_state, filter) => filter, (_state, _filter, forecastId) => forecastId, (state, accountState, accountGroupState, sectionsState, opExState, forecastState, forecastListState, filter, forecastId) => {
|
|
77
|
+
export const getOperatingExpensesReport = createSelector((state) => state, (state) => state.accountState, (state) => state.accountGroupState, (state) => state.sectionAccountsViewState, (state) => state.opExState, (state) => state.forecastState, (state) => state.forecastListState, (_state, filter) => filter, (_state, _filter, forecastId) => forecastId, (_state, _filter, _forecastId, readCustomRangeSlot) => readCustomRangeSlot === true, (state, accountState, accountGroupState, sectionsState, opExState, forecastState, forecastListState, filter, forecastId, readCustomRangeSlot) => {
|
|
76
78
|
const opEx = getState(opExState, opExState.uiState.isForecastModeOn, forecastId);
|
|
79
|
+
// The detail screen opts into customFetchState so its Completed check fires
|
|
80
|
+
// off the custom fetch; the dashboard (no flag) reads the shared fetchState,
|
|
81
|
+
// which custom-mode activation reset to Not-Started so dashboardEpic refetches.
|
|
82
|
+
const activeFetchState = readCustomRangeSlot &&
|
|
83
|
+
opExState.uiState.customTimeframe != null &&
|
|
84
|
+
opEx.customFetchState != null
|
|
85
|
+
? opEx.customFetchState
|
|
86
|
+
: opEx.fetchState;
|
|
77
87
|
const fetchState = reduceAnyFetchState([
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
activeFetchState.month,
|
|
89
|
+
activeFetchState.quarter,
|
|
90
|
+
activeFetchState.year,
|
|
81
91
|
]);
|
|
82
|
-
|
|
92
|
+
// Custom detail reads the isolated custom section-accounts slot; dashboard
|
|
93
|
+
// (no flag) reads the default slot, so a default fetch can't clobber it.
|
|
94
|
+
const useCustomRange = readCustomRangeSlot && opExState.uiState.customTimeframe != null;
|
|
95
|
+
const reportId = reportIdUniqueKey(forecastId, useCustomRange);
|
|
83
96
|
const sections = {};
|
|
84
97
|
const calculatedSections = {};
|
|
85
98
|
if (fetchState.fetchState === 'Completed') {
|
|
@@ -90,7 +103,7 @@ export const getOperatingExpensesReport = createSelector((state) => state, (stat
|
|
|
90
103
|
}, filter);
|
|
91
104
|
});
|
|
92
105
|
}
|
|
93
|
-
const balancesInTimeframe = getOpExBalancesInTimeFrame(state);
|
|
106
|
+
const balancesInTimeframe = getOpExBalancesInTimeFrame(state, useCustomRange);
|
|
94
107
|
const allTimeframeTicks = balancesInTimeframe.map((balance) => toTimeframeTick(balance));
|
|
95
108
|
const forecast = opEx.forecastId != null
|
|
96
109
|
? getForecast(forecastState, opEx.forecastId)
|
|
@@ -117,6 +130,8 @@ export const getOperatingExpensesReport = createSelector((state) => state, (stat
|
|
|
117
130
|
allTimeframeTicks,
|
|
118
131
|
balancesInTimeframe,
|
|
119
132
|
availableNumberOfPeriods: getNumberOfPeriods(opEx.firstMonthOfFY, filter.timeframe, opEx.dataAvailable),
|
|
133
|
+
// Surface the saved custom timeframe so the detail picker can restore them.
|
|
134
|
+
customTimeframe: opExState.uiState.customTimeframe,
|
|
120
135
|
};
|
|
121
136
|
});
|
|
122
137
|
export const getOperatingExpensesForSelectedRange = createSelector((state) => state, (state) => state.opExState, (state, opExWithForecastState) => {
|
|
@@ -126,7 +141,10 @@ export const getOperatingExpensesForSelectedRange = createSelector((state) => st
|
|
|
126
141
|
selectedForecast?.id != null &&
|
|
127
142
|
getOperatingExpensesReportFetchState(selectedForecast.id, opExWithForecastState).fetchState === 'Completed'
|
|
128
143
|
? getOpExWithForecastOnDataAvailableDateRange(state, noFilter, selectedForecast?.id)
|
|
129
|
-
: getOperatingExpensesReport(state, noFilter, opExWithForecastState.uiState.selectedForecast?.id
|
|
144
|
+
: getOperatingExpensesReport(state, noFilter, opExWithForecastState.uiState.selectedForecast?.id,
|
|
145
|
+
// Detail selectors read the custom slot when the detail is in custom
|
|
146
|
+
// mode; the dashboard's getOperatingExpensesForLast5Periods does not.
|
|
147
|
+
opExWithForecastState.uiState.customTimeframe != null);
|
|
130
148
|
});
|
|
131
149
|
export const getOperatingExpensesForHighlightedRange = createSelector((state) => state, (state) => state.opExState, (state, opExWithForecastState) => {
|
|
132
150
|
const { timeframe, highlightedCOABalancesRange, selectedAdditionalBalances, maxNumOfPeriodsToHighlight, isForecastModeOn, selectedForecast, } = opExWithForecastState.uiState;
|
|
@@ -136,7 +154,7 @@ export const getOperatingExpensesForHighlightedRange = createSelector((state) =>
|
|
|
136
154
|
selectedForecast?.id != null &&
|
|
137
155
|
getOperatingExpensesReportFetchState(selectedForecast.id, opExWithForecastState).fetchState === 'Completed'
|
|
138
156
|
? getOpExWithForecastOnDataAvailableDateRange(state, filter, selectedForecast?.id)
|
|
139
|
-
: getOperatingExpensesReport(state, filter, opExWithForecastState.uiState.selectedForecast?.id);
|
|
157
|
+
: getOperatingExpensesReport(state, filter, opExWithForecastState.uiState.selectedForecast?.id, opExWithForecastState.uiState.customTimeframe != null);
|
|
140
158
|
});
|
|
141
159
|
/**
|
|
142
160
|
* Get Net Burn or Income with forecast data overlaid on actuals.
|
|
@@ -25,6 +25,10 @@ export const fetchProfitAndLossForTimeframeEpic = (actions$, _, zeniAPI) => acti
|
|
|
25
25
|
queryValue.forecast_id = action.payload.forecastId;
|
|
26
26
|
queryValue.is_forecast_vs_actual = true;
|
|
27
27
|
}
|
|
28
|
+
if (action.payload.startDate != null && action.payload.endDate != null) {
|
|
29
|
+
queryValue.custom_start_date = action.payload.startDate;
|
|
30
|
+
queryValue.custom_end_date = action.payload.endDate;
|
|
31
|
+
}
|
|
28
32
|
return zeniAPI
|
|
29
33
|
.getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/profit_and_loss?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
|
|
30
34
|
.pipe(mergeMap((response) => {
|
|
@@ -49,9 +49,16 @@ const profitAndLoss = createSlice({
|
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
51
|
fetchProfitAndLossForTimeframe: {
|
|
52
|
-
prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false) {
|
|
52
|
+
prepare(timeframe, forecastId, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
|
|
53
53
|
return {
|
|
54
|
-
payload: {
|
|
54
|
+
payload: {
|
|
55
|
+
forecastId,
|
|
56
|
+
timeframe,
|
|
57
|
+
fetchFullReport,
|
|
58
|
+
cacheOverride,
|
|
59
|
+
startDate,
|
|
60
|
+
endDate,
|
|
61
|
+
},
|
|
55
62
|
};
|
|
56
63
|
},
|
|
57
64
|
reducer(draft, action) {
|
|
@@ -2,12 +2,12 @@ import { from } from 'rxjs';
|
|
|
2
2
|
import { filter, switchMap } from 'rxjs/operators';
|
|
3
3
|
import { fetchRevenue, fetchRevenueForTimeframe, getState, } from './revenueReducer';
|
|
4
4
|
export const fetchRevenueEpic = (actions$, state) => actions$.pipe(filter(fetchRevenue.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
|
-
fetchRevenueForTimeframe('month', forecastId),
|
|
9
|
-
fetchRevenueForTimeframe('quarter', forecastId),
|
|
10
|
-
fetchRevenueForTimeframe('year', forecastId),
|
|
8
|
+
fetchRevenueForTimeframe('month', forecastId, true, false, startDate, endDate),
|
|
9
|
+
fetchRevenueForTimeframe('quarter', forecastId, true, false, startDate, endDate),
|
|
10
|
+
fetchRevenueForTimeframe('year', forecastId, true, false, startDate, endDate),
|
|
11
11
|
]);
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
@@ -16,15 +16,15 @@ export const fetchRevenueEpic = (actions$, state) => actions$.pipe(filter(fetchR
|
|
|
16
16
|
const timeframeFetchState = revState.fetchState;
|
|
17
17
|
if (timeframeFetchState['month'].fetchState !== 'In-Progress' &&
|
|
18
18
|
timeframeFetchState['month'].fetchState !== 'Completed') {
|
|
19
|
-
fetchActions.push(fetchRevenueForTimeframe('month', forecastId));
|
|
19
|
+
fetchActions.push(fetchRevenueForTimeframe('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(fetchRevenueForTimeframe('quarter', forecastId));
|
|
23
|
+
fetchActions.push(fetchRevenueForTimeframe('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(fetchRevenueForTimeframe('year', forecastId));
|
|
27
|
+
fetchActions.push(fetchRevenueForTimeframe('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 { fetchRevenueForTimeframe, updateRevenueForTimeframe, updateRevenueForTimeframeFailure, updateRevenueThisPeriod, } from './revenueReducer';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Reserved forecast-slot marker so custom-date section-accounts data lives under
|
|
9
|
+
// its own key (revenue+<marker>) and a default fetch can never overwrite it.
|
|
10
|
+
// 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
|
+
? `revenue+${CUSTOM_DATE_RANGE_SECTION_MARKER}`
|
|
14
|
+
: forecastId != null
|
|
15
|
+
? `revenue+${forecastId}`
|
|
16
|
+
: 'revenue';
|
|
11
17
|
export const fetchRevenueForTimeframeEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchRevenueForTimeframe.match), mergeMap((action) => {
|
|
12
18
|
const queryValue = {
|
|
13
19
|
is_full_response: action.payload.fetchFullReport,
|
|
@@ -18,18 +24,25 @@ export const fetchRevenueForTimeframeEpic = (actions$, state, zeniAPI) => action
|
|
|
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/revenue?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
|
|
23
33
|
.pipe(mergeMap((response) => {
|
|
24
34
|
if (response.data != null && isSuccessResponse(response)) {
|
|
25
35
|
const actions = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
actions.push(
|
|
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, 'income', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.income, response.data.report.accounts, response.data.report.account_groups, response.data.report.currency));
|
|
40
|
+
actions.push(updateCalculatedSectionAccountsView(action.payload.timeframe, 'grossMargin', reportIdUniqueKey(action.payload.forecastId, isCustomDateRange), response.data.report.gross_profit, response.data.report.currency));
|
|
29
41
|
actions.push(updateRevenueForTimeframe({
|
|
30
42
|
timeframe: action.payload.timeframe,
|
|
31
43
|
forecastId: action.payload.forecastId,
|
|
32
44
|
report: response.data,
|
|
45
|
+
isCustomDateRange,
|
|
33
46
|
}));
|
|
34
47
|
if (action.payload.forecastId == null &&
|
|
35
48
|
action.payload.timeframe === 'month') {
|
|
@@ -44,7 +57,9 @@ export const fetchRevenueForTimeframeEpic = (actions$, state, zeniAPI) => action
|
|
|
44
57
|
return from(actions);
|
|
45
58
|
}
|
|
46
59
|
else {
|
|
47
|
-
return of(updateRevenueForTimeframeFailure(response.status, action.payload.forecastId, action.payload.timeframe
|
|
60
|
+
return of(updateRevenueForTimeframeFailure(response.status, action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
|
|
61
|
+
action.payload.endDate != null));
|
|
48
62
|
}
|
|
49
|
-
}), catchError((error) => of(updateRevenueForTimeframeFailure(createZeniAPIStatus('Unexpected Error', 'Revenue REST API call errored out' + JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe
|
|
63
|
+
}), catchError((error) => of(updateRevenueForTimeframeFailure(createZeniAPIStatus('Unexpected Error', 'Revenue REST API call errored out' + JSON.stringify(error)), action.payload.forecastId, action.payload.timeframe, action.payload.startDate != null &&
|
|
64
|
+
action.payload.endDate != null))));
|
|
50
65
|
}));
|