Skip to content

LF-5301: Add Profitability widget Redux slice, date-range hook, and aggregation utils#4182

Open
litefarm-pr-bot wants to merge 1 commit into
integrationfrom
LF-5301-add-profitability-widget-redux-slice-date-range-hook-and-aggregation-utils
Open

LF-5301: Add Profitability widget Redux slice, date-range hook, and aggregation utils#4182
litefarm-pr-bot wants to merge 1 commit into
integrationfrom
LF-5301-add-profitability-widget-redux-slice-date-range-hook-and-aggregation-utils

Conversation

@litefarm-pr-bot
Copy link
Copy Markdown
Collaborator

@litefarm-pr-bot litefarm-pr-bot commented May 25, 2026

Description

The Profitability home widget needs three things that have no place in any existing module: a per-widget date-range state (so the user's range selection survives navigation and refresh independently of the Finances page's own selection), a hook that exposes that state plus the list of years for which there is data, and a pure-functional aggregation layer the widget's data hook will compose to produce KPIs, sources/categories bars, and per-entity rows. This PR ships those three foundations with no UI surface yet — the presentational components and the smart container both depend on this slice + hook + utils but ship in follow-up PRs.

slice.ts is a createSlice-based date-range slice with a single updateDateRange action, registered under tempStateReducer next to homeReducer. Initial state captures the current Year-to-Date dates at module load so a first-time user sees the correct range with no extra dispatch; the shared useDateRange hook (extended in the prior PR in this story) recomputes those dates on mount so they do not go stale across days, and leaves caller-supplied dynamic options like a persisted calendar year untouched. Redux Persist covers the whole store globally so no whitelist change was required. useProfitabilityDateRange mirrors the existing useFinancesDateRange pattern but reads/writes the new slice and additionally derives availableYears from the legacy Finances sales+expense selectors. utils.ts stays deliberately pure and i18n-free: revenue groups return sub-keys (CROP_SALES / ANIMAL_SALES / FARM_GENERAL) for the component to localise, expense categories return either the caller's expense_name (custom types) or an i18n key path (system types and the synthetic Labour entry), and aggregateByEntity walks crop_variety_sale, animal_sale, farm_expense_crop_variety, and farm_expense_animal to attribute revenue and expense per-entity. Revenue is returned as null (rendered as "Not yet" by the table) for entities that have expense allocations within the date range but no sales; the 'all' tab includes a synthetic farm_general row when there are unallocated revenues or expenses.

The convention this PR establishes for the widget: keep aggregation utilities pure (no i18n calls, no selector reads) so they can be exhaustively unit-tested with synthetic fixtures, and have the consuming hook/container compose them with Redux selectors and RTK Query results. When totals could come from multiple shapes — for example a sale with crop allocations vs an animal-row sale vs a farm-general sale — collapse them through a single helper (sumSaleValue) rather than branching at every call site.

Jira link: https://lite-farm.atlassian.net/browse/LF-5301

Type of change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Passes test case
  • UI components visually reviewed on desktop view
  • UI components visually reviewed on mobile view
  • Other (please explain): pnpm exec vitest run src/tests/profitabilityWidget.test.js — 21 tests pass, covering each public function: filter helpers, KPI sums across mixed sale shapes, YoY direction on two same-length consecutive windows, group/category aggregation including the implicit Labour category and the i18n-key contract, per-entity attribution including the "Not yet" sentinel and the all-tab synthetic farm_general row, year extraction excluding the current year, and the dynamic-year option shape. NODE_OPTIONS=--max_old_space_size=3000 pnpm exec tsc --noEmit from packages/webapp passes with no errors. No UI changed in this PR — visual review is not applicable; it will land with the presentational-components PR that follows.

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The precommit and linting ran successfully
  • I have added or updated language tags for text that's part of the UI
  • I have ordered translation keys alphabetically (optional: run pnpm i18n to help with this)
  • I have added the GNU General Public License to all new files

…ggregation utils

Adds the non-UI foundations of the Profitability home widget: a
createSlice-based date-range slice persisted alongside the other Home
state under tempStateReducer, a useProfitabilityDateRange hook that
mirrors the existing useFinancesDateRange pattern but reads/writes the
new slice, and a utils module with pure aggregation functions that the
widget's data hook (built in a follow-up PR) will compose.

The slice's initial state captures the current Year-to-Date dates at
module load so a first-time user sees the correct range with no extra
dispatch; useDateRange's on-mount recompute (extended in the prior PR
in this story) keeps that fresh and also leaves caller-supplied dynamic
options like a calendar year untouched. The slice is registered under
tempStateReducer next to homeReducer; Redux Persist covers the store
globally so the selection survives a refresh without any whitelist
change.

The utils module deliberately stays pure and i18n-free: revenue groups
return sub-keys (CROP_SALES / ANIMAL_SALES / FARM_GENERAL) for the
component to localise, and expense categories return either the
caller's expense_name (custom types) or an i18n key path (system types
and the synthetic Labour entry). aggregateByEntity walks
crop_variety_sale, animal_sale, farm_expense_crop_variety, and
farm_expense_animal to attribute revenue and expense per-entity;
revenue is returned as null (rendered as "Not yet" by the table) for
entities that have expense allocations but no sales in the date range,
and the all-tab includes a synthetic farm_general row when there are
unallocated revenues or expenses.

21 Vitest tests cover each public function — KPI sums across mixed sale
shapes, YoY direction across two same-length windows, group/category
aggregation including the implicit Labour category, per-entity
attribution including the Not yet sentinel and the all-tab synthetic
row, year extraction, and the dynamic-year option shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@litefarm-pr-bot litefarm-pr-bot requested review from a team as code owners May 25, 2026 16:14
@litefarm-pr-bot litefarm-pr-bot requested review from SayakaOno and removed request for a team May 25, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant