Skip to content

LF-5277: Add crop/animal association section to expense add and edit forms#4179

Merged
kathyavini merged 30 commits into
integrationfrom
LF-5277/Add_crop/animal_association_section_to_expense_add_and_edit_forms
May 22, 2026
Merged

LF-5277: Add crop/animal association section to expense add and edit forms#4179
kathyavini merged 30 commits into
integrationfrom
LF-5277/Add_crop/animal_association_section_to_expense_add_and_edit_forms

Conversation

@SayakaOno
Copy link
Copy Markdown
Collaborator

@SayakaOno SayakaOno commented May 21, 2026

Description

Add ExpenseEntitySection to the expense forms. I had to try several form patterns to make validation work correctly. The final approach simplifies the form structure using an allocations field array shared between crop and animal types. Data is converted from DB format to form values before display and back before submission.

{ …, allocations: [], entityType: ‘crop’ | ‘animal’ | null }

When entityType is switched or cleared, allocations is reset via useFieldArray.remove.

  • Add ExpenseEntitySection component (EntityAssociationToggle + allocation inputs)
  • Integrate ExpenseEntitySection into:
    • AddExpense (ExpenseItemInputs)
      • Wrap the component with FormProvider instead of passing form props through ExpenseItemsForType ➡️ ExpenseItemInputs
      • Story
    • PureExpenseDetail
      • Wrap the component with FormProvider
      • Format expense data from DB and set it as default form values
  • Fix NumberInput logic
  • Update CheckboxMultiSelect to accept noOptionsMessage
  • Update AnimalSaleInputs
    • Add “No options” message
    • Add “(removed)” label
  • Update CropSaleInputs
    • Add “No options” message
    • Use crop varieties instead of managementPlans (Remove the no longer used selector)
    • Extract label generation logic into formatCropVarietyLabel
  • Update AddExpense and ExpenseDetail containers
    • Pass cropVarietyOptions and animalOptions to pure components
    • Update handleSubmit to format allocations
  • Add cropVarietyOptionsSelector and animalOptionsSelector
    • animalApi file is expected to contain animal APIs in the future

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

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • 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)

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

SayakaOno and others added 27 commits May 20, 2026 11:58
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SayakaOno SayakaOno self-assigned this May 21, 2026
@SayakaOno SayakaOno added enhancement New feature or request new translations New translations to be sent to CrowdIn are present labels May 21, 2026
const rawInitialValue = field.value ?? get(formState.defaultValues, name) ?? defaultValue;
const { inputProps, numericValue, increment, decrement, clear } = useNumberInput({
initialValue: field.value || get(formState.defaultValues, name) || defaultValue,
initialValue: rawInitialValue === '' ? NaN : rawInitialValue,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

field.value can be '' (empty string) when RHF requires an empty initial state, but useNumberInput treats '' as 0 via isNaN (isNaN("") === false). Converting to NaN here ensures the input displays empty instead. Using ?? instead of || prevents 0 from being incorrectly ignored.

@SayakaOno SayakaOno marked this pull request as ready for review May 21, 2026 21:52
@SayakaOno SayakaOno requested review from a team as code owners May 21, 2026 21:52
@SayakaOno SayakaOno requested review from kathyavini and removed request for a team May 21, 2026 21:52
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

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

This is lovely on so many levels!! The new form addition looks and works perfectly, and the form <--> API logic is so clean and readable 🔥 And I love the new options selectors, thank you!

This is not for this PR, but did you talk to Loïc about what's expected in the Transaction List Expanded Content for these allocated expenses? I don't think I ever saw a design, but I don't love that there is no sign of allocation in the <GeneralTransactionTable>; that felt a bit jarring to add the breakdowns and then not see them again until re-opening the form:

Image Image

I don't know how he would prioritize that compared to e.g. the Weight/Unit quantities, but it seems like it would be a nice addition if we have some extra time left before release...!

);
const farmCropVarieties = useSelector(cropVarietiesSelector);

// Management plans determine which crop varieties are sale-eligible,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh a good example of a comment quickly becoming dead / junk, sorry. In fact it was a bad comment to start with because it was only in reference to moving off the old pattern 🙇

Could you please remove it? 🙏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oh sorry! Thank you for catching!!

crop_translation_key: mp.crop_translation_key,
crop_variety_photo_url: mp.crop_variety_photo_url,
for (const cv of farmCropVarieties ?? []) {
if (!(cv.crop_variety_id in tileDataById)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This if isn't necessary anymore now that cropVarietiesSelector is being used since there will only be one of each crop variety (unlike before with management plans).

Just curious -- what you you think about using the cropVarietyOptionsSelector here too though, and just building the tile data separately?

const options = useSelector(cropVarietyOptionsSelector);
const farmCropVarieties = useSelector(cropVarietiesSelector);

const cropVarietyTileDataById: Record<number, CropVarietySaleTileData> = useMemo(
  () =>
    Object.fromEntries(
      (farmCropVarieties ?? []).map((cv) => [
        cv.crop_variety_id,
        {
          crop_variety_name: cv.crop_variety_name,
          crop_translation_key: cv.crop_translation_key,
          crop_variety_photo_url: cv.crop_variety_photo_url,
        },
      ]),
    ),
  [farmCropVarieties],
);

I like the idea of revenue and expense using the same selector! The only thing is that the cropVarietyOptionsSelector is not currently sorted (while the optionList here was), but I wonder if sorting in the selector would be fine / good anyway, because it would then match the sorted animalsOptionsSelector?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added sorting in cropVarietyOptionsSelector and updated the useMemo!

value: generateInventoryId(AnimalOrBatchKeys.BATCH, batch),
label: generateSelectOptionLabel(batch),
}))
.sort((a, b) => String(a.label).localeCompare(String(b.label))),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm guessing that the whole array was meant to be sorted? And not just the batches?

