Skip to content

Add carbon sequestration feature with calculations and metrics#465

Merged
SvenVw merged 12 commits into
developmentfrom
FDM464
Feb 19, 2026
Merged

Add carbon sequestration feature with calculations and metrics#465
SvenVw merged 12 commits into
developmentfrom
FDM464

Conversation

@SvenVw
Copy link
Copy Markdown
Collaborator

@SvenVw SvenVw commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Carbon sequestration card in field details showing current vs potential storage, CO2e, water storage, N‑mineralisation and economic estimates.
    • Floating back-to-map button on field details.
  • Improvements

    • Redesigned, responsive field details layout with reorganised panels.
    • Enhanced cultivation history timeline with copy-to-clipboard and mobile expand/collapse.
    • Updated loading skeletons and streamlined soil/groundwater presentations.

Closes #464

…t carbon sequestration, inlcuding current state, potential maximal state and relatable figures of how much carbon can be stored
@SvenVw SvenVw self-assigned this Feb 18, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 18, 2026

🦋 Changeset detected

Latest commit: 8ecaebe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@svenvw/fdm-app Minor
@svenvw/fdm-calculator Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Walkthrough

Adds a Carbon Sequestration feature: new NMI estimate fields and schema, two calculator functions (water & NLV supply by SOM) with tests and exports, a CarbonSequestrationCard and skeleton, layout and atlas field UI refactors, and loader wiring to compute and pass carbon estimates to the UI.

Changes

Cohort / File(s) Summary
Changesets
.changeset/quick-rocks-yawn.md, .changeset/silver-rice-take.md, .changeset/slick-boats-win.md, .changeset/soft-chairs-punch.md, .changeset/wicked-lies-lead.md
Added changelog entries: app and calculator minor bumps documenting new carbon card, calculator functions, UI/design tweaks, and navigation improvements.
Carbon Sequestration UI
fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx
New CarbonSequestrationCard and CarbonSequestrationSkeleton components: gauge of current vs potential SOM, CO2eq/equivalents, impact grid, methodology collapsible, and a loading state.
Layout & Skeleton
fdm-app/app/components/blocks/atlas-fields/layout.tsx, fdm-app/app/components/blocks/atlas-fields/skeleton.tsx
Layout API changed (removed title, added carbon), responsive grid reworked to sidebar + main stack; skeleton now uses CarbonSequestrationSkeleton and container adjusted.
Atlas Field Components (UI refactors)
fdm-app/app/components/blocks/atlas-fields/field-details.tsx, fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx, fdm-app/app/components/blocks/atlas-fields/groundwater.tsx, fdm-app/app/components/blocks/atlas-fields/soil-texture.tsx
Refactored cards to simpler responsive grids; added region label mapping; cultivation history now a timeline with mobile toggle; updated skeletons, icons and copy-to-clipboard behavior.
NMI integration
fdm-app/app/integrations/nmi.ts
Extended soilParameterEstimatesSchema and getSoilParameterEstimates return type with b_c_st03, b_som_potential, b_c_st03_potential, b_c_delta.
Route & loader wiring
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields.$centroid.tsx, fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx
Loader now parses/validates centroid earlier, computes carbonEstimates (including extraWaterStorage and extraNMineralization via calculator functions), returns b_id_farm; FieldDetailsAtlas now accepts b_id_farm and receives carbonEstimates; removed conditional header back action.
Calculator functions & tests
fdm-calculator/src/other/water-supply-by-som.ts, fdm-calculator/src/other/water-supply-by-som.test.ts, fdm-calculator/src/other/nlv-supply-by-som.ts, fdm-calculator/src/other/nlv-supply-by-som.test.ts
Added calculateWaterSupplyBySom (Wösten PTFs, Decimal.js) and calculateNlvSupplyBySom (MINIP-like NLV change) with unit tests covering edge cases and typical scenarios.
Calculator exports
fdm-calculator/src/index.ts
Exported new functions and param types: calculateWaterSupplyBySom, WaterSupplyBySomParams, calculateNlvSupplyBySom, NlvSupplyBySomParams.
Design / planning doc
issue-carbon-sequestration.md
Feature plan describing NMI fields, UI scaffold, loader wiring, pedotransfer functions and relatable impact metrics.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Route as Route Loader
    participant NMI as NMI API
    participant Calc as fdm-calculator
    participant UI as CarbonSequestrationCard

    User->>Route: Navigate to field details
    Route->>NMI: Fetch soil estimates (include new carbon fields)
    NMI-->>Route: Return a_som_loi, b_som_potential, b_c_st03, b_c_st03_potential, b_c_delta

    Route->>Calc: calculateWaterSupplyBySom(soil params)
    Calc-->>Route: extraWaterStorage (mm)

    Route->>Calc: calculateNlvSupplyBySom(soil params)
    Calc-->>Route: extraNMineralization (kg N/ha)

    Route->>Route: Aggregate carbonEstimates object
    Route-->>UI: Pass carbonEstimates & soil params

    UI->>UI: Compute % of potential, CO2eq, equivalents, euro estimate
    UI-->>User: Render CarbonSequestrationCard with metrics and methodology
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • BoraIneviNMI
  • gerardhros

Poem

🐰 I hopped through rows of soil and code,
Counting SOM where new fields showed.
From water drops to nitrogen song,
I stitched the metrics all day long.
Now farmers peek and hum along. 🌱✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All primary objectives from issue #464 have been implemented: NMI integration extended with new fields [1], CarbonSequestrationCard component created with gauge and relatable impact section [2], loader data exposure and route integration completed [3], skeleton loading state implemented [4], and pedotransfer functions for water storage and N-mineralization calculations added to fdm-calculator [5].
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #464 objectives. UI refactoring of field details pages (cultivation-history, groundwater, soil-texture, field-details) improves responsive design and information display as noted in changesets, supporting the overall feature integration.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FDM464

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.10%. Comparing base (f99e688) to head (8ecaebe).
⚠️ Report is 13 commits behind head on development.

Additional details and impacted files
@@               Coverage Diff               @@
##           development     #465      +/-   ##
===============================================
+ Coverage        87.98%   88.10%   +0.11%     
===============================================
  Files               91       93       +2     
  Lines             4629     4675      +46     
  Branches          1497     1498       +1     
===============================================
+ Hits              4073     4119      +46     
  Misses             556      556              
Flag Coverage Δ
fdm-calculator 88.86% <100.00%> (+0.19%) ⬆️
fdm-core 86.69% <ø> (ø)
fdm-data 92.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@SvenVw SvenVw changed the title FDM464 @coderabbitai Feb 18, 2026
@SvenVw SvenVw marked this pull request as ready for review February 18, 2026 15:22
@coderabbitai coderabbitai Bot changed the title @coderabbitai Add carbon sequestration feature with calculations and metrics Feb 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx (1)

94-100: ⚠️ Potential issue | 🟡 Minor

Add required action prop to <Header>.

The Header component requires an action prop (typed as action: HeaderActionProps | undefined without optional marker). Omitting it will cause a TypeScript compilation error. All other usages in the codebase explicitly pass action={undefined}. Apply the same pattern here.

Fix
-            <Header>
+            <Header action={undefined}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.tsx around lines 94 -
100, The Header component requires an explicit action prop (typed as
HeaderActionProps | undefined); update the JSX that renders <Header> (the block
containing <HeaderFarm ... /> and <HeaderAtlas ... />) to pass
action={undefined} (matching other usages) so TypeScript compiles without error.
fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx (1)

240-272: ⚠️ Potential issue | 🟡 Minor

Skeleton description text diverges from the live card.

The skeleton's CardDescription (Lines 247-249) says "De gewassen van de afgelopen jaren op dit perceel volgens Basisregistratie Gewaspercelen." while the live card (Lines 77-78) says "Dit zijn de gewassen zoals geregistreerd in de Basisregistratie Gewaspercelen (BRP)." Keeping these consistent avoids a visible text shift when the skeleton is replaced by the loaded content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx` around
lines 240 - 272, The skeleton's CardDescription text in the
CultivationHistorySkeleton component currently differs from the real card;
update the string inside the CardDescription in CultivationHistorySkeleton so it
exactly matches the live card text "Dit zijn de gewassen zoals geregistreerd in
de Basisregistratie Gewaspercelen (BRP)." to prevent a visible shift when
swapping skeleton -> loaded content; locate the CardDescription inside the
CultivationHistorySkeleton function and replace the current Dutch sentence with
the exact live-card sentence.
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields.$centroid.tsx (1)

160-165: ⚠️ Potential issue | 🔴 Critical

Remove a_cn_fr from soilParameterEstimates — it's not accepted by SoilTextureCard.

Line 164 adds a_cn_fr to the soilParameterEstimates object passed to SoilTextureCard, but the component's prop type definition (at fdm-app/app/components/blocks/atlas-fields/soil-texture.tsx:13-17) only accepts a_clay_mi, a_silt_mi, and a_sand_mi. The strict type definition will cause a TypeScript compilation error. Either remove a_cn_fr from this object, or update SoilTextureCard's prop type if the field should be displayed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.fields.$centroid.tsx
around lines 160 - 165, The soilParameterEstimates object being passed to
SoilTextureCard includes a non-supported key a_cn_fr which triggers a TypeScript
error; remove a_cn_fr from the soilParameterEstimates object so it only contains
a_clay_mi, a_silt_mi, and a_sand_mi (or alternatively, if you intend to display
a_cn_fr, add it to SoilTextureCard's prop type and component interface instead).
Locate the construction of soilParameterEstimates (the object with
a_clay_mi/a_silt_mi/a_sand_mi/a_cn_fr) and delete the a_cn_fr entry or update
SoilTextureCard's props to accept a_cn_fr and render it accordingly.
🧹 Nitpick comments (4)
fdm-app/app/components/blocks/atlas-fields/field-details.tsx (1)

22-28: Consider hoisting regionLabels to module scope.

This is a static constant recreated on every render. Moving it outside the component avoids unnecessary allocations.

Proposed change
+const regionLabels = {
+    klei: "Klei",
+    veen: "Veen",
+    loess: "Löss",
+    zand_nwc: "Noordelijk, westelijk, en centraal zand",
+    zand_zuid: "Zuidelijk zand",
+} as const
+
 export function FieldDetailsCard({
     fieldDetails,
 }: {
     ...
 }) {
-    const regionLabels = {
-        klei: "Klei",
-        veen: "Veen",
-        loess: "Löss",
-        zand_nwc: "Noordelijk, westelijk, en centraal zand",
-        zand_zuid: "Zuidelijk zand",
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/field-details.tsx` around lines 22
- 28, regionLabels is a static object recreated on every render inside the
FieldDetails component; move the const regionLabels definition out to module
scope (above the FieldDetails component) so it’s allocated once, then update any
references within FieldDetails to use the hoisted regionLabels; ensure the
identifier stays the same and remove the in-component declaration to avoid
shadowing.
fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx (1)

48-61: Derived metrics use pre-rounded intermediates, causing compounding rounding errors.

carKmEquivalent (line 59) and the euro estimate (line 200) are computed from potentialCO2eq, which is itself already rounded. This can silently shift displayed values. For example, b_c_delta = 0.14potentialCO2eq rounds to 0.5carKmEquivalent = 4000, but the unrounded path gives ~4111 km.

Compute derived values from raw inputs, rounding only at the presentation layer.

Proposed refactor
     // API values
     const currentOM = Math.round(carbonEstimates.a_som_loi * 10) / 10
     const maxOM = Math.round(carbonEstimates.b_som_potential * 10) / 10
-    const potentialCO2eq =
-        Math.round(carbonEstimates.b_c_delta * 3.67 * 10) / 10
+    const rawCO2eq = carbonEstimates.b_c_delta * 3.67
+    const potentialCO2eq = Math.round(rawCO2eq * 10) / 10

     // Derived values
     const percentageOfMax =
-        maxOM > 0 ? Math.min((currentOM / maxOM) * 100, 100) : 0
+        carbonEstimates.b_som_potential > 0
+            ? Math.min((carbonEstimates.a_som_loi / carbonEstimates.b_som_potential) * 100, 100)
+            : 0

     // Impact calculations
-    const carKmEquivalent = Math.round(potentialCO2eq * 8000)
+    const carKmEquivalent = Math.round(rawCO2eq * 8000)

And on line ~200:

-                                    {Math.round(
-                                        potentialCO2eq * 75,
-                                    ).toLocaleString()}
+                                    {Math.round(
+                                        rawCO2eq * 75,
+                                    ).toLocaleString()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx` around
lines 48 - 61, Right now currentOM, maxOM and potentialCO2eq are pre-rounded and
later used to compute carKmEquivalent and the euro estimate, which causes
compounding rounding errors; change computations so carKmEquivalent and the euro
estimate are calculated from the raw API values (e.g., use
carbonEstimates.b_c_delta * 3.67 for CO2eq and then *8000 for carKmEquivalent)
and only apply Math.round / formatting at render time (keep currentOM/maxOM
rounding for display only), updating references to potentialCO2eq,
carKmEquivalent, and the euro-estimate variable so they use the unrounded
intermediate values before presentation rounding.
fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx (1)

1-1: HelpCircle is imported but unused in this component.

HelpCircle is imported on Line 1 but not referenced anywhere in CultivationHistoryCard or CultivationHistorySkeleton. It may be a leftover from development.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx` at line
1, The import HelpCircle is unused; remove it from the import list at the top of
the file (where ChevronDown, ChevronUp, Copy are imported) to clean up the
module and avoid lint warnings; check the CultivationHistoryCard and
CultivationHistorySkeleton components to ensure no remaining references to
HelpCircle before deleting the import.
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields.$centroid.tsx (1)

274-281: Fixed floating button for "Terug naar kaart" is a nice UX addition.

The z-50 ensures it overlays scrollable content. One thing to consider: on mobile with limited screen space, this floating button may overlap with content at the bottom of the page. A small pb-20 (or similar) on the <main> in the layout could prevent the last card from being obscured by the button.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.fields.$centroid.tsx
around lines 274 - 281, The floating "Terug naar kaart" button (className "fixed
bottom-6 right-6 z-50" inside the Button/NavLink) can obscure bottom content on
small screens; add bottom padding to the page layout by applying a sufficient
padding-bottom (e.g., pb-20) to the surrounding <main> element in the layout
component so the last card isn’t hidden behind the fixed button—update the
layout's main element (where this route renders) rather than the button itself.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/wicked-lies-lead.md:
- Line 5: Update the changeset text to list all primary additions from the PR:
mention the new CarbonSequestrationCard UI component, the new NMI API fields
b_c_st03, b_som_potential, b_c_st03_potential, and b_c_delta, the
layout/skeleton refactors, and the new calculation functions (referencing the
functions you added for carbon/sequestration calculations). Replace the
single-line description with a short bulleted or comma-separated summary
covering each item so release notes accurately reflect the PR surface.

In `@fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx`:
- Around line 158-206: The component renders cultivation.b_lu_name directly into
TooltipTrigger and TooltipContent which can be undefined; update both usages to
use a safe fallback (e.g., cultivation.b_lu_name ?? "Onbekend gewas") so the
tooltip trigger and content never render empty; locate the occurrences in this
file (the TooltipTrigger and TooltipContent blocks around cultivation.b_lu_name)
and replace the direct property access with the fallback expression (or
conditionally render a default string) to ensure a visible label even when
b_lu_name is undefined.

In `@fdm-app/app/components/blocks/atlas-fields/groundwater.tsx`:
- Around line 44-53: The unit label "cm-mv" renders even when b_gwl_ghg (and
similarly b_gwl_glg) is null because the check uses !== undefined; change the
conditional that controls the unit span to test for both null and undefined (use
!= null / loose null check) so the unit only renders when a real numeric value
exists for groundwaterEstimates.b_gwl_ghg and groundwaterEstimates.b_gwl_glg.

In `@fdm-app/app/components/blocks/atlas-fields/soil-texture.tsx`:
- Around line 78-86: The skeleton grid in the SoilTexture component is using a
fixed "grid-cols-3" which causes layout shift; update the div inside CardContent
that currently has className "grid grid-cols-3 gap-6" to match the real
content's responsive classes (e.g., "grid grid-cols-1 sm:grid-cols-3 gap-6") so
the Skeleton placeholders use the same responsive breakpoint behavior as the
actual grid.

In `@fdm-calculator/src/other/water-supply-by-som.ts`:
- Around line 33-151: The calculateWhc internal function and its callers in
calculateWaterSupplyBySom must guard against zero/negative inputs: clamp
a_som_loi, a_clay_mi and a_silt_mi to a small epsilon (e.g. 1e-6) at the start
of calculateWhc (and/or before computing normalizedClay/normalizedSilt) so
divisions and ln calls never receive 0 or negative values; update calculateWhc
signature usage and the normalizedClay/normalizedSilt computation to use the
clamped values (referencing calculateWaterSupplyBySom, calculateWhc,
normalizedClay, normalizedSilt, and params a_som_loi/a_clay_mi/a_silt_mi) and
ensure all places that divide by or ln() these values use the clamped variables.

---

Outside diff comments:
In `@fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx`:
- Around line 240-272: The skeleton's CardDescription text in the
CultivationHistorySkeleton component currently differs from the real card;
update the string inside the CardDescription in CultivationHistorySkeleton so it
exactly matches the live card text "Dit zijn de gewassen zoals geregistreerd in
de Basisregistratie Gewaspercelen (BRP)." to prevent a visible shift when
swapping skeleton -> loaded content; locate the CardDescription inside the
CultivationHistorySkeleton function and replace the current Dutch sentence with
the exact live-card sentence.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.fields.$centroid.tsx:
- Around line 160-165: The soilParameterEstimates object being passed to
SoilTextureCard includes a non-supported key a_cn_fr which triggers a TypeScript
error; remove a_cn_fr from the soilParameterEstimates object so it only contains
a_clay_mi, a_silt_mi, and a_sand_mi (or alternatively, if you intend to display
a_cn_fr, add it to SoilTextureCard's prop type and component interface instead).
Locate the construction of soilParameterEstimates (the object with
a_clay_mi/a_silt_mi/a_sand_mi/a_cn_fr) and delete the a_cn_fr entry or update
SoilTextureCard's props to accept a_cn_fr and render it accordingly.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.tsx:
- Around line 94-100: The Header component requires an explicit action prop
(typed as HeaderActionProps | undefined); update the JSX that renders <Header>
(the block containing <HeaderFarm ... /> and <HeaderAtlas ... />) to pass
action={undefined} (matching other usages) so TypeScript compiles without error.

---

Nitpick comments:
In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx`:
- Around line 48-61: Right now currentOM, maxOM and potentialCO2eq are
pre-rounded and later used to compute carKmEquivalent and the euro estimate,
which causes compounding rounding errors; change computations so carKmEquivalent
and the euro estimate are calculated from the raw API values (e.g., use
carbonEstimates.b_c_delta * 3.67 for CO2eq and then *8000 for carKmEquivalent)
and only apply Math.round / formatting at render time (keep currentOM/maxOM
rounding for display only), updating references to potentialCO2eq,
carKmEquivalent, and the euro-estimate variable so they use the unrounded
intermediate values before presentation rounding.

In `@fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx`:
- Line 1: The import HelpCircle is unused; remove it from the import list at the
top of the file (where ChevronDown, ChevronUp, Copy are imported) to clean up
the module and avoid lint warnings; check the CultivationHistoryCard and
CultivationHistorySkeleton components to ensure no remaining references to
HelpCircle before deleting the import.

In `@fdm-app/app/components/blocks/atlas-fields/field-details.tsx`:
- Around line 22-28: regionLabels is a static object recreated on every render
inside the FieldDetails component; move the const regionLabels definition out to
module scope (above the FieldDetails component) so it’s allocated once, then
update any references within FieldDetails to use the hoisted regionLabels;
ensure the identifier stays the same and remove the in-component declaration to
avoid shadowing.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.fields.$centroid.tsx:
- Around line 274-281: The floating "Terug naar kaart" button (className "fixed
bottom-6 right-6 z-50" inside the Button/NavLink) can obscure bottom content on
small screens; add bottom padding to the page layout by applying a sufficient
padding-bottom (e.g., pb-20) to the surrounding <main> element in the layout
component so the last card isn’t hidden behind the fixed button—update the
layout's main element (where this route renders) rather than the button itself.

Comment thread .changeset/wicked-lies-lead.md
Comment thread fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx
Comment thread fdm-app/app/components/blocks/atlas-fields/groundwater.tsx
Comment thread fdm-app/app/components/blocks/atlas-fields/soil-texture.tsx
Comment thread fdm-calculator/src/other/water-supply-by-som.ts
@coderabbitai coderabbitai Bot added branch:development Issue only affecting development, not the main branch (yet) enhancement New feature or request fdm-app fdm-calculator labels Feb 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx (2)

48-56: Progress gauge uses rounded values, introducing minor inaccuracy.

percentageOfMax is derived from currentOM and maxOM, which are already rounded to 1 decimal. This means the progress bar and percentage label can be slightly off compared to the raw API values. For a display-only gauge this is acceptable, but using raw values for the ratio would be more precise:

Suggested improvement
-    const percentageOfMax =
-        maxOM > 0 ? Math.min((currentOM / maxOM) * 100, 100) : 0
+    const percentageOfMax =
+        carbonEstimates.b_som_potential > 0
+            ? Math.min((carbonEstimates.a_som_loi / carbonEstimates.b_som_potential) * 100, 100)
+            : 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx` around
lines 48 - 56, The progress percentage is computed from the already-rounded
values currentOM and maxOM causing small inaccuracies; change percentageOfMax to
use the raw API values from carbonEstimates (e.g. use carbonEstimates.a_som_loi
and carbonEstimates.b_som_potential directly, with a max check) to compute the
ratio, then keep rounding only for display variables (currentOM / maxOM) so the
gauge/percentage label uses the precise ratio while UI numbers remain rounded;
update references to percentageOfMax accordingly.

1-12: Unused imports: Database, Target, Zap.

These three icons are imported but never used in the component JSX. They should be removed to keep the new file clean.

Proposed fix
 import {
     Leaf,
     Car,
     HelpCircle,
     ChevronDown,
     Droplets,
     Sprout,
     Euro,
-    Database,
-    Target,
-    Zap,
 } from "lucide-react"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx` around
lines 1 - 12, The import list in the CarbonSequestration component includes
unused icons Database, Target, and Zap; remove those three symbols from the
import statement (the line importing Leaf, Car, HelpCircle, ChevronDown,
Droplets, Sprout, Euro, Database, Target, Zap from "lucide-react") so only the
actually used icons remain (e.g., Leaf, Car, HelpCircle, ChevronDown, Droplets,
Sprout, Euro), and run a quick lint/compile to confirm no other references to
Database, Target, or Zap exist.
fdm-app/app/components/blocks/atlas-fields/layout.tsx (1)

3-37: Clean layout composition with the new carbon slot.

The 12-column grid with sticky sidebar, responsive ordering, and pb-20 for the floating button clearance is well thought out.

One note: lg:row-span-10 (line 25) is an arbitrarily large value to ensure the sidebar spans the full right-side content. This works with CSS Grid implicit rows but is brittle if more cards are added later. Consider documenting the intent with a comment, or alternatively, using a nested flex/grid layout for the two columns to avoid the magic number.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/layout.tsx` around lines 3 - 37,
The lg:row-span-10 on the cultivationHistory sidebar in FieldDetailsAtlasLayout
is a brittle "magic number" used to force the sidebar to span the right-side
content; replace this with a more robust layout or at least document the intent:
either refactor the outer grid into a two-column container (e.g., a nested
grid/flex pair where the left column is the main flow and the right column is a
sticky sidebar so no arbitrary row-span is needed) or add an inline comment next
to the lg:row-span-10 reference explaining why the value is required and when it
should be revisited to prevent future regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx`:
- Line 264: The skeleton's grid layout in the CarbonSequestration component is
not responsive: replace the hardcoded "grid-cols-3" on the skeleton container
with the same responsive classes used by the real card ("grid-cols-1
md:grid-cols-3") so the skeleton matches the real card's behavior; locate the
div with className containing "grid grid-cols-3 gap-6" inside the
CarbonSequestration component and update its className to "grid grid-cols-1
md:grid-cols-3 gap-6".
- Around line 74-88: The HelpCircle icon used as the TooltipTrigger is an SVG
and cannot receive keyboard focus; update the trigger so it's a focusable
element (e.g., wrap the HelpCircle in a <button> or render a button as the
TooltipTrigger child) to make the tooltip keyboard-accessible and screen-reader
friendly. Specifically, modify the TooltipTrigger/TooltipProvider block that
currently uses TooltipTrigger asChild with HelpCircle so the child is a
focusable button (or another focusable primitive) and add an appropriate
accessible name (aria-label or visually-hidden text) on that button; keep
Tooltip, TooltipContent and the existing tooltip text unchanged.

In `@fdm-app/app/components/blocks/atlas-fields/field-details.tsx`:
- Around line 104-113: The skeleton layout's responsive grid classes don't match
the real card; update the first skeleton container (inside CardContent with
className "space-y-6") to use grid classes "grid grid-cols-1 md:grid-cols-2
gap-4" instead of "grid-cols-2", and update the second skeleton container (the
one with "pt-0 sm:pt-4 border-t-0 sm:border-t") to use "grid grid-cols-1
sm:grid-cols-3 gap-4" instead of "grid-cols-3" so the skeleton matches the real
card's responsiveness.
- Around line 35-36: Replace the hardcoded "Status voor 2025" string in the
FieldDetails component with a year derived from a prop (e.g., calendar or
activeYear) so it stays current: add a prop to the FieldDetails component
signature (e.g., { calendar }: { calendar: number | string } or activeYear), use
that prop in the JSX (e.g., <p>{`Status voor ${calendar}`}</p>), and update any
parent/route that renders FieldDetails to pass the route calendar param into
this prop; ensure prop typing is added/adjusted in the component export or
interface.

In `@fdm-app/app/components/blocks/atlas-fields/groundwater.tsx`:
- Line 84: The skeleton grid uses a fixed three-column layout causing a layout
shift; update the skeleton's container (the div in the Groundwater component
rendering the skeleton) to match the real card's responsive classes by changing
its className from a fixed "grid grid-cols-3 gap-6" to the responsive "grid
grid-cols-1 sm:grid-cols-3 gap-6" so the skeleton and actual content share the
same breakpoints.

---

Nitpick comments:
In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx`:
- Around line 48-56: The progress percentage is computed from the
already-rounded values currentOM and maxOM causing small inaccuracies; change
percentageOfMax to use the raw API values from carbonEstimates (e.g. use
carbonEstimates.a_som_loi and carbonEstimates.b_som_potential directly, with a
max check) to compute the ratio, then keep rounding only for display variables
(currentOM / maxOM) so the gauge/percentage label uses the precise ratio while
UI numbers remain rounded; update references to percentageOfMax accordingly.
- Around line 1-12: The import list in the CarbonSequestration component
includes unused icons Database, Target, and Zap; remove those three symbols from
the import statement (the line importing Leaf, Car, HelpCircle, ChevronDown,
Droplets, Sprout, Euro, Database, Target, Zap from "lucide-react") so only the
actually used icons remain (e.g., Leaf, Car, HelpCircle, ChevronDown, Droplets,
Sprout, Euro), and run a quick lint/compile to confirm no other references to
Database, Target, or Zap exist.

In `@fdm-app/app/components/blocks/atlas-fields/layout.tsx`:
- Around line 3-37: The lg:row-span-10 on the cultivationHistory sidebar in
FieldDetailsAtlasLayout is a brittle "magic number" used to force the sidebar to
span the right-side content; replace this with a more robust layout or at least
document the intent: either refactor the outer grid into a two-column container
(e.g., a nested grid/flex pair where the left column is the main flow and the
right column is a sticky sidebar so no arbitrary row-span is needed) or add an
inline comment next to the lg:row-span-10 reference explaining why the value is
required and when it should be revisited to prevent future regressions.

Comment thread fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx
Comment thread fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx Outdated
Comment thread fdm-app/app/components/blocks/atlas-fields/field-details.tsx
Comment thread fdm-app/app/components/blocks/atlas-fields/field-details.tsx
Comment thread fdm-app/app/components/blocks/atlas-fields/groundwater.tsx Outdated
@nmi-agro nmi-agro deleted a comment from coderabbitai Bot Feb 18, 2026
@SvenVw SvenVw requested a review from gerardhros February 18, 2026 16:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx (1)

45-67: Extract domain constants and document their sources.

The conversion factors 3.67 (C→CO₂), 8000 (km/ton CO₂ for a car), and 75 (EUR/ton CO₂ via EU ETS) are hard-coded without documentation. Especially the 75 EUR value is a volatile market price that will become stale — consider making it a named constant with a comment noting the source/date, so it's easy to find and update.

Suggested approach
+// Molecular weight ratio CO₂/C = 44/12 ≈ 3.67
+const C_TO_CO2_FACTOR = 3.67
+// Average EU passenger car ≈ 125 g CO₂/km → ~8 000 km per ton CO₂
+const CAR_KM_PER_TON_CO2 = 8000
+// EU ETS carbon price estimate (EUR/ton CO₂) — update periodically
+const EUR_PER_TON_CO2 = 75
+
 export function CarbonSequestrationCard({ carbonEstimates }: { ... }) {
     ...
-    const rawCO2eq = carbonEstimates.b_c_delta * 3.67
+    const rawCO2eq = carbonEstimates.b_c_delta * C_TO_CO2_FACTOR
     ...
-    const carKmEquivalent = Math.round(rawCO2eq * 8000)
+    const carKmEquivalent = Math.round(rawCO2eq * CAR_KM_PER_TON_CO2)
     ...
-    const euroEstimate = Math.round(rawCO2eq * 75)
+    const euroEstimate = Math.round(rawCO2eq * EUR_PER_TON_CO2)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx` around
lines 45 - 67, Extract the magic numbers into named constants (e.g., const
C_TO_CO2 = 3.67, CAR_KM_PER_TON_CO2 = 8000, EUR_PER_TON_CO2 = 75) and replace
usages in rawCO2eq, carKmEquivalent, and euroEstimate so the calculations
reference these constants; add inline comments for each constant indicating its
source and date (e.g., "C→CO2 conversion factor (atomic mass ratio) — source:
... , retrieved YYYY-MM-DD", "approx. car km per ton CO2 — source: ...",
"EUR/ton CO2 (EU ETS spot or representative price) — source and date, note that
this is volatile and may be configured elsewhere"); consider exporting
EUR_PER_TON_CO2 to a config or constants module if it may change at runtime or
require easy updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx`:
- Around line 149-161: The displayed plus sign with negative values comes from
rendering "+{potentialCO2eq}" (and similarly "~{carKmEquivalent}" and
"~{euroEstimate}") while b_c_delta can make these values ≤ 0; update the
rendering to either clamp the numeric values to zero (e.g., use Math.max(0,
potentialCO2eq), Math.max(0, carKmEquivalent), Math.max(0, euroEstimate)) or
compute a formatted string that conditionally prepends the "+" only for positive
numbers and shows "0" (or no sign) for non-positive values; adjust the JSX in
the CarbonSequestration component where potentialCO2eq, carKmEquivalent, and
euroEstimate are output to use the chosen clamping/formatting function so you
never render artifacts like "+-0.5" or "~ € -75".

---

Nitpick comments:
In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx`:
- Around line 45-67: Extract the magic numbers into named constants (e.g., const
C_TO_CO2 = 3.67, CAR_KM_PER_TON_CO2 = 8000, EUR_PER_TON_CO2 = 75) and replace
usages in rawCO2eq, carKmEquivalent, and euroEstimate so the calculations
reference these constants; add inline comments for each constant indicating its
source and date (e.g., "C→CO2 conversion factor (atomic mass ratio) — source:
... , retrieved YYYY-MM-DD", "approx. car km per ton CO2 — source: ...",
"EUR/ton CO2 (EU ETS spot or representative price) — source and date, note that
this is volatile and may be configured elsewhere"); consider exporting
EUR_PER_TON_CO2 to a config or constants module if it may change at runtime or
require easy updates.

Comment on lines +149 to +161
<div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground uppercase flex items-center gap-1">
CO₂-vastlegging
</p>
<div className="flex items-baseline gap-1">
<span className="text-xl font-bold">
+{potentialCO2eq}
</span>
<span className="text-xs text-muted-foreground">
ton CO₂eq/ha
</span>
</div>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Negative b_c_delta produces "+-" display artifacts.

If b_c_delta is ≤ 0 (current carbon stock already at or above potential), potentialCO2eq becomes negative, and Line 155 renders +{potentialCO2eq} → e.g. +-0.5 ton CO₂eq/ha. The same issue cascades to carKmEquivalent (Line 177: ~-800 km) and euroEstimate (Line 211: ~€ -75).

Consider either clamping to zero or conditionally formatting the sign:

Suggested fix (clamping approach)
-    const rawCO2eq = carbonEstimates.b_c_delta * 3.67
+    const rawCO2eq = Math.max(carbonEstimates.b_c_delta, 0) * 3.67
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx` around
lines 149 - 161, The displayed plus sign with negative values comes from
rendering "+{potentialCO2eq}" (and similarly "~{carKmEquivalent}" and
"~{euroEstimate}") while b_c_delta can make these values ≤ 0; update the
rendering to either clamp the numeric values to zero (e.g., use Math.max(0,
potentialCO2eq), Math.max(0, carKmEquivalent), Math.max(0, euroEstimate)) or
compute a formatted string that conditionally prepends the "+" only for positive
numbers and shows "0" (or no sign) for non-positive values; adjust the JSX in
the CarbonSequestration component where potentialCO2eq, carKmEquivalent, and
euroEstimate are output to use the chosen clamping/formatting function so you
never render artifacts like "+-0.5" or "~ € -75".

Comment thread fdm-app/app/components/blocks/atlas-fields/carbon-sequestration.tsx
Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx
Comment thread fdm-calculator/src/other/nlv-supply-by-som.ts
Comment thread issue-carbon-sequestration.md Outdated
Copy link
Copy Markdown
Collaborator

@gerardhros gerardhros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature. it might be connected to the C-balance later so that there you directly see what and how the C-balance contribute to the roadmap to SOM limits. A few small questions left.

@SvenVw SvenVw merged commit 04e6e21 into development Feb 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch:development Issue only affecting development, not the main branch (yet) enhancement New feature or request fdm-app fdm-calculator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Carbon Sequestration Card using NMI API Estimates data

2 participants