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