I think the difference is functionally silent in Expenses (due to sortAllocations) but you can just barely notice it in Sales, if you set up your animals right 😉

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

What a mistake 😥 Thank you so much for catching!

@@ -0,0 +1,39 @@
/*
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini May 22, 2026

Choose a reason for hiding this comment

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

If we follow the current codebase convention, store/api/<entity>Api.ts has so far been used for API endpoints -- I believe this one looks like it should be in store/selectors/? Like the location.ts there?

Edit: oh sorry I just belatedly re-read this part of your description:

animalApi file is expected to contain animal APIs in the future

Does that mean that selectors/location.ts should be folded into the api/locationApi.ts as well? I don't know if I have a particularly strong preference but it would be nice to decide if selectors belong with the endpoints or not, going forward.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Did you look at Duncan's PR!?? I completely forgot about the new folder 😅
I've moved this file there!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Haha no I didn't 😂 I was actually looking for/remembering the old sensorSelectors.ts which I had once wrote a long time ago and put in store/api/selectors/. Actually I just now noticed that in addition to removing that file, the locations RTK Query upgrade moved that folder up a level into store/selectors 🤷

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I see! I'm sorry I didn't notice the folder path had changed!!

Copy link
Copy Markdown
Collaborator Author

@SayakaOno SayakaOno left a comment

Choose a reason for hiding this comment

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

Thank you so much for reviewing Joyce!!
I didn't ask Loïc about the allocations in the transaction list, but we can ask him on Wednesday!
Thank you for re-reviewing in advance 🙏

);
const farmCropVarieties = useSelector(cropVarietiesSelector);

// Management plans determine which crop varieties are sale-eligible,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oh sorry! Thank you for catching!!

crop_translation_key: mp.crop_translation_key,
crop_variety_photo_url: mp.crop_variety_photo_url,
for (const cv of farmCropVarieties ?? []) {
if (!(cv.crop_variety_id in tileDataById)) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added sorting in cropVarietyOptionsSelector and updated the useMemo!

@@ -0,0 +1,39 @@
/*
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Did you look at Duncan's PR!?? I completely forgot about the new folder 😅
I've moved this file there!

value: generateInventoryId(AnimalOrBatchKeys.BATCH, batch),
label: generateSelectOptionLabel(batch),
}))
.sort((a, b) => String(a.label).localeCompare(String(b.label))),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

What a mistake 😥 Thank you so much for catching!

Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

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

❤️

@kathyavini kathyavini added this pull request to the merge queue May 22, 2026
Merged via the queue into integration with commit 42676f9 May 22, 2026
6 of 7 checks passed
@SayakaOno SayakaOno deleted the LF-5277/Add_crop/animal_association_section_to_expense_add_and_edit_forms branch May 22, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new translations New translations to be sent to CrowdIn are present

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants