@zeniai/client-epic-state 5.2.93-betaNB2 → 5.2.94-betaNB1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/lib/commonStateTypes/timeframeTick.d.ts +1 -0
  2. package/lib/commonStateTypes/timeframeTick.js +1 -0
  3. package/lib/commonStateTypes/viewAndReport/reportUIState.d.ts +2 -0
  4. package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -0
  5. package/lib/esm/commonStateTypes/timeframeTick.js +1 -0
  6. package/lib/esm/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
  7. package/lib/esm/view/balanceSheet/balanceSheetReducer.js +8 -2
  8. package/lib/esm/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
  9. package/lib/esm/view/cashFlow/cashFlowReducer.js +2 -2
  10. package/lib/esm/view/cashInCashOut/cashInCashOutEpic.js +22 -14
  11. package/lib/esm/view/cashInCashOut/cashInCashOutReducer.js +161 -41
  12. package/lib/esm/view/cashInCashOut/cashInCashOutSelector.js +42 -15
  13. package/lib/esm/view/cashPosition/cashPositionEpic.js +21 -13
  14. package/lib/esm/view/cashPosition/cashPositionReducer.js +155 -44
  15. package/lib/esm/view/cashPosition/cashPositionSelector.js +28 -9
  16. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  17. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +25 -10
  18. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  19. package/lib/esm/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  20. package/lib/esm/view/opEx/opExEpic.js +7 -7
  21. package/lib/esm/view/opEx/opExForTimeFrameEpic.js +21 -6
  22. package/lib/esm/view/opEx/opExReducer.js +169 -32
  23. package/lib/esm/view/opEx/opExSelector.js +28 -10
  24. package/lib/esm/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  25. package/lib/esm/view/profitAndLoss/profitAndLossReducer.js +9 -2
  26. package/lib/esm/view/revenue/revenueEpic.js +7 -7
  27. package/lib/esm/view/revenue/revenueForTimeframeEpic.js +23 -8
  28. package/lib/esm/view/revenue/revenueReducer.js +183 -40
  29. package/lib/esm/view/revenue/revenueSelector.js +30 -10
  30. package/lib/view/balanceSheet/balanceSheetForTimeframeEpic.js +4 -0
  31. package/lib/view/balanceSheet/balanceSheetReducer.d.ts +3 -1
  32. package/lib/view/balanceSheet/balanceSheetReducer.js +8 -2
  33. package/lib/view/cashFlow/cashFlowForTimeframeEpic.js +4 -0
  34. package/lib/view/cashFlow/cashFlowReducer.d.ts +3 -1
  35. package/lib/view/cashFlow/cashFlowReducer.js +2 -2
  36. package/lib/view/cashInCashOut/cashInCashOutEpic.d.ts +8 -2
  37. package/lib/view/cashInCashOut/cashInCashOutEpic.js +22 -14
  38. package/lib/view/cashInCashOut/cashInCashOutReducer.d.ts +13 -1
  39. package/lib/view/cashInCashOut/cashInCashOutReducer.js +161 -41
  40. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +2 -1
  41. package/lib/view/cashInCashOut/cashInCashOutSelector.js +42 -15
  42. package/lib/view/cashInCashOut/cashInCashOutState.d.ts +5 -1
  43. package/lib/view/cashPosition/cashPositionEpic.d.ts +8 -2
  44. package/lib/view/cashPosition/cashPositionEpic.js +21 -13
  45. package/lib/view/cashPosition/cashPositionReducer.d.ts +13 -2
  46. package/lib/view/cashPosition/cashPositionReducer.js +155 -44
  47. package/lib/view/cashPosition/cashPositionSelector.d.ts +2 -1
  48. package/lib/view/cashPosition/cashPositionSelector.js +28 -9
  49. package/lib/view/cashPosition/cashPositionState.d.ts +5 -1
  50. package/lib/view/financeStatement/financeStatementEpic.d.ts +6 -0
  51. package/lib/view/netBurnOrIncome/fetchNetBurnOrIncomeWithForecastEpic.d.ts +1 -0
  52. package/lib/view/netBurnOrIncome/netBurnOrIncomeEpic.js +7 -7
  53. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.d.ts +2 -1
  54. package/lib/view/netBurnOrIncome/netBurnOrIncomeForTimeframeEpic.js +26 -11
  55. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.d.ts +12 -3
  56. package/lib/view/netBurnOrIncome/netBurnOrIncomeReducer.js +184 -40
  57. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.d.ts +1 -1
  58. package/lib/view/netBurnOrIncome/netBurnOrIncomeSelector.js +26 -8
  59. package/lib/view/netBurnOrIncome/netBurnOrIncomeState.d.ts +8 -0
  60. package/lib/view/opEx/fetchOpExWithForecastEpic.d.ts +1 -0
  61. package/lib/view/opEx/opExEpic.js +7 -7
  62. package/lib/view/opEx/opExForTimeFrameEpic.d.ts +2 -1
  63. package/lib/view/opEx/opExForTimeFrameEpic.js +22 -7
  64. package/lib/view/opEx/opExReducer.d.ts +12 -3
  65. package/lib/view/opEx/opExReducer.js +168 -31
  66. package/lib/view/opEx/opExSelector.d.ts +1 -1
  67. package/lib/view/opEx/opExSelector.js +28 -10
  68. package/lib/view/opEx/opExState.d.ts +6 -0
  69. package/lib/view/profitAndLoss/profitAndLossForTimeframeEpic.js +4 -0
  70. package/lib/view/profitAndLoss/profitAndLossReducer.d.ts +3 -1
  71. package/lib/view/profitAndLoss/profitAndLossReducer.js +9 -2
  72. package/lib/view/reportUIOptions/updateReportUIOptionCOABalancesRangeEpic.d.ts +15 -0
  73. package/lib/view/revenue/fetchRevenueWithForecastEpic.d.ts +1 -0
  74. package/lib/view/revenue/revenueEpic.js +7 -7
  75. package/lib/view/revenue/revenueForTimeframeEpic.d.ts +2 -1
  76. package/lib/view/revenue/revenueForTimeframeEpic.js +24 -9
  77. package/lib/view/revenue/revenueReducer.d.ts +12 -3
  78. package/lib/view/revenue/revenueReducer.js +182 -39
  79. package/lib/view/revenue/revenueSelector.d.ts +1 -1
  80. package/lib/view/revenue/revenueSelector.js +30 -10
  81. package/lib/view/revenue/revenueState.d.ts +8 -0
  82. package/package.json +1 -1
@@ -9,6 +9,7 @@ export interface TimeframeTick {
9
9
  export interface TimeframeTickWithMetaData extends TimeframeTick {
10
10
  balanceType: COABalanceType;
11
11
  currencySymbol: string;
12
+ label?: string;
12
13
  }
13
14
  export declare const toTimeframeTick: (balance: COABalance) => TimeframeTickWithMetaData;
14
15
  export declare const toTimePeriod: (tick: TimeframeTick) => TimePeriod;
@@ -8,6 +8,7 @@ const toTimeframeTick = (balance) => ({
8
8
  endDate: balance.endDate,
9
9
  currencySymbol: balance.balance.currencySymbol,
10
10
  balanceType: balance.type,
11
+ label: balance.label,
11
12
  });
12
13
  exports.toTimeframeTick = toTimeframeTick;
13
14
  const toTimePeriod = (tick) => ({
@@ -2,6 +2,7 @@ import { COABalanceMonthQuarterYearTimeframe } from '../coaBalance/coaBalance';
2
2
  import { COABalanceType } from '../coaBalance/coaBalanceType';
3
3
  import { COABalancesOrder, COABalancesRange, COABalancesRangeWithThisPeriod } from '../coaBalance/coaBalancesFilter';
4
4
  import { FetchState, ID } from '../common';
5
+ import { TimeframeTick } from '../timeframeTick';
5
6
  import { ReportFormat } from './viewAndReport';
6
7
  export interface ReportUIState {
7
8
  displayOrder: COABalancesOrder;
@@ -12,6 +13,7 @@ export interface ReportUIState {
12
13
  selectedAdditionalBalances: COABalanceType[];
13
14
  selectedCOABalancesRange: COABalancesRange;
14
15
  timeframe: COABalanceMonthQuarterYearTimeframe;
16
+ customTimeframe?: TimeframeTick;
15
17
  highlightedCOABalancesRange?: COABalancesRangeWithThisPeriod;
16
18
  isForecastModeOn?: boolean;
17
19
  selectedForecast?: {
@@ -3,6 +3,7 @@ import { FetchStateAndError, ID } from '../common';
3
3
  import { DataAvailable } from '../dataAvailable';
4
4
  import { Status } from '../status';
5
5
  import { Month } from '../timePeriod';
6
+ import { TimeframeTick } from '../timeframeTick';
6
7
  export declare const ALL_REPORT_IDS: readonly ["cash_balance", "card_balance", "expense_automation_card", "operating_expenses", "operating_expenses_by_classes", "operating_expenses_by_projects", "revenue", "revenue_by_classes", "revenue_by_projects", "net_burn_or_income", "net_burn_or_income_story_card", "net_burn_or_income_by_classes", "net_burn_or_income_by_projects", "cash_position", "top_expenses", "profit_and_loss", "profit_and_loss_by_classes", "profit_and_loss_by_projects", "balance_sheet", "balance_sheet_by_classes", "balance_sheet_by_projects", "cash_flow", "cash_flow_by_classes", "cash_flow_by_projects", "dashboard", "finance_statement", "transaction_list_of_account", "transaction_list_missing_receipt", "transaction_list_of_account_by_class", "transaction_list_of_account_by_project", "transaction_detail", "cash_in_cash_out", "insights_dashboard", "company_details", "class_list", "account_list", "account_list_by_type", "vendor", "task_card", "bill_pay", "reimbursement", "bill_pay_card", "reimbursement_card", "bill_pay_promo_card", "zeni_accounts", "zeni_accounts_promo_card", "zeni_treasury", "charge_cards", "other_connection", "reimbursement_promo_card", "onboarding", "operating_expenses_by_vendor", "operating_expenses_by_vendor_summary", "vendors_tab", "accounts_payable_aging", "accounts_receivable_aging", "vendor_global_review", "prepaid_expenses", "fixed_assets", "notification", "rewards_card", "billing", "flux_analysis_operating_expense", "account_reconciliation", "zeni_credit_promo_card", "treasury_promo_card", "excluded_account_reconciliation", "transfer_accounts", "credit_card_transfer_accounts"];
7
8
  export declare const toReportID: (v: string) => "cash_balance" | "card_balance" | "expense_automation_card" | "operating_expenses" | "operating_expenses_by_classes" | "operating_expenses_by_projects" | "revenue" | "revenue_by_classes" | "revenue_by_projects" | "net_burn_or_income" | "net_burn_or_income_story_card" | "net_burn_or_income_by_classes" | "net_burn_or_income_by_projects" | "cash_position" | "top_expenses" | "profit_and_loss" | "profit_and_loss_by_classes" | "profit_and_loss_by_projects" | "balance_sheet" | "balance_sheet_by_classes" | "balance_sheet_by_projects" | "cash_flow" | "cash_flow_by_classes" | "cash_flow_by_projects" | "dashboard" | "finance_statement" | "transaction_list_of_account" | "transaction_list_missing_receipt" | "transaction_list_of_account_by_class" | "transaction_list_of_account_by_project" | "transaction_detail" | "cash_in_cash_out" | "insights_dashboard" | "company_details" | "class_list" | "account_list" | "account_list_by_type" | "vendor" | "task_card" | "bill_pay" | "reimbursement" | "bill_pay_card" | "reimbursement_card" | "bill_pay_promo_card" | "zeni_accounts" | "zeni_accounts_promo_card" | "zeni_treasury" | "charge_cards" | "other_connection" | "reimbursement_promo_card" | "onboarding" | "operating_expenses_by_vendor" | "operating_expenses_by_vendor_summary" | "vendors_tab" | "accounts_payable_aging" | "accounts_receivable_aging" | "vendor_global_review" | "prepaid_expenses" | "fixed_assets" | "notification" | "rewards_card" | "billing" | "flux_analysis_operating_expense" | "account_reconciliation" | "zeni_credit_promo_card" | "treasury_promo_card" | "excluded_account_reconciliation" | "transfer_accounts" | "credit_card_transfer_accounts";
8
9
  export type ReportID = ReturnType<typeof toReportID>;
@@ -30,6 +31,7 @@ export interface Report extends View {
30
31
  */
31
32
  export interface SelectorReport extends Report, FetchStateAndError {
32
33
  availableNumberOfPeriods?: number;
34
+ customTimeframe?: TimeframeTick;
33
35
  [key: string]: unknown;
34
36
  }
35
37
  export type PageToken = string | null;
@@ -5,6 +5,7 @@ export const toTimeframeTick = (balance) => ({
5
5
  endDate: balance.endDate,
6
6
  currencySymbol: balance.balance.currencySymbol,
7
7
  balanceType: balance.type,
8
+ label: balance.label,
8
9
  });
9
10
  export const toTimePeriod = (tick) => ({
10
11
  start: toAbsoluteDay(tick.startDate.format('YYYY-MM-DD')), // eslint-disable-line @typescript-eslint/no-non-null-assertion
@@ -20,6 +20,10 @@ export const fetchBalanceSheetForTimeframeEpic = (actions$, _, zeniAPI) => actio
20
20
  is_full_response: action.payload.fetchFullReport,
21
21
  time_series_groups: [getTimeSeriesGroup(action.payload.timeframe)],
22
22
  };
23
+ if (action.payload.startDate != null && action.payload.endDate != null) {
24
+ queryValue.custom_start_date = action.payload.startDate;
25
+ queryValue.custom_end_date = action.payload.endDate;
26
+ }
23
27
  return zeniAPI
24
28
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/balance_sheet?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
25
29
  .pipe(mergeMap((response) => {
@@ -45,9 +45,15 @@ const balanceSheet = createSlice({
45
45
  },
46
46
  },
47
47
  fetchBalanceSheetForTimeframe: {
48
- prepare(timeframe, fetchFullReport = true, cacheOverride = false) {
48
+ prepare(timeframe, fetchFullReport = true, cacheOverride = false, startDate, endDate) {
49
49
  return {
50
- payload: { timeframe, fetchFullReport, cacheOverride },
50
+ payload: {
51
+ timeframe,
52
+ fetchFullReport,
53
+ cacheOverride,
54
+ startDate,
55
+ endDate,
56
+ },
51
57
  };
52
58
  },
53
59
  reducer(draft, action) {
@@ -20,6 +20,10 @@ export const fetchCashFlowForTimeframeEpic = (actions$, _, zeniAPI) => actions$.
20
20
  is_full_response: action.payload.fetchFullReport,
21
21
  time_series_groups: [getTimeSeriesGroup(action.payload.timeframe)],
22
22
  };
23
+ if (action.payload.startDate != null && action.payload.endDate != null) {
24
+ queryValue.custom_start_date = action.payload.startDate;
25
+ queryValue.custom_end_date = action.payload.endDate;
26
+ }
23
27
  return zeniAPI
24
28
  .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/2.0/reports/cash_flow?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
25
29
  .pipe(mergeMap((response) => {
@@ -43,9 +43,9 @@ const cashFlow = createSlice({
43
43
  },
44
44
  },
45
45
  fetchCashFlowForTimeframe: {
46
- prepare(timeframe, fetchFullReport = true) {
46
+ prepare(timeframe, fetchFullReport = true, startDate, endDate) {
47
47
  return {
48
- payload: { timeframe, fetchFullReport },
48
+ payload: { timeframe, fetchFullReport, startDate, endDate },
49
49
  };
50
50
  },
51
51
  reducer(draft, action) {
@@ -4,17 +4,25 @@ import { updateAccounts } from '../../entity/account/accountReducer';
4
4
  import { updateAccountGroups } from '../../entity/accountGroup/accountGroupReducer';
5
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
6
6
  import { fetchCashInCashOut, updateCashInCashOut, updateCashInCashOutFailure, updateCashInCashOutThisPeriod, } from './cashInCashOutReducer';
7
- export const fetchCashInCashOutEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchCashInCashOut.match), switchMap(() => zeniAPI
8
- .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_in_cash_out`)
9
- .pipe(mergeMap((response) => {
10
- if (isSuccessResponse(response)) {
11
- return of(updateAccounts(response.data?.report.accounts ?? [], 'cash_in_cash_out'), updateAccountGroups(response.data?.report.account_groups ?? [], 'cash_in_cash_out'), updateCashInCashOut(response.data), // eslint-disable-line @typescript-eslint/no-non-null-assertion
12
- updateCashInCashOutThisPeriod({
13
- thisPeriod: state.value.reportUIOptionsState.thisPeriod,
14
- }));
15
- }
16
- else {
17
- return of(updateCashInCashOutFailure(response.status));
18
- }
19
- }), catchError((error) => of(updateCashInCashOutFailure(createZeniAPIStatus('Unexpected error', 'CashInCashOut REST API call errored out' +
20
- JSON.stringify(error))))))));
7
+ export const fetchCashInCashOutEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchCashInCashOut.match), switchMap((action) => {
8
+ const { startDate, endDate } = action.payload;
9
+ // Route by the fetch's own dates, not a global flag: a custom fetch carries
10
+ // start/end, so its results land in the isolated custom slots and the
11
+ // dashboard's default window is untouched.
12
+ const isCustomDateRange = startDate != null && endDate != null;
13
+ const url = `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_in_cash_out${isCustomDateRange
14
+ ? `?query=${encodeURIComponent(JSON.stringify({ custom_end_date: endDate, custom_start_date: startDate }))}`
15
+ : ''}`;
16
+ return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
17
+ if (isSuccessResponse(response)) {
18
+ return of(updateAccounts(response.data?.report.accounts ?? [], 'cash_in_cash_out'), updateAccountGroups(response.data?.report.account_groups ?? [], 'cash_in_cash_out'), updateCashInCashOut(response.data, isCustomDateRange), // eslint-disable-line @typescript-eslint/no-non-null-assertion
19
+ updateCashInCashOutThisPeriod({
20
+ thisPeriod: state.value.reportUIOptionsState.thisPeriod,
21
+ }));
22
+ }
23
+ else {
24
+ return of(updateCashInCashOutFailure(response.status, isCustomDateRange));
25
+ }
26
+ }), catchError((error) => of(updateCashInCashOutFailure(createZeniAPIStatus('Unexpected error', 'CashInCashOut REST API call errored out' +
27
+ JSON.stringify(error)), isCustomDateRange))));
28
+ }));
@@ -5,6 +5,7 @@ import { getDataAvailableDateRange, getNumberOfPeriods, } from '../../commonStat
5
5
  import { extractThisPeriod, getMatchingThisPeriod, } from '../../commonStateTypes/viewAndReport/thisPeriodHelpers';
6
6
  import { mapInsightsPayloadtoInsightsState } from '../../entity/insights/insightSummaryPayload';
7
7
  import { clearAll } from '../../rootActions';
8
+ import { date } from '../../zeniDayJS';
8
9
  import { getAvailableCOABalancesWithMostRecentNonZeroBalance, getInitialSelectedAndHighlightedRanges, getSelectedAndHighlightedRangesForNewSelection, getSelectedAndHighlightedRangesForThisPeriod, } from '../common/getSelectedAndHighlightedRanges';
9
10
  export const defaultPeriods = {
10
11
  numberOfPeriods: 5,
@@ -15,6 +16,10 @@ export const initialState = {
15
16
  cashIn: undefined,
16
17
  cashOut: undefined,
17
18
  netCashFlow: undefined,
19
+ customCashIn: undefined,
20
+ customCashOut: undefined,
21
+ customNetCashFlow: undefined,
22
+ customFetchState: undefined,
18
23
  dataAvailable: undefined,
19
24
  firstMonthOfFY: 1,
20
25
  reportId: 'cash_in_cash_out',
@@ -49,49 +54,118 @@ const cashInCashOut = createSlice({
49
54
  name: 'cashInCashOut',
50
55
  initialState,
51
56
  reducers: {
52
- fetchCashInCashOut(draft) {
53
- draft.fetchState = 'In-Progress';
54
- draft.error = undefined;
57
+ fetchCashInCashOut: {
58
+ prepare(startDate, endDate) {
59
+ return { payload: { endDate, startDate } };
60
+ },
61
+ reducer(draft, action) {
62
+ // Route by per-fetch dates: a custom (dated) fetch marks only the
63
+ // isolated customFetchState so the shared fetchState the dashboard reads
64
+ // stays as custom-mode activation left it (self-heal), and the shared
65
+ // error isn't cleared by a detail-only fetch.
66
+ if (action.payload.startDate != null &&
67
+ action.payload.endDate != null) {
68
+ draft.customFetchState = 'In-Progress';
69
+ }
70
+ else {
71
+ draft.fetchState = 'In-Progress';
72
+ draft.error = undefined;
73
+ }
74
+ },
55
75
  },
56
- updateCashInCashOut(draft, action) {
57
- draft.fetchState = 'Completed';
58
- draft.error = undefined;
59
- const report = mapReportPayloadToReport(action.payload.report);
60
- Object.assign(draft, report);
61
- const { cash_in, cash_out, net_cash_flow, insights } = action.payload.report;
62
- draft.cashIn =
63
- mapCOABalanceGroupedWithIDsPayloadToCOABalanceGroupedWithIDs(cash_in);
64
- draft.cashOut =
65
- mapCOABalanceGroupedWithIDsPayloadToCOABalanceGroupedWithIDs(cash_out);
66
- draft.netCashFlow =
67
- mapCOABalanceGroupedPayloadToCOABalanceGrouped(net_cash_flow);
68
- draft.insights = mapInsightsPayloadtoInsightsState(insights);
69
- const cashInOrCashOut = draft.cashIn != null ? draft.cashIn : draft.cashOut;
70
- draft.dataAvailable = report.dataAvailable
71
- ? report.dataAvailable
72
- : getDataAvailableDateRange(cashInOrCashOut);
73
- if (draft.dataAvailable != null) {
74
- const timeframe = draft.uiState.timeframe;
75
- const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [draft.cashIn, draft.cashOut], draft.dataAvailable);
76
- const selectionRanges = getInitialSelectedAndHighlightedRanges({
77
- firstMonthOfFY: draft.firstMonthOfFY,
78
- timeframe,
79
- coaBalances,
80
- orderBy: 'ascending_date',
81
- maxNumOfPeriodsToSelect: defaultPeriods.numberOfPeriods,
82
- maxNumOfPeriodsToHighlight: initialState.uiState.maxNumOfPeriodsToHighlight,
83
- });
84
- if (selectionRanges?.selectedCOABalancesRange != null) {
85
- draft.uiState.selectedCOABalancesRange =
86
- selectionRanges?.selectedCOABalancesRange;
76
+ updateCashInCashOut: {
77
+ prepare(data, isCustomDateRange = false) {
78
+ return { payload: { data, isCustomDateRange } };
79
+ },
80
+ reducer(draft, action) {
81
+ const isCustomDateRange = action.payload.isCustomDateRange === true;
82
+ // A custom (dated) fetch marks the isolated scalar customFetchState so the
83
+ // shared fetchState the dashboard reads via hasValidState() stays as
84
+ // custom-mode activation left it (Not-Started), letting the card self-heal.
85
+ if (isCustomDateRange) {
86
+ draft.customFetchState = 'Completed';
87
87
  }
88
- draft.uiState.highlightedCOABalancesRange =
89
- selectionRanges?.highlightedCOABalancesRange;
90
- }
88
+ else {
89
+ draft.fetchState = 'Completed';
90
+ draft.error = undefined;
91
+ }
92
+ const report = mapReportPayloadToReport(action.payload.data.report);
93
+ // A custom (dated) fetch must not overwrite the shared report fields the
94
+ // dashboard card reads (firstMonthOfFY, bookCloseDate, reportTitle,
95
+ // status, version) — only its isolated custom* slots below.
96
+ if (!isCustomDateRange) {
97
+ Object.assign(draft, report);
98
+ }
99
+ const { cash_in, cash_out, net_cash_flow, insights } = action.payload.data.report;
100
+ const mappedCashIn = mapCOABalanceGroupedWithIDsPayloadToCOABalanceGroupedWithIDs(cash_in);
101
+ const mappedCashOut = mapCOABalanceGroupedWithIDsPayloadToCOABalanceGroupedWithIDs(cash_out);
102
+ const mappedNetCashFlow = mapCOABalanceGroupedPayloadToCOABalanceGrouped(net_cash_flow);
103
+ // Route custom-date results to the separate slots so the dashboard card
104
+ // (which reads cashIn/cashOut/netCashFlow) keeps the default window.
105
+ if (isCustomDateRange) {
106
+ draft.customCashIn = mappedCashIn;
107
+ draft.customCashOut = mappedCashOut;
108
+ draft.customNetCashFlow = mappedNetCashFlow;
109
+ }
110
+ else {
111
+ draft.cashIn = mappedCashIn;
112
+ draft.cashOut = mappedCashOut;
113
+ draft.netCashFlow = mappedNetCashFlow;
114
+ }
115
+ // Insights are shared (read by the dashboard card); a custom slice must
116
+ // not overwrite them.
117
+ if (!isCustomDateRange) {
118
+ draft.insights = mapInsightsPayloadtoInsightsState(insights);
119
+ }
120
+ // Derive dataAvailable from the slot this fetch actually populated.
121
+ const cashInOrCashOut = mappedCashIn != null ? mappedCashIn : mappedCashOut;
122
+ // Don't let a custom slice shrink the shared dataAvailable the dashboard
123
+ // uses for its period count; a default fetch always updates it, a custom
124
+ // one only populates it on first load (when nothing has set it yet).
125
+ if (!isCustomDateRange || draft.dataAvailable == null) {
126
+ draft.dataAvailable = report.dataAvailable
127
+ ? report.dataAvailable
128
+ : getDataAvailableDateRange(cashInOrCashOut);
129
+ }
130
+ if (draft.dataAvailable != null &&
131
+ // A custom date range drives the window; don't reset it to the default on
132
+ // every report receipt (this is what clamped wide ranges back to 8).
133
+ draft.uiState.customTimeframe == null) {
134
+ const timeframe = draft.uiState.timeframe;
135
+ const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [draft.cashIn, draft.cashOut], draft.dataAvailable);
136
+ const selectionRanges = getInitialSelectedAndHighlightedRanges({
137
+ firstMonthOfFY: draft.firstMonthOfFY,
138
+ timeframe,
139
+ coaBalances,
140
+ orderBy: 'ascending_date',
141
+ maxNumOfPeriodsToSelect: defaultPeriods.numberOfPeriods,
142
+ maxNumOfPeriodsToHighlight: initialState.uiState.maxNumOfPeriodsToHighlight,
143
+ });
144
+ if (selectionRanges?.selectedCOABalancesRange != null) {
145
+ draft.uiState.selectedCOABalancesRange =
146
+ selectionRanges?.selectedCOABalancesRange;
147
+ }
148
+ draft.uiState.highlightedCOABalancesRange =
149
+ selectionRanges?.highlightedCOABalancesRange;
150
+ }
151
+ },
91
152
  },
92
- updateCashInCashOutFailure(draft, action) {
93
- draft.fetchState = 'Error';
94
- draft.error = action.payload;
153
+ updateCashInCashOutFailure: {
154
+ prepare(error, isCustomDateRange = false) {
155
+ return { payload: { error, isCustomDateRange } };
156
+ },
157
+ reducer(draft, action) {
158
+ // Route the error like the request/success: a custom (dated) fetch's
159
+ // failure lands in customFetchState so the shared default the dashboard
160
+ // reads isn't marked Error.
161
+ if (action.payload.isCustomDateRange === true) {
162
+ draft.customFetchState = 'Error';
163
+ }
164
+ else {
165
+ draft.fetchState = 'Error';
166
+ draft.error = action.payload.error;
167
+ }
168
+ },
95
169
  },
96
170
  updateCashInCashOutAdditionalBalancesSelection(draft, action) {
97
171
  const { cashIn, cashOut, dataAvailable } = draft;
@@ -107,7 +181,7 @@ const cashInCashOut = createSlice({
107
181
  action.payload.additionalBalances.length != 0
108
182
  ? true
109
183
  : false;
110
- if (dataAvailable != null) {
184
+ if (dataAvailable != null && draft.uiState.customTimeframe == null) {
111
185
  const coaBalances = getAvailableCOABalancesWithMostRecentNonZeroBalance(draft.firstMonthOfFY, timeframe, [cashIn, cashOut], dataAvailable);
112
186
  const thisPeriod = extractThisPeriod(draft.firstMonthOfFY, timeframe, selectedCOABalancesRange, coaBalances);
113
187
  if (thisPeriod != null &&
@@ -136,6 +210,45 @@ const cashInCashOut = createSlice({
136
210
  }
137
211
  },
138
212
  updateCashInCashOutCOABalancesRange(draft, action) {
213
+ if (action.payload.isCustomDateRange != null) {
214
+ if (action.payload.isCustomDateRange === true) {
215
+ // Collapse the two raw date strings into a single customTimeframe object.
216
+ // Presence of customTimeframe signals custom-mode active (replaces the
217
+ // old isCustomDateRangeActive boolean + customStartDate/customEndDate).
218
+ draft.uiState.customTimeframe =
219
+ action.payload.customStartDate != null &&
220
+ action.payload.customEndDate != null
221
+ ? {
222
+ startDate: date(action.payload.customStartDate),
223
+ endDate: date(action.payload.customEndDate),
224
+ }
225
+ : undefined;
226
+ // Invalidate the shared default-window fetchState so dashboardEpic
227
+ // refetches the default data on the next dashboard visit — the custom
228
+ // fetch writes into the custom slots, and this lets the card self-heal
229
+ // without a reload. The detail screen reads customFetchState.
230
+ draft.fetchState = 'Not-Started';
231
+ }
232
+ else {
233
+ draft.uiState.customTimeframe = undefined;
234
+ }
235
+ }
236
+ // A custom date range drives the visible window directly: honor the exact
237
+ // numberOfPeriods + thisPeriod from the payload instead of re-deriving
238
+ // (which clamps the range to the available/non-zero periods).
239
+ if (draft.uiState.customTimeframe != null) {
240
+ draft.uiState.selectedCOABalancesRange =
241
+ action.payload.selectedCOABalancesRange;
242
+ if (action.payload.highlightedCOABalancesRange != null) {
243
+ // Clamp the highlight to the same cap the non-custom path enforces so
244
+ // a wide custom range doesn't highlight all N periods at once.
245
+ draft.uiState.highlightedCOABalancesRange = {
246
+ ...action.payload.highlightedCOABalancesRange,
247
+ numberOfPeriods: Math.min(action.payload.highlightedCOABalancesRange.numberOfPeriods, draft.uiState.maxNumOfPeriodsToHighlight),
248
+ };
249
+ }
250
+ return;
251
+ }
139
252
  const { cashIn, cashOut, dataAvailable } = draft;
140
253
  const { timeframe, selectedCOABalancesRange, maxNumOfPeriodsToHighlight, highlightedCOABalancesRange, } = draft.uiState;
141
254
  if (dataAvailable != null &&
@@ -180,6 +293,13 @@ const cashInCashOut = createSlice({
180
293
  draft.uiState.downloadState = action.payload;
181
294
  },
182
295
  updateCashInCashOutThisPeriod(draft, action) {
296
+ // In custom-date mode the picker drives the window; skip re-deriving the
297
+ // selected/highlighted ranges from the default dataAvailable (the epic
298
+ // re-dispatches the default thisPeriod on every response, which would
299
+ // otherwise clamp the custom window back to the default).
300
+ if (draft.uiState.customTimeframe != null) {
301
+ return;
302
+ }
183
303
  const { cashIn, cashOut, dataAvailable } = draft;
184
304
  const { timeframe, selectedCOABalancesRange, highlightedCOABalancesRange, maxNumOfPeriodsToHighlight, } = draft.uiState;
185
305
  if (dataAvailable != null && action.payload.thisPeriod != null) {
@@ -5,39 +5,53 @@ import { getNumberOfPeriods } from '../../commonStateTypes/dataAvailable';
5
5
  import { toTimeframeTick, } from '../../commonStateTypes/timeframeTick';
6
6
  import { getAccountsOrdered } from '../../entity/account/accountSelector';
7
7
  import { getAccountGroupsOrdered } from '../../entity/accountGroup/accountGroupSelector';
8
- export const getCashInCashOut = (accountState, accountGroupState, cashInCashOutState, filter) => {
8
+ export const getCashInCashOut = (accountState, accountGroupState, cashInCashOutState, filter, readCustomRangeSlot) => {
9
9
  const reportId = 'cash_in_cash_out';
10
+ // The detail (in custom mode) reads the isolated custom slots so a default
11
+ // fetch can't overwrite the partial-period data; the dashboard (no flag) reads
12
+ // the default slots, so the two can never clobber each other.
13
+ const useCustomRange = readCustomRangeSlot === true &&
14
+ cashInCashOutState.uiState.customTimeframe != null;
15
+ const cashInSource = useCustomRange
16
+ ? cashInCashOutState.customCashIn
17
+ : cashInCashOutState.cashIn;
18
+ const cashOutSource = useCustomRange
19
+ ? cashInCashOutState.customCashOut
20
+ : cashInCashOutState.cashOut;
21
+ const netCashFlowSource = useCustomRange
22
+ ? cashInCashOutState.customNetCashFlow
23
+ : cashInCashOutState.netCashFlow;
10
24
  const cashIn = {
11
25
  itemOrder: 'last_balance_descending',
12
- accountGroupsOrdered: getAccountGroupsOrdered(filter, accountState, accountGroupState, cashInCashOutState.cashIn?.accountGroupIDs ?? [], 'cash_in_cash_out', 'cash_in_cash_out', 'cashIn'),
13
- accountsOrdered: cashInCashOutState.cashIn?.accountGroupIDs.length === 0
26
+ accountGroupsOrdered: getAccountGroupsOrdered(filter, accountState, accountGroupState, cashInSource?.accountGroupIDs ?? [], 'cash_in_cash_out', 'cash_in_cash_out', 'cashIn'),
27
+ accountsOrdered: cashInSource?.accountGroupIDs.length === 0
14
28
  ? getAccountsOrdered(filter, accountState, {
15
- accountIDs: cashInCashOutState.cashIn?.accountIDs ?? [],
29
+ accountIDs: cashInSource?.accountIDs ?? [],
16
30
  reportId,
17
31
  }, 'cashIn')
18
32
  : [],
19
- ...getCOABalances(cashInCashOutState.cashIn, filter),
33
+ ...getCOABalances(cashInSource, filter),
20
34
  };
21
35
  const cashOut = {
22
36
  itemOrder: 'last_balance_descending',
23
- accountGroupsOrdered: getAccountGroupsOrdered(filter, accountState, accountGroupState, cashInCashOutState.cashOut?.accountGroupIDs ?? [], 'cash_in_cash_out', 'cash_in_cash_out', 'cashOut'),
24
- accountsOrdered: cashInCashOutState.cashOut?.accountGroupIDs.length === 0
37
+ accountGroupsOrdered: getAccountGroupsOrdered(filter, accountState, accountGroupState, cashOutSource?.accountGroupIDs ?? [], 'cash_in_cash_out', 'cash_in_cash_out', 'cashOut'),
38
+ accountsOrdered: cashOutSource?.accountGroupIDs.length === 0
25
39
  ? getAccountsOrdered(filter, accountState, {
26
- accountIDs: cashInCashOutState.cashOut?.accountIDs ?? [],
40
+ accountIDs: cashOutSource?.accountIDs ?? [],
27
41
  reportId,
28
42
  }, 'cashOut')
29
43
  : [],
30
- ...getCOABalances(cashInCashOutState.cashOut, filter),
44
+ ...getCOABalances(cashOutSource, filter),
31
45
  };
32
- const netCashFlow = getCOABalances(cashInCashOutState.netCashFlow, filter);
46
+ const netCashFlow = getCOABalances(netCashFlowSource, filter);
33
47
  let allTimeframeTicks = [];
34
48
  const dataAvailable = cashInCashOutState.dataAvailable;
35
49
  const firstMonthOfFY = cashInCashOutState.firstMonthOfFY;
36
50
  if (dataAvailable != null) {
37
51
  const noFilter = newBalancesFilterForDateRange(firstMonthOfFY, cashInCashOutState.uiState.timeframe, dataAvailable, 'ascending_date', []);
38
52
  const cashInCashOutBalances = cashIn.balances.length > 0
39
- ? getCOABalances(cashInCashOutState.cashIn, noFilter)
40
- : getCOABalances(cashInCashOutState.cashOut, noFilter);
53
+ ? getCOABalances(cashInSource, noFilter)
54
+ : getCOABalances(cashOutSource, noFilter);
41
55
  allTimeframeTicks = cashInCashOutBalances.balances.map((balance) => toTimeframeTick(balance));
42
56
  }
43
57
  return {
@@ -53,10 +67,17 @@ export const getCashInCashOut = (accountState, accountGroupState, cashInCashOutS
53
67
  reportTitle: cashInCashOutState.reportTitle,
54
68
  version: cashInCashOutState.version,
55
69
  status: cashInCashOutState.status,
56
- fetchState: cashInCashOutState.fetchState,
70
+ // The detail reads the isolated customFetchState so its loading state follows
71
+ // the custom fetch; the dashboard (no flag) reads the shared fetchState, which
72
+ // custom-mode activation reset to Not-Started so dashboardEpic refetches.
73
+ fetchState: useCustomRange && cashInCashOutState.customFetchState != null
74
+ ? cashInCashOutState.customFetchState
75
+ : cashInCashOutState.fetchState,
57
76
  error: cashInCashOutState.error,
58
77
  insights: cashInCashOutState.insights ?? [],
59
78
  downloadState: cashInCashOutState.uiState.downloadState,
79
+ // Surface the customTimeframe so the detail picker can restore the selection.
80
+ customTimeframe: cashInCashOutState.uiState.customTimeframe,
60
81
  };
61
82
  };
62
83
  export const getCashInCashOutUIState = createSelector((state) => state.cashInCashOutState, (state) => {
@@ -82,11 +103,17 @@ export const getCashInCashOutUIState = createSelector((state) => state.cashInCas
82
103
  export const getCashInCashOutForSelectedRange = createSelector((state) => state.accountState, (state) => state.accountGroupState, (state) => state.cashInCashOutState, (accountState, accountGroupState, cashInCashOutState) => {
83
104
  const { timeframe, selectedCOABalancesRange, selectedAdditionalBalances } = cashInCashOutState.uiState;
84
105
  const noFilter = newBalancesFilter(cashInCashOutState.firstMonthOfFY, timeframe, selectedCOABalancesRange.numberOfPeriods, selectedCOABalancesRange.thisPeriod, 'ascending_date', selectedAdditionalBalances);
85
- return getCashInCashOut(accountState, accountGroupState, cashInCashOutState, noFilter);
106
+ return getCashInCashOut(accountState, accountGroupState, cashInCashOutState, noFilter,
107
+ // Detail selectors read the custom slot when the detail is in custom mode;
108
+ // the dashboard's getCashInCashOutLast4Periods does not.
109
+ cashInCashOutState.uiState.customTimeframe != null);
86
110
  });
87
111
  export const getCashInCashOutForHighlightedRange = createSelector((state) => state.accountState, (state) => state.accountGroupState, (state) => state.cashInCashOutState, (accountState, accountGroupState, cashInCashOutState) => {
88
112
  const { timeframe, highlightedCOABalancesRange, selectedAdditionalBalances, maxNumOfPeriodsToHighlight, } = cashInCashOutState.uiState;
89
113
  const filter = newBalancesFilter(cashInCashOutState.firstMonthOfFY, timeframe, highlightedCOABalancesRange?.numberOfPeriods ??
90
114
  maxNumOfPeriodsToHighlight, highlightedCOABalancesRange?.thisPeriod, 'ascending_date', selectedAdditionalBalances);
91
- return getCashInCashOut(accountState, accountGroupState, cashInCashOutState, filter);
115
+ return getCashInCashOut(accountState, accountGroupState, cashInCashOutState, filter,
116
+ // Detail selectors read the custom slot when the detail is in custom mode;
117
+ // the dashboard's getCashInCashOutLast4Periods does not.
118
+ cashInCashOutState.uiState.customTimeframe != null);
92
119
  });
@@ -3,17 +3,25 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateAccounts } from '../../entity/account/accountReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
5
5
  import { fetchCashPosition, updateCashPosition, updateCashPositionFailure, updateCashPositionThisPeriod, } from './cashPositionReducer';
6
- export const fetchCashPositionEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchCashPosition.match), switchMap((action) => zeniAPI
7
- .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_position?query=${encodeURIComponent(`{"is_full_response":${action.payload.fetchFullReport}}`)}`)
8
- .pipe(mergeMap((response) => {
9
- if (isSuccessResponse(response)) {
10
- return of(updateAccounts(response.data?.report.accounts ?? [], 'cash_position'), updateCashPosition(response.data), // eslint-disable-line @typescript-eslint/no-non-null-assertion
11
- updateCashPositionThisPeriod({
12
- thisPeriod: state.value.reportUIOptionsState.thisPeriod,
13
- }));
6
+ export const fetchCashPositionEpic = (actions$, state, zeniAPI) => actions$.pipe(filter(fetchCashPosition.match), switchMap((action) => {
7
+ const queryValue = { is_full_response: action.payload.fetchFullReport };
8
+ const isCustomDateRange = action.payload.startDate != null && action.payload.endDate != null;
9
+ if (isCustomDateRange) {
10
+ queryValue.custom_start_date = action.payload.startDate;
11
+ queryValue.custom_end_date = action.payload.endDate;
14
12
  }
15
- else {
16
- return of(updateCashPositionFailure(response.status));
17
- }
18
- }), catchError((error) => of(updateCashPositionFailure(createZeniAPIStatus('Unexpected error', 'Cash Position REST API call errored out' +
19
- JSON.stringify(error))))))));
13
+ return zeniAPI
14
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/reports/cash_position?query=${encodeURIComponent(JSON.stringify(queryValue))}`)
15
+ .pipe(mergeMap((response) => {
16
+ if (isSuccessResponse(response)) {
17
+ return of(updateAccounts(response.data?.report.accounts ?? [], 'cash_position'), updateCashPosition(response.data, isCustomDateRange), // eslint-disable-line @typescript-eslint/no-non-null-assertion
18
+ updateCashPositionThisPeriod({
19
+ thisPeriod: state.value.reportUIOptionsState.thisPeriod,
20
+ }));
21
+ }
22
+ else {
23
+ return of(updateCashPositionFailure(response.status, isCustomDateRange));
24
+ }
25
+ }), catchError((error) => of(updateCashPositionFailure(createZeniAPIStatus('Unexpected error', 'Cash Position REST API call errored out' +
26
+ JSON.stringify(error)), isCustomDateRange))));
27
+ }));