From 9b6319ab480178b6482262104794ad701673129f Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:41:03 -0700 Subject: [PATCH 01/36] LF-5274 Step 17: Add AnimalSaleItem component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors CropSaleItem contract. Renders the animal/batch name label above a SaleLineItem for the shared quantity + sale value inputs. No image or tile (the animal/batch row carries only a string label). Does not register the entity-id field itself — that registration is performed by SaleLineItem via entityIdFieldKey. Co-Authored-By: Claude Opus 4.7 --- .../Forms/GeneralRevenue/AnimalSaleItem.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx b/packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx new file mode 100644 index 0000000000..7b89c4cd8c --- /dev/null +++ b/packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2026 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { Semibold } from '../../Typography'; +import SaleLineItem from './SaleLineItem'; +import styles from './styles.module.scss'; + +interface AnimalSaleItemProps { + animalName: string; + entityId: string; + system: string; + currency: string; + fieldPrefix: string; + entityIdFieldKey: string; + disabledInput: boolean; +} + +function AnimalSaleItem({ + animalName, + entityId, + system, + currency, + fieldPrefix, + entityIdFieldKey, + disabledInput, +}: AnimalSaleItemProps) { + return ( +
+
+ {animalName} + +
+
+ ); +} + +export default AnimalSaleItem; From da269b4e71d120d28c36fde58935a1f55cc1c340 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:45:30 -0700 Subject: [PATCH 02/36] LF-5274 Step 21: Add ANIMAL_SALE and ANIMAL_KEY constants ANIMAL_SALE matches the backend animal_sale junction table name and is the RHF field-prefix key for the animal sale input section. ANIMAL_KEY is the per-row entity-id field name; it stays generic (not animal_id) because option values are prefixed strings encoding either an animal_id or animal_batch_id on the same row. Also adds ANIMAL_SALE to REVENUE_FORM_TYPES so util.getRevenueFormType can dispatch on it. Co-Authored-By: Claude Opus 4.7 --- .../webapp/src/components/Forms/GeneralRevenue/constants.js | 4 ++++ packages/webapp/src/containers/Finances/constants.js | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/constants.js b/packages/webapp/src/components/Forms/GeneralRevenue/constants.js index 8ff2e31381..e163898501 100644 --- a/packages/webapp/src/components/Forms/GeneralRevenue/constants.js +++ b/packages/webapp/src/components/Forms/GeneralRevenue/constants.js @@ -30,3 +30,7 @@ export const CROP_VARIETY_ID = 'crop_variety_id'; export const QUANTITY = 'quantity'; export const QUANTITY_UNIT = 'quantity_unit'; export const SALE_VALUE = 'sale_value'; + +// animal sale +export const ANIMAL_SALE = 'animal_sale'; +export const ANIMAL_KEY = 'animal_key'; diff --git a/packages/webapp/src/containers/Finances/constants.js b/packages/webapp/src/containers/Finances/constants.js index 9ef9152f0a..12dda46e0d 100644 --- a/packages/webapp/src/containers/Finances/constants.js +++ b/packages/webapp/src/containers/Finances/constants.js @@ -35,6 +35,7 @@ export const SET_IS_FETCHING_DATA = 'SET_IS_FETCHING_DATA'; export const REVENUE_FORM_TYPES = { CROP_SALE: 'crop_sale', + ANIMAL_SALE: 'animal_sale', GENERAL: 'general', }; From 1e90431f2047e7a24ed8279f8fc1b84e590f7fb5 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:45:53 -0700 Subject: [PATCH 03/36] LF-5274 Step 18: Add AnimalSaleInputs container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the simplified CropSaleInputs interface ({ sale?, disabledInput }). The component only mounts when the selected revenue type has entity_type === 'animal' (gated upstream by GeneralRevenue), so no isActive flag and no skip on the RTK Query hooks. Animals and batches are merged into a single option list with values prefixed (animal_${id} / batch_${id}) so a single CheckboxMultiSelect can represent both kinds of entity on one row. The decode back to animal_id / animal_batch_id happens in util.js. Exports getAnimalSaleDefaultValues for the parent form's customFormChildrenDefaultValues. quantity_unit is reshaped from a string into a SelectOption via getUnitOptionMap so the Unit dropdown populates correctly in edit/read-only views — same shape as getCropSaleDefaultValues. Co-Authored-By: Claude Opus 4.7 --- .../containers/Finances/AnimalSaleInputs.tsx | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx diff --git a/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx new file mode 100644 index 0000000000..a0a7aff314 --- /dev/null +++ b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2026 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ANIMAL_KEY, ANIMAL_SALE } from '../../components/Forms/GeneralRevenue/constants'; +import AnimalSaleItem from '../../components/Forms/GeneralRevenue/AnimalSaleItem'; +import EntitySaleInputs from './EntitySaleInputs'; +import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../store/api/apiSlice'; +import { chooseIdentification } from '../Animals/utils'; +import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; +import type { Animal, AnimalBatch } from '../../store/api/types'; +import type { SelectOption } from '../../components/Form/ReactSelect/CheckboxMultiSelect'; + +interface AnimalSaleRecord { + animal_id: number | null; + animal_batch_id: number | null; + quantity: number; + quantity_unit: string | undefined; + sale_value: number; +} + +interface AnimalSale { + animal_sale?: AnimalSaleRecord[]; +} + +interface AnimalSaleInputsProps { + sale?: AnimalSale; + disabledInput: boolean; +} + +const animalOptionKey = (id: number) => `animal_${id}`; +const batchOptionKey = (id: number) => `batch_${id}`; + +const saleRecordToOptionKey = (record: AnimalSaleRecord) => + record.animal_id != null + ? animalOptionKey(record.animal_id) + : batchOptionKey(record.animal_batch_id as number); + +export const getAnimalSaleDefaultValues = (sale: AnimalSale | undefined) => { + const existingSales = sale?.animal_sale?.reduce< + Record & { quantity_unit?: SelectOption }> + >((acc, cur) => { + const key = saleRecordToOptionKey(cur); + acc[key] = { + animal_id: cur.animal_id, + animal_batch_id: cur.animal_batch_id, + quantity: cur.quantity, + quantity_unit: cur.quantity_unit + ? ((getUnitOptionMap() as Record)[cur.quantity_unit] ?? { + label: cur.quantity_unit, + value: cur.quantity_unit, + }) + : undefined, + sale_value: cur.sale_value, + }; + return acc; + }, {}); + return { + [ANIMAL_SALE]: existingSales ?? undefined, + }; +}; + +export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInputsProps) { + const { t } = useTranslation(); + const { data: animals } = useGetAnimalsQuery(); + const { data: animalBatches } = useGetAnimalBatchesQuery(); + + const options = useMemo(() => { + const list: SelectOption[] = []; + (animals ?? []).forEach((a: Animal) => { + list.push({ label: chooseIdentification(a), value: animalOptionKey(a.id) }); + }); + (animalBatches ?? []).forEach((b: AnimalBatch) => { + list.push({ label: chooseIdentification(b), value: batchOptionKey(b.id) }); + }); + list.sort((a, b) => String(a.label).localeCompare(String(b.label))); + return list; + }, [animals, animalBatches]); + + const savedSalesById = sale?.animal_sale?.reduce>( + (acc, cur) => ({ ...acc, [saleRecordToOptionKey(cur)]: cur }), + {}, + ); + + return ( + + {({ option, system, currency, disabledInput }) => ( + + )} + + ); +} From c820543f258fe70dc65bafcac6e261a3e6fb7380 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:48:26 -0700 Subject: [PATCH 04/36] LF-5274 Step 19: Add RevenueSaleInputs dispatcher; rename EntitySaleInputs -> EntitySaleRows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rows component (CheckboxMultiSelect + per-entity rows) is renamed to EntitySaleRows so the more semantically correct name EntitySaleInputs is conceptually free, with the actual dispatcher named RevenueSaleInputs to align with the revenue domain. RevenueSaleInputs receives the full prop set from GeneralRevenue ({ sale, disabledInput, revenueTypes, selectedTypeOption }), derives the entity_type, and conditionally renders CropSaleInputs or AnimalSaleInputs. It returns null as a defensive fallback — GeneralRevenue already gates on selectedRevenueType?.entity_type so this path is unreachable in practice. Also exports getRevenueSaleDefaultValues(sale, entityType) so the default-values switch lives in one place rather than being duplicated in RevenueDetail and AddSale. Co-Authored-By: Claude Opus 4.7 --- .../containers/Finances/AnimalSaleInputs.tsx | 6 +- .../containers/Finances/CropSaleInputs.tsx | 6 +- ...ntitySaleInputs.tsx => EntitySaleRows.tsx} | 6 +- .../containers/Finances/RevenueSaleInputs.tsx | 66 +++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) rename packages/webapp/src/containers/Finances/{EntitySaleInputs.tsx => EntitySaleRows.tsx} (97%) create mode 100644 packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx diff --git a/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx index a0a7aff314..ab30248d4b 100644 --- a/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx @@ -17,7 +17,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ANIMAL_KEY, ANIMAL_SALE } from '../../components/Forms/GeneralRevenue/constants'; import AnimalSaleItem from '../../components/Forms/GeneralRevenue/AnimalSaleItem'; -import EntitySaleInputs from './EntitySaleInputs'; +import EntitySaleRows from './EntitySaleRows'; import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../store/api/apiSlice'; import { chooseIdentification } from '../Animals/utils'; import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; @@ -96,7 +96,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu ); return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/CropSaleInputs.tsx index f5c8a40292..502c94090c 100644 --- a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/CropSaleInputs.tsx @@ -22,7 +22,7 @@ import { } from '../../components/Forms/GeneralRevenue/constants'; import CropSaleItem from '../../components/Forms/GeneralRevenue/CropSaleItem'; import { selectManagementPlansForSale } from '../managementPlanSlice'; -import EntitySaleInputs from './EntitySaleInputs'; +import EntitySaleRows from './EntitySaleRows'; import type { CropVarietySaleTileData } from '../../components/CropTile/CropVarietySaleTile'; import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; import type { SelectOption } from '../../components/Form/ReactSelect/CheckboxMultiSelect/index'; @@ -104,7 +104,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr ); return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleRows.tsx similarity index 97% rename from packages/webapp/src/containers/Finances/EntitySaleInputs.tsx rename to packages/webapp/src/containers/Finances/EntitySaleRows.tsx index f910137ecc..f056f57c7c 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleRows.tsx @@ -38,7 +38,7 @@ export interface EntitySaleItemProps { disabledInput: boolean; } -interface EntitySaleInputsProps { +interface EntitySaleRowsProps { disabledInput: boolean; options: SelectOption[]; savedSalesById: Record | null | undefined; @@ -48,7 +48,7 @@ interface EntitySaleInputsProps { children: (props: EntitySaleItemProps) => ReactNode; } -export default function EntitySaleInputs({ +export default function EntitySaleRows({ disabledInput, options, savedSalesById, @@ -56,7 +56,7 @@ export default function EntitySaleInputs({ entityIdFieldKey, placeholder, children, -}: EntitySaleInputsProps): ReactNode { +}: EntitySaleRowsProps): ReactNode { const { t } = useTranslation(); const system = useSelector(measurementSelector); const currency = useCurrencySymbol(); diff --git a/packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx b/packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx new file mode 100644 index 0000000000..642b734902 --- /dev/null +++ b/packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2026 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import CropSaleInputs, { getCropSaleDefaultValues } from './CropSaleInputs'; +import AnimalSaleInputs, { getAnimalSaleDefaultValues } from './AnimalSaleInputs'; + +interface RevenueType { + revenue_type_id: number; + entity_type?: 'crop' | 'animal' | 'none' | null; +} + +interface RevenueTypeOption { + value: number; + label: string; +} + +interface RevenueSaleInputsProps { + sale?: any; + disabledInput: boolean; + revenueTypes?: RevenueType[]; + selectedTypeOption?: RevenueTypeOption; +} + +export const getRevenueSaleDefaultValues = ( + sale: any, + entityType: RevenueType['entity_type'] | undefined, +) => { + if (entityType === 'crop') { + return getCropSaleDefaultValues(sale); + } + if (entityType === 'animal') { + return getAnimalSaleDefaultValues(sale); + } + return undefined; +}; + +export default function RevenueSaleInputs({ + sale, + disabledInput, + revenueTypes, + selectedTypeOption, +}: RevenueSaleInputsProps) { + const entityType = revenueTypes?.find( + (rt) => rt.revenue_type_id === selectedTypeOption?.value, + )?.entity_type; + + if (entityType === 'crop') { + return ; + } + if (entityType === 'animal') { + return ; + } + return null; +} From 9f99348c82d666d70f8c4598fbfa266be4a4d45a Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:50:43 -0700 Subject: [PATCH 05/36] LF-5274 Step 20: Add animal branches to util.js getRevenueFormType: dispatch entity_type === 'animal' to REVENUE_FORM_TYPES.ANIMAL_SALE. mapRevenueFormDataToApiCallFormat: add an animal branch that decodes the prefixed animal_key form value back to either animal_id or animal_batch_id (exactly one is non-null per row, mirroring the backend CHECK constraint). mapSalesToRevenueItems: add an animal branch that displays each animal_sale row similarly to crop_variety_sale rows (title from chooseIdentification, subtitle as quantity + unit, amount as sale_value). Extends the signature with optional animals / animalBatches arrays; the caller in useTransactions wires them in Step 24. Co-Authored-By: Claude Opus 4.7 --- .../webapp/src/containers/Finances/util.js | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index 7f5d0c54d6..ec01149944 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -17,6 +17,8 @@ import { groupBy as lodashGroupBy } from 'lodash-es'; import moment from 'moment'; import { useTranslation } from 'react-i18next'; import { + ANIMAL_KEY, + ANIMAL_SALE, CROP_VARIETY_ID, CROP_VARIETY_SALE, CUSTOMER_NAME, @@ -28,6 +30,7 @@ import { SALE_VALUE, VALUE, } from '../../components/Forms/GeneralRevenue/constants'; +import { chooseIdentification } from '../Animals/utils'; import i18n from '../../locales/i18n'; import { getMass, getMassUnit, roundToTwoDecimal } from '../../util'; import { isSameDay } from '../../util/date-migrate-TS'; @@ -95,9 +98,13 @@ export function calcActualRevenueFromRevenueItems(revenueItems) { } export const getRevenueFormType = (revenueType) => { - return revenueType?.entity_type === 'crop' - ? REVENUE_FORM_TYPES.CROP_SALE - : REVENUE_FORM_TYPES.GENERAL; + if (revenueType?.entity_type === 'crop') { + return REVENUE_FORM_TYPES.CROP_SALE; + } + if (revenueType?.entity_type === 'animal') { + return REVENUE_FORM_TYPES.ANIMAL_SALE; + } + return REVENUE_FORM_TYPES.GENERAL; }; export const mapTasksToLabourItems = (tasks, taskTypes, users) => { @@ -157,7 +164,13 @@ export const mapTasksToLabourItems = (tasks, taskTypes, users) => { return labourItemGroups; }; -export const mapSalesToRevenueItems = (sales, revenueTypes, cropVarieties) => { +export const mapSalesToRevenueItems = ( + sales, + revenueTypes, + cropVarieties, + animals = [], + animalBatches = [], +) => { const revenueItems = sales.map((sale) => { const revenueType = revenueTypes.find( (revenueType) => revenueType.revenue_type_id === sale.revenue_type_id, @@ -188,6 +201,33 @@ export const mapSalesToRevenueItems = (sales, revenueTypes, cropVarieties) => { }; }), }; + } else if (revenueType?.entity_type === 'animal') { + const quantityUnit = getMassUnit(); + const animalSale = sale.animal_sale ?? []; + return { + sale, + totalAmount: animalSale.reduce((total, row) => total + row.sale_value, 0), + financeItemsProps: animalSale.map((row) => { + const convertedQuantity = roundToTwoDecimal(getMass(row.quantity).toString()); + const matched = + row.animal_id != null + ? animals.find((a) => a.id === row.animal_id) + : animalBatches.find((b) => b.id === row.animal_batch_id); + const title = matched + ? chooseIdentification(matched) + : (row.animal_id ?? row.animal_batch_id); + const key = + row.animal_id != null ? `animal_${row.animal_id}` : `batch_${row.animal_batch_id}`; + return { + key, + title, + subtitle: `${convertedQuantity} ${quantityUnit}`, + quantity: convertedQuantity, + quantityUnit, + amount: row.sale_value, + }; + }), + }; } else { return { sale, @@ -242,6 +282,20 @@ export function mapRevenueFormDataToApiCallFormat(data, revenueTypes, sale_id, f crop_variety_id: c[CROP_VARIETY_ID], }; }); + } else if (revenueType?.entity_type === 'animal') { + sale.value = undefined; + sale.animal_sale = Object.values(data[ANIMAL_SALE]).map((a) => { + const key = a[ANIMAL_KEY]; + const isBatch = typeof key === 'string' && key.startsWith('batch_'); + const id = parseInt(String(key).split('_')[1], 10); + return { + sale_value: a[SALE_VALUE], + quantity: a[QUANTITY], + quantity_unit: a[QUANTITY_UNIT].label, + animal_id: isBatch ? null : id, + animal_batch_id: isBatch ? id : null, + }; + }); } else { sale.crop_variety_sale = undefined; sale.value = data[VALUE]; From 779bc71a92ddc9d393a925edcb8516d2936dee1c Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:51:14 -0700 Subject: [PATCH 06/36] LF-5274 Step 22: Wire RevenueDetail to RevenueSaleInputs Replaces CropSaleInputs with the new RevenueSaleInputs dispatcher so the edit/read-only flow renders the correct entity-specific inputs based on the sale's revenue type. The entity_type switch for default values now lives in getRevenueSaleDefaultValues, removing the isCropSale ternary here. Co-Authored-By: Claude Opus 4.7 --- .../containers/Finances/RevenueDetail/index.jsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 9a8473b146..b44aa8fe92 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -23,13 +23,9 @@ import { useTranslation } from 'react-i18next'; import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { setPersistedPaths } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import CropSaleInputs, { getCropSaleDefaultValues } from '../CropSaleInputs'; +import RevenueSaleInputs, { getRevenueSaleDefaultValues } from '../RevenueSaleInputs'; import useHookFormPersist from '../../hooks/useHookFormPersist'; -import { - isCropSale, - mapRevenueFormDataToApiCallFormat, - mapRevenueTypesToReactSelectOptions, -} from '../util'; +import { mapRevenueFormDataToApiCallFormat, mapRevenueTypesToReactSelectOptions } from '../util'; import useSortedRevenueTypes from '../AddSale/RevenueTypes/useSortedRevenueTypes'; import { REVENUE_TYPE_OPTION } from '../../../components/Forms/GeneralRevenue/constants'; import { createEditRevenueDetailsUrl } from '../../../util/siteMapConstants'; @@ -88,10 +84,8 @@ function RevenueDetail() { title={isEditing ? t('SALE.EDIT_SALE.TITLE') : t('SALE.DETAIL.TITLE')} currency={useCurrencySymbol()} sale={sale} - CustomFormChildren={CropSaleInputs} - customFormChildrenDefaultValues={ - isCropSale(revenueType) ? getCropSaleDefaultValues(sale) : undefined - } + CustomFormChildren={RevenueSaleInputs} + customFormChildrenDefaultValues={getRevenueSaleDefaultValues(sale, revenueType?.entity_type)} view={isEditing ? 'edit' : 'read-only'} handleGoBack={handleGoBack} onClick={isEditing ? undefined : handleEdit} From c2cb54aeb42cb6c308385be971747e040a353392 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:51:48 -0700 Subject: [PATCH 07/36] LF-5274 Step 23: Wire AddSale to RevenueSaleInputs; unblock ANIMAL_SALE type AddSale now uses the RevenueSaleInputs dispatcher so newly created sales render the entity-specific inputs depending on the selected revenue type. Also removes the temporary getRevenueTypesSaga filter that hid the seeded ANIMAL_SALE revenue type from the dropdown. The frontend now fully supports animal sales end-to-end, so the filter is obsolete. Co-Authored-By: Claude Opus 4.7 --- packages/webapp/src/containers/Finances/AddSale/index.jsx | 4 ++-- packages/webapp/src/containers/Finances/saga.js | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/webapp/src/containers/Finances/AddSale/index.jsx b/packages/webapp/src/containers/Finances/AddSale/index.jsx index 939d9bda59..5f148b2218 100644 --- a/packages/webapp/src/containers/Finances/AddSale/index.jsx +++ b/packages/webapp/src/containers/Finances/AddSale/index.jsx @@ -15,7 +15,7 @@ import React from 'react'; import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import CropSaleInputs from '../CropSaleInputs'; +import RevenueSaleInputs from '../RevenueSaleInputs'; import { addSale } from '../actions'; import { userFarmSelector } from '../../userFarmSlice'; import { useDispatch, useSelector } from 'react-redux'; @@ -59,7 +59,7 @@ function AddSale() { interpolation: { escapeValue: false }, })} currency={useCurrencySymbol()} - CustomFormChildren={CropSaleInputs} + CustomFormChildren={RevenueSaleInputs} view={'add'} handleGoBack={handleGoBack} buttonText={t('common:SAVE')} diff --git a/packages/webapp/src/containers/Finances/saga.js b/packages/webapp/src/containers/Finances/saga.js index f7c634f659..18af899bb6 100644 --- a/packages/webapp/src/containers/Finances/saga.js +++ b/packages/webapp/src/containers/Finances/saga.js @@ -367,13 +367,7 @@ export function* getRevenueTypesSaga() { try { const result = yield call(axios.get, `${revenueTypeUrl}/farm/${farm_id}`, header); - - // TODO LF-5274: Remove filter when ANIMAL_SALE is supported - const formattedResult = result.data.filter(({ farm_id, revenue_translation_key }) => { - return !!farm_id || revenue_translation_key !== 'ANIMAL_SALE'; - }); - - yield put(getRevenueTypesSuccess(formattedResult)); + yield put(getRevenueTypesSuccess(result.data)); } catch (e) { console.log('failed to fetch revenue types from database'); } From 7469dbbfb369d54835a302225af63cd2aa7fd79a Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 13:53:06 -0700 Subject: [PATCH 08/36] LF-5274 Step 24: Add animalRevenue transaction type Adds transactionTypeEnum.animalRevenue and extends buildRevenueTransactions to dispatch animal-typed sales to it. useTransactions now also reads animals and animal batches from the RTK Query cache and forwards them to mapSalesToRevenueItems so each animal_sale row in the transaction list can show the animal's identification label as its title. Co-Authored-By: Claude Opus 4.7 --- .../containers/Finances/useTransactions.js | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/webapp/src/containers/Finances/useTransactions.js b/packages/webapp/src/containers/Finances/useTransactions.js index 068264c6a9..e76d993749 100644 --- a/packages/webapp/src/containers/Finances/useTransactions.js +++ b/packages/webapp/src/containers/Finances/useTransactions.js @@ -25,6 +25,7 @@ import { allRevenueTypesSelector } from '../revenueTypeSlice'; import { tasksSelector } from '../taskSlice'; import { taskTypesSelector } from '../taskTypeSlice'; import { userFarmsByFarmSelector } from '../userFarmSlice'; +import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../store/api/apiSlice'; import { LABOUR_ITEMS_GROUPING_OPTIONS } from './constants'; import { allExpenseTypeSelector, expenseSelector, salesSelector } from './selectors'; import { isCropSale, mapSalesToRevenueItems, mapTasksToLabourItems } from './util'; @@ -34,6 +35,7 @@ export const transactionTypeEnum = { labourExpense: 'LABOUR_EXPENSE', revenue: 'REVENUE', cropRevenue: 'CROP_REVENUE', + animalRevenue: 'ANIMAL_REVENUE', }; // Polyfill for tests and older browsers @@ -116,7 +118,7 @@ const buildExpenseTransactions = ({ expenses, expenseTypes, dateFilter, expenseT (expenseType) => expenseType?.expense_type_id === expense.expense_type_id, ); return { - icon: expenseType?.farm_id ? 'OTHER' : expenseType?.expense_translation_key ?? 'OTHER', + icon: expenseType?.farm_id ? 'OTHER' : (expenseType?.expense_translation_key ?? 'OTHER'), date: expense.expense_date, transactionType: transactionTypeEnum.expense, typeLabel: getExpenseTypeLabel(expenseType), @@ -127,10 +129,22 @@ const buildExpenseTransactions = ({ expenses, expenseTypes, dateFilter, expenseT }); }; +const getRevenueTransactionType = (revenueType) => { + if (isCropSale(revenueType)) { + return transactionTypeEnum.cropRevenue; + } + if (revenueType?.entity_type === 'animal') { + return transactionTypeEnum.animalRevenue; + } + return transactionTypeEnum.revenue; +}; + const buildRevenueTransactions = ({ sales, revenueTypes, cropVarieties, + animals, + animalBatches, dateFilter, revenueTypeFilter, }) => { @@ -141,18 +155,22 @@ const buildRevenueTransactions = ({ moment(sale.sale_date).isSameOrBefore(dateFilter.endDate, 'day'))) && (!revenueTypeFilter || revenueTypeFilter[sale.revenue_type_id]?.active), ); - const revenueItems = mapSalesToRevenueItems(filteredSales, revenueTypes, cropVarieties); + const revenueItems = mapSalesToRevenueItems( + filteredSales, + revenueTypes, + cropVarieties, + animals, + animalBatches, + ); return revenueItems.map((item) => { const revenueType = revenueTypes.find( (revenueType) => revenueType?.revenue_type_id == item.sale.revenue_type_id, ); return { - icon: revenueType?.farm_id ? 'CUSTOM' : revenueType?.revenue_translation_key ?? 'CUSTOM', + icon: revenueType?.farm_id ? 'CUSTOM' : (revenueType?.revenue_translation_key ?? 'CUSTOM'), date: item.sale.sale_date, - transactionType: isCropSale(revenueType) - ? transactionTypeEnum.cropRevenue - : transactionTypeEnum.revenue, + transactionType: getRevenueTransactionType(revenueType), typeLabel: getRevenueTypeLabel(revenueType), amount: item.totalAmount, note: item.sale.customer_name, @@ -170,6 +188,8 @@ export const buildTransactions = ({ revenueTypes = [], taskTypes = [], cropVarieties = [], + animals = [], + animalBatches = [], users = [], dateFilter, expenseTypeFilter, @@ -188,6 +208,8 @@ export const buildTransactions = ({ sales, revenueTypes, cropVarieties, + animals, + animalBatches, dateFilter, revenueTypeFilter, }), @@ -207,6 +229,8 @@ const useTransactions = ({ dateFilter, expenseTypeFilter, revenueTypeFilter }) = const taskTypes = useSelector(taskTypesSelector); const cropVarieties = useSelector(cropVarietiesSelector); const users = useSelector(userFarmsByFarmSelector); + const { data: animals } = useGetAnimalsQuery(); + const { data: animalBatches } = useGetAnimalBatchesQuery(); const transactions = useMemo(() => { if (!expenseTypes?.length || !revenueTypes?.length) { @@ -221,6 +245,8 @@ const useTransactions = ({ dateFilter, expenseTypeFilter, revenueTypeFilter }) = revenueTypes, taskTypes, cropVarieties, + animals, + animalBatches, users, dateFilter, expenseTypeFilter, @@ -234,6 +260,8 @@ const useTransactions = ({ dateFilter, expenseTypeFilter, revenueTypeFilter }) = revenueTypes, taskTypes, cropVarieties, + animals, + animalBatches, users, buildTransactions, dateFilter, From 154d2ac0e9139964c7d7d05acdc881cd8ce55aae Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 14:55:46 -0700 Subject: [PATCH 09/36] LF-5274 Step 31: Add ANIMAL_SALE revenue translation keys Adds the en source keys for the seeded ANIMAL_SALE revenue type (REVENUE_NAME, CUSTOM_DESCRIPTION) and the UI strings used by the animal sale inputs (SALE.ADD_SALE.ANIMAL placeholder, ANIMAL_REQUIRED mirror of CROP_REQUIRED). Only the English source files are touched; the other locales are populated by the Crowdin pipeline. Co-Authored-By: Claude Opus 4.7 --- packages/webapp/public/locales/en/revenue.json | 4 ++++ packages/webapp/public/locales/en/translation.json | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/webapp/public/locales/en/revenue.json b/packages/webapp/public/locales/en/revenue.json index 3b198a6fe6..c4ff1016fc 100644 --- a/packages/webapp/public/locales/en/revenue.json +++ b/packages/webapp/public/locales/en/revenue.json @@ -1,4 +1,8 @@ { + "ANIMAL_SALE": { + "REVENUE_NAME": "Animal Sale", + "CUSTOM_DESCRIPTION": "Revenues associated with the sale of animals from this farm." + }, "CROP_SALE": { "REVENUE_NAME": "Crop Sale", "CUSTOM_DESCRIPTION": "Revenues associated with the sale of crops harvested from this farm." diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index b8e73bb4d7..9d3aa913bc 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1880,6 +1880,8 @@ "ADD_SALE": { "ADD_CUSTOM_REVENUE_TYPE": "Add custom revenue type", "ADD_REVENUE": "Add revenue", + "ANIMAL": "Animal", + "ANIMAL_REQUIRED": "Required", "CROP_REQUIRED": "Required", "CROP_VARIETY": "Crop variety", "FLOW": "revenue creation", From b235e9dc0fa6bfcc6a6a9a29bf20b652066e5b6c Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 15:17:51 -0700 Subject: [PATCH 10/36] LF-5274 Register ANIMAL_SALE into iconMap --- packages/webapp/src/components/Icons/icons.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/webapp/src/components/Icons/icons.tsx b/packages/webapp/src/components/Icons/icons.tsx index da92ed6146..91bc8d1b37 100644 --- a/packages/webapp/src/components/Icons/icons.tsx +++ b/packages/webapp/src/components/Icons/icons.tsx @@ -22,6 +22,7 @@ import { ReactComponent as ProfitLossIcon } from '../../assets/images/finance/Pr // Revenue types import { ReactComponent as CropSaleIcon } from '../../assets/images/finance/Crop-sale-icn.svg'; +import { ReactComponent as AnimalSaleIcon } from '../../assets/images/nav/animals.svg'; import { ReactComponent as CustomTypeIcon } from '../../assets/images/finance/Custom-revenue.svg'; // Expense types @@ -116,6 +117,7 @@ export const iconMap = { PROFIT_LOSS: ProfitLossIcon, // Revenue types CROP_SALE: CropSaleIcon, + ANIMAL_SALE: AnimalSaleIcon, CUSTOM: CustomTypeIcon, // Expense types EQUIPMENT: EquipIcon, From 781aa96d293ceabaa07d3fd408bab587116f4d5d Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Mon, 11 May 2026 15:43:28 -0700 Subject: [PATCH 11/36] LF-5274 Update GeneralRevenue and EntitySaleRows according to Figma Notes field showing as a TextArea now with entity-dependent placeholder; updated spacing; no
--- .../webapp/public/locales/en/translation.json | 3 ++ .../components/Forms/GeneralRevenue/index.jsx | 15 ++++++- .../Forms/GeneralRevenue/styles.module.scss | 34 ++++++++++----- .../containers/Finances/EntitySaleRows.tsx | 41 ++++++++++--------- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 9d3aa913bc..f422f0d996 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1881,11 +1881,14 @@ "ADD_CUSTOM_REVENUE_TYPE": "Add custom revenue type", "ADD_REVENUE": "Add revenue", "ANIMAL": "Animal", + "ANIMAL_NOTES_PLACEHOLDER": "Animal sale description", "ANIMAL_REQUIRED": "Required", + "CROP_NOTES_PLACEHOLDER": "Crop sale description", "CROP_REQUIRED": "Required", "CROP_VARIETY": "Crop variety", "FLOW": "revenue creation", "MANAGE_CUSTOM_REVENUE_TYPE": "Manage custom revenue types", + "NOTES_PLACEHOLDER": "Sale description", "SALE_VALUE_ERROR": "Sale value must be a positive number less than 999,999,999", "TABLE_HEADERS": { "TOTAL": "Total" diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/index.jsx b/packages/webapp/src/components/Forms/GeneralRevenue/index.jsx index 90d55f1d49..fd1ceb1daa 100644 --- a/packages/webapp/src/components/Forms/GeneralRevenue/index.jsx +++ b/packages/webapp/src/components/Forms/GeneralRevenue/index.jsx @@ -88,6 +88,17 @@ const GeneralRevenue = ({ (rt) => rt.revenue_type_id === selectedTypeOption?.value, ); + const notesPlaceholder = (() => { + const maxCharsSuffix = ` - ${t('common:MAX_CHARS', { value: 125 })}`; + if (selectedRevenueType?.entity_type === 'crop') { + return t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') + maxCharsSuffix; + } + if (selectedRevenueType?.entity_type === 'animal') { + return t('SALE.ADD_SALE.ANIMAL_NOTES_PLACEHOLDER') + maxCharsSuffix; + } + return t('SALE.ADD_SALE.NOTES_PLACEHOLDER'); + })(); + useEffect(() => { if (revenueTypeOptions?.length && !selectedTypeOption) { setValue( @@ -161,8 +172,10 @@ const GeneralRevenue = ({ style={{ marginBottom: '40px' }} label={t('LOG_COMMON.NOTES')} optional={true} - hookFormRegister={register(NOTE, { maxLength: hookFormMaxCharsValidation(10000) })} + hookFormRegister={register(NOTE, { maxLength: hookFormMaxCharsValidation(125) })} name={NOTE} + placeholder={notesPlaceholder} + minRows={5} errors={getInputErrors(errors, NOTE)} disabled={disabledInput} /> diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss b/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss index aa62d02c9d..10944baa72 100644 --- a/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss +++ b/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss @@ -108,22 +108,34 @@ .sale { display: flex; } -.saleItemContainer { +.entitySaleRows { display: flex; - margin-top: 32px; - margin-bottom: 32px; + flex-direction: column; + gap: 16px; } -.saleItemInputGroup { - min-width: 200px; - flex-grow: 1; - margin-left: 24px; +.selectorGroup { + display: flex; + flex-direction: column; + gap: 4px; +} + +.saleItemList { + display: flex; + flex-direction: column; + gap: 24px; + margin-top: 8px; } -.saleItemInputGroup > * { - margin-bottom: 40px; +.saleItemContainer { + display: flex; + gap: 24px; } -.selectionErrorZone { - height: 32px; +.saleItemInputGroup { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 200px; + flex-grow: 1; } diff --git a/packages/webapp/src/containers/Finances/EntitySaleRows.tsx b/packages/webapp/src/containers/Finances/EntitySaleRows.tsx index f056f57c7c..58765b2535 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleRows.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleRows.tsx @@ -94,27 +94,28 @@ export default function EntitySaleRows({ }; return ( - <> - -
+
+
+ {!isSelectionValid && {t('common:REQUIRED')}}
-
- {selectedOptions.map((option) => - children({ - option, - system, - currency, - fieldPrefix, - disabledInput, - }), - )} - +
+ {selectedOptions.map((option) => + children({ + option, + system, + currency, + fieldPrefix, + disabledInput, + }), + )} +
+
); } From 9dea14fb1c9f8867638e4bed6f5d4f889b70fc51 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 12 May 2026 11:55:53 -0700 Subject: [PATCH 12/36] LF-5274 Revert the EntitySaleInputs to EntitySaleRows rename --- .../components/Forms/GeneralRevenue/styles.module.scss | 2 +- .../webapp/src/containers/Finances/AnimalSaleInputs.tsx | 6 +++--- .../webapp/src/containers/Finances/CropSaleInputs.tsx | 6 +++--- .../Finances/{EntitySaleRows.tsx => EntitySaleInputs.tsx} | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename packages/webapp/src/containers/Finances/{EntitySaleRows.tsx => EntitySaleInputs.tsx} (95%) diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss b/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss index 10944baa72..7a54718afc 100644 --- a/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss +++ b/packages/webapp/src/components/Forms/GeneralRevenue/styles.module.scss @@ -108,7 +108,7 @@ .sale { display: flex; } -.entitySaleRows { +.EntitySaleInputs { display: flex; flex-direction: column; gap: 16px; diff --git a/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx index ab30248d4b..a0a7aff314 100644 --- a/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/AnimalSaleInputs.tsx @@ -17,7 +17,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { ANIMAL_KEY, ANIMAL_SALE } from '../../components/Forms/GeneralRevenue/constants'; import AnimalSaleItem from '../../components/Forms/GeneralRevenue/AnimalSaleItem'; -import EntitySaleRows from './EntitySaleRows'; +import EntitySaleInputs from './EntitySaleInputs'; import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../store/api/apiSlice'; import { chooseIdentification } from '../Animals/utils'; import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; @@ -96,7 +96,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu ); return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/CropSaleInputs.tsx index 502c94090c..f5c8a40292 100644 --- a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/CropSaleInputs.tsx @@ -22,7 +22,7 @@ import { } from '../../components/Forms/GeneralRevenue/constants'; import CropSaleItem from '../../components/Forms/GeneralRevenue/CropSaleItem'; import { selectManagementPlansForSale } from '../managementPlanSlice'; -import EntitySaleRows from './EntitySaleRows'; +import EntitySaleInputs from './EntitySaleInputs'; import type { CropVarietySaleTileData } from '../../components/CropTile/CropVarietySaleTile'; import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; import type { SelectOption } from '../../components/Form/ReactSelect/CheckboxMultiSelect/index'; @@ -104,7 +104,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr ); return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/EntitySaleRows.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx similarity index 95% rename from packages/webapp/src/containers/Finances/EntitySaleRows.tsx rename to packages/webapp/src/containers/Finances/EntitySaleInputs.tsx index 58765b2535..ab40b8f3af 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleRows.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx @@ -38,7 +38,7 @@ export interface EntitySaleItemProps { disabledInput: boolean; } -interface EntitySaleRowsProps { +interface EntitySaleInputsProps { disabledInput: boolean; options: SelectOption[]; savedSalesById: Record | null | undefined; @@ -48,7 +48,7 @@ interface EntitySaleRowsProps { children: (props: EntitySaleItemProps) => ReactNode; } -export default function EntitySaleRows({ +export default function EntitySaleInputs({ disabledInput, options, savedSalesById, @@ -56,7 +56,7 @@ export default function EntitySaleRows({ entityIdFieldKey, placeholder, children, -}: EntitySaleRowsProps): ReactNode { +}: EntitySaleInputsProps): ReactNode { const { t } = useTranslation(); const system = useSelector(measurementSelector); const currency = useCurrencySymbol(); @@ -94,7 +94,7 @@ export default function EntitySaleRows({ }; return ( -
+
Date: Tue, 12 May 2026 11:59:31 -0700 Subject: [PATCH 13/36] LF-5274 Rename RevenueSaleInputs --- packages/webapp/src/containers/Finances/AddSale/index.jsx | 4 ++-- .../webapp/src/containers/Finances/RevenueDetail/index.jsx | 4 ++-- .../{RevenueSaleInputs.tsx => SaleInputsByEntityType.tsx} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/webapp/src/containers/Finances/{RevenueSaleInputs.tsx => SaleInputsByEntityType.tsx} (93%) diff --git a/packages/webapp/src/containers/Finances/AddSale/index.jsx b/packages/webapp/src/containers/Finances/AddSale/index.jsx index 5f148b2218..c1862b0c16 100644 --- a/packages/webapp/src/containers/Finances/AddSale/index.jsx +++ b/packages/webapp/src/containers/Finances/AddSale/index.jsx @@ -15,7 +15,7 @@ import React from 'react'; import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import RevenueSaleInputs from '../RevenueSaleInputs'; +import SaleInputsByEntityType from '../SaleInputsByEntityType'; import { addSale } from '../actions'; import { userFarmSelector } from '../../userFarmSlice'; import { useDispatch, useSelector } from 'react-redux'; @@ -59,7 +59,7 @@ function AddSale() { interpolation: { escapeValue: false }, })} currency={useCurrencySymbol()} - CustomFormChildren={RevenueSaleInputs} + CustomFormChildren={SaleInputsByEntityType} view={'add'} handleGoBack={handleGoBack} buttonText={t('common:SAVE')} diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index b44aa8fe92..324daa19cd 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'; import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { setPersistedPaths } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import RevenueSaleInputs, { getRevenueSaleDefaultValues } from '../RevenueSaleInputs'; +import SaleInputsByEntityType, { getRevenueSaleDefaultValues } from '../SaleInputsByEntityType'; import useHookFormPersist from '../../hooks/useHookFormPersist'; import { mapRevenueFormDataToApiCallFormat, mapRevenueTypesToReactSelectOptions } from '../util'; import useSortedRevenueTypes from '../AddSale/RevenueTypes/useSortedRevenueTypes'; @@ -84,7 +84,7 @@ function RevenueDetail() { title={isEditing ? t('SALE.EDIT_SALE.TITLE') : t('SALE.DETAIL.TITLE')} currency={useCurrencySymbol()} sale={sale} - CustomFormChildren={RevenueSaleInputs} + CustomFormChildren={SaleInputsByEntityType} customFormChildrenDefaultValues={getRevenueSaleDefaultValues(sale, revenueType?.entity_type)} view={isEditing ? 'edit' : 'read-only'} handleGoBack={handleGoBack} diff --git a/packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx b/packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx similarity index 93% rename from packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx rename to packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx index 642b734902..46709b75f3 100644 --- a/packages/webapp/src/containers/Finances/RevenueSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx @@ -26,7 +26,7 @@ interface RevenueTypeOption { label: string; } -interface RevenueSaleInputsProps { +interface SaleInputsByEntityTypeProps { sale?: any; disabledInput: boolean; revenueTypes?: RevenueType[]; @@ -46,12 +46,12 @@ export const getRevenueSaleDefaultValues = ( return undefined; }; -export default function RevenueSaleInputs({ +export default function SaleInputsByEntityType({ sale, disabledInput, revenueTypes, selectedTypeOption, -}: RevenueSaleInputsProps) { +}: SaleInputsByEntityTypeProps) { const entityType = revenueTypes?.find( (rt) => rt.revenue_type_id === selectedTypeOption?.value, )?.entity_type; From f0fc25465e4f10603f40f508160b6031600fa9f6 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 12 May 2026 14:17:22 -0700 Subject: [PATCH 14/36] LF-5274 Rename and reorganization of components --- .../AnimalSaleItem.tsx | 0 .../CropSaleItem.jsx | 0 .../Forms/RevenueForm/EntitySaleEntries.tsx} | 26 ++++++++---------- .../SaleLineItem.tsx | 0 .../constants.js | 0 .../{GeneralRevenue => RevenueForm}/index.jsx | 27 +++++++++++-------- .../styles.module.scss | 2 +- .../src/containers/Finances/AddSale/index.jsx | 7 ++--- .../AnimalSaleInputs.tsx | 20 +++++++------- .../{ => EntitySaleInputs}/CropSaleInputs.tsx | 18 ++++++------- .../index.tsx} | 13 ++++----- .../Finances/RevenueDetail/index.jsx | 9 +++---- .../containers/Finances/useTransactions.js | 4 +-- .../webapp/src/containers/Finances/util.js | 4 ++- .../stories/Finances/CropSaleItem.stories.jsx | 2 +- ...me.stories.jsx => RevenueForm.stories.jsx} | 24 ++++++----------- 16 files changed, 71 insertions(+), 85 deletions(-) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/AnimalSaleItem.tsx (100%) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/CropSaleItem.jsx (100%) rename packages/webapp/src/{containers/Finances/EntitySaleInputs.tsx => components/Forms/RevenueForm/EntitySaleEntries.tsx} (82%) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/SaleLineItem.tsx (100%) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/constants.js (100%) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/index.jsx (93%) rename packages/webapp/src/components/Forms/{GeneralRevenue => RevenueForm}/styles.module.scss (99%) rename packages/webapp/src/containers/Finances/{ => EntitySaleInputs}/AnimalSaleInputs.tsx (85%) rename packages/webapp/src/containers/Finances/{ => EntitySaleInputs}/CropSaleInputs.tsx (86%) rename packages/webapp/src/containers/Finances/{SaleInputsByEntityType.tsx => EntitySaleInputs/index.tsx} (84%) rename packages/webapp/src/stories/Finances/{GeneralIncome.stories.jsx => RevenueForm.stories.jsx} (83%) diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx b/packages/webapp/src/components/Forms/RevenueForm/AnimalSaleItem.tsx similarity index 100% rename from packages/webapp/src/components/Forms/GeneralRevenue/AnimalSaleItem.tsx rename to packages/webapp/src/components/Forms/RevenueForm/AnimalSaleItem.tsx diff --git a/packages/webapp/src/components/Forms/GeneralRevenue/CropSaleItem.jsx b/packages/webapp/src/components/Forms/RevenueForm/CropSaleItem.jsx similarity index 100% rename from packages/webapp/src/components/Forms/GeneralRevenue/CropSaleItem.jsx rename to packages/webapp/src/components/Forms/RevenueForm/CropSaleItem.jsx diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx similarity index 82% rename from packages/webapp/src/containers/Finances/EntitySaleInputs.tsx rename to packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx index ab40b8f3af..039c1763b0 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs.tsx +++ b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx @@ -18,17 +18,13 @@ import { useFormContext } from 'react-hook-form'; import { MultiValue } from 'react-select'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { measurementSelector } from '../userFarmSlice'; -import { - QUANTITY, - QUANTITY_UNIT, - SALE_VALUE, -} from '../../components/Forms/GeneralRevenue/constants'; -import { CheckboxMultiSelect } from '../../components/Form/ReactSelect/CheckboxMultiSelect'; -import type { SelectOption } from '../../components/Form/ReactSelect/CheckboxMultiSelect'; -import { Error } from '../../components/Typography'; -import styles from '../../components/Forms/GeneralRevenue/styles.module.scss'; -import { useCurrencySymbol } from '../hooks/useCurrencySymbol'; +import { measurementSelector } from '../../../containers/userFarmSlice'; +import { QUANTITY, QUANTITY_UNIT, SALE_VALUE } from './constants'; +import { CheckboxMultiSelect } from '../../Form/ReactSelect/CheckboxMultiSelect'; +import type { SelectOption } from '../../Form/ReactSelect/CheckboxMultiSelect'; +import { Error } from '../../Typography'; +import styles from './styles.module.scss'; +import { useCurrencySymbol } from '../../../containers/hooks/useCurrencySymbol'; export interface EntitySaleItemProps { option: SelectOption; @@ -38,7 +34,7 @@ export interface EntitySaleItemProps { disabledInput: boolean; } -interface EntitySaleInputsProps { +interface EntitySaleEntriesProps { disabledInput: boolean; options: SelectOption[]; savedSalesById: Record | null | undefined; @@ -48,7 +44,7 @@ interface EntitySaleInputsProps { children: (props: EntitySaleItemProps) => ReactNode; } -export default function EntitySaleInputs({ +export default function EntitySaleEntries({ disabledInput, options, savedSalesById, @@ -56,7 +52,7 @@ export default function EntitySaleInputs({ entityIdFieldKey, placeholder, children, -}: EntitySaleInputsProps): ReactNode { +}: EntitySaleEntriesProps): ReactNode { const { t } = useTranslation(); const system = useSelector(measurementSelector); const currency = useCurrencySymbol(); @@ -94,7 +90,7 @@ export default function EntitySaleInputs({ }; return ( -
+
. */ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useForm, Controller, FormProvider } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import Form from '../../Form'; @@ -35,8 +35,12 @@ import { REVENUE_TYPE_ID, } from './constants'; import PropTypes from 'prop-types'; +import { isEntitySale } from '../../../containers/Finances/util'; +import EntitySaleInputs, { + getEntitySaleDefaultValues, +} from '../../../containers/Finances/EntitySaleInputs'; -const GeneralRevenue = ({ +const RevenueForm = ({ onSubmit, title, currency, @@ -48,8 +52,6 @@ const GeneralRevenue = ({ revenueTypeOptions, onTypeChange, buttonText, - customFormChildrenDefaultValues, - CustomFormChildren, revenueTypes, onRetire, }) => { @@ -58,6 +60,11 @@ const GeneralRevenue = ({ const data = sale || persistedFormData; + const initialRevenueType = revenueTypes?.find( + (rt) => rt.revenue_type_id === data[REVENUE_TYPE_ID], + ); + const entitySaleDefaults = getEntitySaleDefaultValues(sale, initialRevenueType?.entity_type); + const reactHookFormFunctions = useForm({ mode: 'onChange', defaultValues: { @@ -70,7 +77,7 @@ const GeneralRevenue = ({ }), [VALUE]: !isNaN(data[VALUE]) ? data[VALUE] : null, [NOTE]: data[NOTE] ?? null, - ...customFormChildrenDefaultValues, + ...entitySaleDefaults, }, }); @@ -200,8 +207,8 @@ const GeneralRevenue = ({ )} /> )} - {CustomFormChildren && selectedRevenueType?.entity_type ? ( - . */ -import React from 'react'; -import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import SaleInputsByEntityType from '../SaleInputsByEntityType'; +import RevenueForm from '../../../components/Forms/RevenueForm'; import { addSale } from '../actions'; import { userFarmSelector } from '../../userFarmSlice'; import { useDispatch, useSelector } from 'react-redux'; @@ -52,14 +50,13 @@ function AddSale() { return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx similarity index 86% rename from packages/webapp/src/containers/Finances/CropSaleInputs.tsx rename to packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx index f5c8a40292..aee6da714e 100644 --- a/packages/webapp/src/containers/Finances/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx @@ -19,13 +19,13 @@ import { useTranslation } from 'react-i18next'; import { CROP_VARIETY_SALE, CROP_VARIETY_ID, -} from '../../components/Forms/GeneralRevenue/constants'; -import CropSaleItem from '../../components/Forms/GeneralRevenue/CropSaleItem'; -import { selectManagementPlansForSale } from '../managementPlanSlice'; -import EntitySaleInputs from './EntitySaleInputs'; -import type { CropVarietySaleTileData } from '../../components/CropTile/CropVarietySaleTile'; -import { getUnitOptionMap } from '../../util/convert-units/getUnitOptionMap'; -import type { SelectOption } from '../../components/Form/ReactSelect/CheckboxMultiSelect/index'; +} from '../../../components/Forms/RevenueForm/constants'; +import CropSaleItem from '../../../components/Forms/RevenueForm/CropSaleItem'; +import { selectManagementPlansForSale } from '../../managementPlanSlice'; +import EntitySaleEntries from '../../../components/Forms/RevenueForm/EntitySaleEntries'; +import type { CropVarietySaleTileData } from '../../../components/CropTile/CropVarietySaleTile'; +import { getUnitOptionMap } from '../../../util/convert-units/getUnitOptionMap'; +import type { SelectOption } from '../../../components/Form/ReactSelect/CheckboxMultiSelect/index'; export const getCropSaleDefaultValues = (sale: CropSale | undefined) => { const existingSales = sale?.crop_variety_sale?.reduce< @@ -104,7 +104,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr ); return ( - )} - + ); } diff --git a/packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx similarity index 84% rename from packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx rename to packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx index 46709b75f3..e705247bcd 100644 --- a/packages/webapp/src/containers/Finances/SaleInputsByEntityType.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx @@ -26,14 +26,14 @@ interface RevenueTypeOption { label: string; } -interface SaleInputsByEntityTypeProps { +interface EntitySaleInputsProps { sale?: any; disabledInput: boolean; revenueTypes?: RevenueType[]; selectedTypeOption?: RevenueTypeOption; } -export const getRevenueSaleDefaultValues = ( +export const getEntitySaleDefaultValues = ( sale: any, entityType: RevenueType['entity_type'] | undefined, ) => { @@ -46,21 +46,18 @@ export const getRevenueSaleDefaultValues = ( return undefined; }; -export default function SaleInputsByEntityType({ +export default function EntitySaleInputs({ sale, disabledInput, revenueTypes, selectedTypeOption, -}: SaleInputsByEntityTypeProps) { +}: EntitySaleInputsProps) { const entityType = revenueTypes?.find( (rt) => rt.revenue_type_id === selectedTypeOption?.value, )?.entity_type; - if (entityType === 'crop') { - return ; - } if (entityType === 'animal') { return ; } - return null; + return ; } diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 324daa19cd..59e2039597 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -22,12 +22,11 @@ import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { setPersistedPaths } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; -import GeneralRevenue from '../../../components/Forms/GeneralRevenue'; -import SaleInputsByEntityType, { getRevenueSaleDefaultValues } from '../SaleInputsByEntityType'; +import RevenueForm from '../../../components/Forms/RevenueForm'; import useHookFormPersist from '../../hooks/useHookFormPersist'; import { mapRevenueFormDataToApiCallFormat, mapRevenueTypesToReactSelectOptions } from '../util'; import useSortedRevenueTypes from '../AddSale/RevenueTypes/useSortedRevenueTypes'; -import { REVENUE_TYPE_OPTION } from '../../../components/Forms/GeneralRevenue/constants'; +import { REVENUE_TYPE_OPTION } from '../../../components/Forms/RevenueForm/constants'; import { createEditRevenueDetailsUrl } from '../../../util/siteMapConstants'; function RevenueDetail() { @@ -78,14 +77,12 @@ function RevenueDetail() { }; return ( - { if (isCropSale(revenueType)) { return transactionTypeEnum.cropRevenue; } - if (revenueType?.entity_type === 'animal') { + if (isAnimalSale(revenueType)) { return transactionTypeEnum.animalRevenue; } return transactionTypeEnum.revenue; diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index ec01149944..584565a044 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -29,7 +29,7 @@ import { SALE_DATE, SALE_VALUE, VALUE, -} from '../../components/Forms/GeneralRevenue/constants'; +} from '../../components/Forms/RevenueForm/constants'; import { chooseIdentification } from '../Animals/utils'; import i18n from '../../locales/i18n'; import { getMass, getMassUnit, roundToTwoDecimal } from '../../util'; @@ -345,3 +345,5 @@ export const getFinanceTypeSearchableStringFunc = (typeCategory) => (type) => { }; export const isCropSale = (revenueType) => revenueType?.entity_type === 'crop'; +export const isAnimalSale = (revenueType) => revenueType?.entity_type === 'animal'; +export const isEntitySale = (revenueType) => isCropSale(revenueType) || isAnimalSale(revenueType); diff --git a/packages/webapp/src/stories/Finances/CropSaleItem.stories.jsx b/packages/webapp/src/stories/Finances/CropSaleItem.stories.jsx index 9120bc21ac..0878fc24e1 100644 --- a/packages/webapp/src/stories/Finances/CropSaleItem.stories.jsx +++ b/packages/webapp/src/stories/Finances/CropSaleItem.stories.jsx @@ -12,7 +12,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ -import CropSaleItem from '../../components/Forms/GeneralRevenue/CropSaleItem'; +import CropSaleItem from '../../components/Forms/RevenueForm/CropSaleItem'; import { componentDecorators } from '../Pages/config/Decorators'; import { FormProvider, useForm } from 'react-hook-form'; diff --git a/packages/webapp/src/stories/Finances/GeneralIncome.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx similarity index 83% rename from packages/webapp/src/stories/Finances/GeneralIncome.stories.jsx rename to packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index 9d954cbe10..ee40da094a 100644 --- a/packages/webapp/src/stories/Finances/GeneralIncome.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -12,11 +12,9 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ -import GeneralRevenue from '../../components/Forms/GeneralRevenue'; +import { useState } from 'react'; +import RevenueForm from '../../components/Forms/RevenueForm'; import { componentDecorators } from '../Pages/config/Decorators'; -import React, { useState } from 'react'; -import CropSaleInputs, { getCropSaleDefaultValues } from '../../containers/Finances/CropSaleInputs'; -import { isCropSale } from '../../containers/Finances/util'; const cropSale = { sale_id: 17, @@ -82,7 +80,7 @@ const revenueTypeOptions = [ }, ]; -const GeneralRevenueWithState = (props) => { +const RevenueFormWithState = (props) => { const { view, sale, revenueType } = props; const [isEditing, setIsEditing] = useState(false); @@ -93,22 +91,16 @@ const GeneralRevenueWithState = (props) => { setValue(REVENUE_TYPE_OPTION, newType); }; if (view === 'add') { - // TODO LF-5274 update passed component - return ; + return ; } else { return ( - setIsEditing(false) : () => {}} onClick={isEditing ? undefined : () => setIsEditing(true)} buttonText={isEditing ? 'Save' : 'Edit'} - // TODO LF-5274 update passed component - CustomFormChildren={CropSaleInputs} - customFormChildrenDefaultValues={ - isCropSale(selectedRevenueType) ? getCropSaleDefaultValues(sale) : undefined - } onTypeChange={onTypeChange} revenueType={selectedRevenueType} {...props} @@ -118,12 +110,12 @@ const GeneralRevenueWithState = (props) => { }; export default { - title: 'Components/GeneralRevenue', - component: GeneralRevenueWithState, + title: 'Components/RevenueForm', + component: RevenueFormWithState, decorators: componentDecorators, }; -const Template = (args) => ; +const Template = (args) => ; export const AddCropSale = Template.bind({}); From a88f221102f4788664c8f5f0347fbff31c5dd5be Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 12 May 2026 16:06:03 -0700 Subject: [PATCH 15/36] LF-5274 Clean up and update Story --- .../stories/Finances/RevenueForm.stories.jsx | 114 ++++++++++++------ 1 file changed, 77 insertions(+), 37 deletions(-) diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index ee40da094a..fe11eaa49e 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiteFarm.org + * Copyright 2023-26 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify @@ -51,6 +51,31 @@ const generalSale = { note: 'hya', }; +const animalSale = { + sale_id: 23, + customer_name: 'Animal customer', + sale_date: '2023-10-15T04:00:00.000Z', + farm_id: null, + revenue_type_id: 3, + note: 'animal note', + animal_sale: [ + { + animal_id: 101, + animal_batch_id: null, + quantity: 1, + sale_value: 250, + quantity_unit: 'unit', + }, + { + animal_id: null, + animal_batch_id: 7, + quantity: 10, + sale_value: 500, + quantity_unit: 'unit', + }, + ], +}; + const revenueTypes = [ { revenue_type_id: 1, @@ -68,6 +93,14 @@ const revenueTypes = [ deleted: false, entity_type: null, }, + { + revenue_type_id: 3, + revenue_name: 'Animal Sale', + revenue_translation_key: 'ANIMAL_SALE', + farm_id: null, + deleted: false, + entity_type: 'animal', + }, ]; const revenueTypeOptions = [ { @@ -78,35 +111,30 @@ const revenueTypeOptions = [ value: 2, label: 'General Sale', }, + { + value: 3, + label: 'Animal Sale', + }, ]; const RevenueFormWithState = (props) => { - const { view, sale, revenueType } = props; - + const { view } = props; const [isEditing, setIsEditing] = useState(false); - const [selectedRevenueType, setSelectedRevenueType] = useState(revenueType); - const onTypeChange = (typeId, setValue, REVENUE_TYPE_OPTION) => { - const newType = revenueTypes.find((option) => option.value === typeId); - setValue(REVENUE_TYPE_OPTION, newType); - }; if (view === 'add') { return ; - } else { - return ( - setIsEditing(false) : () => {}} - onClick={isEditing ? undefined : () => setIsEditing(true)} - buttonText={isEditing ? 'Save' : 'Edit'} - onTypeChange={onTypeChange} - revenueType={selectedRevenueType} - {...props} - /> - ); } + return ( + setIsEditing(false) : () => {}} + onClick={isEditing ? undefined : () => setIsEditing(true)} + buttonText={isEditing ? 'Save' : 'Edit'} + {...props} + /> + ); }; export default { @@ -122,30 +150,38 @@ export const AddCropSale = Template.bind({}); AddCropSale.args = { onSubmit: console.log, title: 'Add crop sale', - dateLabel: 'Date', - //useHookFormPersist: () => ({}), currency: '$', view: 'add', handleGoBack: () => {}, buttonText: 'Save', - revenueType: revenueTypes[0], revenueTypes, persistedFormData: { revenue_type_id: 1 }, revenueTypeOptions, }; +export const AddAnimalSale = Template.bind({}); + +AddAnimalSale.args = { + onSubmit: console.log, + title: 'Add animal sale', + currency: '$', + view: 'add', + handleGoBack: () => {}, + buttonText: 'Save', + revenueTypes, + persistedFormData: { revenue_type_id: 3 }, + revenueTypeOptions, +}; + export const AddGeneralSale = Template.bind({}); AddGeneralSale.args = { onSubmit: console.log, title: 'Add general sale', - dateLabel: 'Date', - //useHookFormPersist: () => ({}), currency: '$', view: 'add', handleGoBack: () => {}, buttonText: 'Save', - revenueType: revenueTypes[1], revenueTypes, persistedFormData: { revenue_type_id: 2 }, revenueTypeOptions, @@ -155,25 +191,29 @@ export const DetailGeneralSale = Template.bind({}); DetailGeneralSale.args = { title: 'General sale detail', - dateLabel: 'Date', - //useHookFormPersist: () => ({}), currency: '$', sale: generalSale, revenueTypeOptions, onRetire: () => {}, - revenueType: revenueTypes[1], revenueTypes, }; -export const DetailCropSale = Template.bind({}); -DetailCropSale.args = { - title: 'General sale detail', - dateLabel: 'Date', - //useHookFormPersist: () => ({}), +export const CropSaleDetail = Template.bind({}); +CropSaleDetail.args = { + title: 'Crop sale detail', currency: '$', sale: cropSale, revenueTypeOptions, onRetire: () => {}, - revenueType: revenueTypes[0], + revenueTypes, +}; + +export const AnimalSaleDetail = Template.bind({}); +AnimalSaleDetail.args = { + title: 'Animal sale detail', + currency: '$', + sale: animalSale, + revenueTypeOptions, + onRetire: () => {}, revenueTypes, }; From ca95e6b098b567fa533fb084ff25d3ff73b0fcf9 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 12 May 2026 22:50:23 -0700 Subject: [PATCH 16/36] LF-5274 Put child components directly into Revenue Form --- .../components/Forms/RevenueForm/index.jsx | 34 +++++----- .../Finances/EntitySaleInputs/index.tsx | 63 ------------------- .../webapp/src/containers/Finances/util.js | 2 +- 3 files changed, 21 insertions(+), 78 deletions(-) delete mode 100644 packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index 10830450fb..93f7aecb22 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiteFarm.org + * Copyright 2023-26 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify @@ -35,10 +35,13 @@ import { REVENUE_TYPE_ID, } from './constants'; import PropTypes from 'prop-types'; -import { isEntitySale } from '../../../containers/Finances/util'; -import EntitySaleInputs, { - getEntitySaleDefaultValues, -} from '../../../containers/Finances/EntitySaleInputs'; +import { isAnimalSale, isCropSale, isGeneralSale } from '../../../containers/Finances/util'; +import AnimalSaleInputs, { + getAnimalSaleDefaultValues, +} from '../../../containers/Finances/EntitySaleInputs/AnimalSaleInputs'; +import CropSaleInputs, { + getCropSaleDefaultValues, +} from '../../../containers/Finances/EntitySaleInputs/CropSaleInputs'; const RevenueForm = ({ onSubmit, @@ -63,7 +66,11 @@ const RevenueForm = ({ const initialRevenueType = revenueTypes?.find( (rt) => rt.revenue_type_id === data[REVENUE_TYPE_ID], ); - const entitySaleDefaults = getEntitySaleDefaultValues(sale, initialRevenueType?.entity_type); + const entitySaleDefaults = isCropSale(initialRevenueType) + ? getCropSaleDefaultValues(sale) + : isAnimalSale(initialRevenueType) + ? getAnimalSaleDefaultValues(sale) + : undefined; const reactHookFormFunctions = useForm({ mode: 'onChange', @@ -207,14 +214,13 @@ const RevenueForm = ({ )} /> )} - {isEntitySale(selectedRevenueType) ? ( - - ) : ( + {isCropSale(selectedRevenueType) && ( + + )} + {isAnimalSale(selectedRevenueType) && ( + + )} + {isGeneralSale(selectedRevenueType) && ( . - */ - -import CropSaleInputs, { getCropSaleDefaultValues } from './CropSaleInputs'; -import AnimalSaleInputs, { getAnimalSaleDefaultValues } from './AnimalSaleInputs'; - -interface RevenueType { - revenue_type_id: number; - entity_type?: 'crop' | 'animal' | 'none' | null; -} - -interface RevenueTypeOption { - value: number; - label: string; -} - -interface EntitySaleInputsProps { - sale?: any; - disabledInput: boolean; - revenueTypes?: RevenueType[]; - selectedTypeOption?: RevenueTypeOption; -} - -export const getEntitySaleDefaultValues = ( - sale: any, - entityType: RevenueType['entity_type'] | undefined, -) => { - if (entityType === 'crop') { - return getCropSaleDefaultValues(sale); - } - if (entityType === 'animal') { - return getAnimalSaleDefaultValues(sale); - } - return undefined; -}; - -export default function EntitySaleInputs({ - sale, - disabledInput, - revenueTypes, - selectedTypeOption, -}: EntitySaleInputsProps) { - const entityType = revenueTypes?.find( - (rt) => rt.revenue_type_id === selectedTypeOption?.value, - )?.entity_type; - - if (entityType === 'animal') { - return ; - } - return ; -} diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index 584565a044..1063007c25 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -346,4 +346,4 @@ export const getFinanceTypeSearchableStringFunc = (typeCategory) => (type) => { export const isCropSale = (revenueType) => revenueType?.entity_type === 'crop'; export const isAnimalSale = (revenueType) => revenueType?.entity_type === 'animal'; -export const isEntitySale = (revenueType) => isCropSale(revenueType) || isAnimalSale(revenueType); +export const isGeneralSale = (revenueType) => !revenueType?.entity_type; From 843ed58eab9a44bcd11e7078c41035145169c5a9 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Tue, 12 May 2026 23:00:27 -0700 Subject: [PATCH 17/36] LF-5274 Expand CropSaleTable into general EntitySaleTable --- packages/webapp/public/locales/en/translation.json | 1 + .../{CropSaleTable.jsx => EntitySaleTable.jsx} | 8 ++++---- .../Finances/Transaction/ExpandedContent/index.jsx | 8 ++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) rename packages/webapp/src/components/Finances/Transaction/ExpandedContent/{CropSaleTable.jsx => EntitySaleTable.jsx} (90%) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index f422f0d996..233972960d 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1097,6 +1097,7 @@ "REVENUE_TYPES": "Search revenue type" }, "TRANSACTION": { + "ANIMALS": "Animals", "CROPS": "Crops", "DAILY_TOTAL": "DAILY TOTAL", "LABOUR_EXPENSE": "Labour expense", diff --git a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/CropSaleTable.jsx b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx similarity index 90% rename from packages/webapp/src/components/Finances/Transaction/ExpandedContent/CropSaleTable.jsx rename to packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx index 479a8b6711..31e50c974e 100644 --- a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/CropSaleTable.jsx +++ b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx @@ -20,10 +20,10 @@ import history from '../../../../history'; import styles from './styles.module.scss'; import { createRevenueDetailsUrl } from '../../../../util/siteMapConstants'; -const getColumns = (t, mobileView, totalAmount, quantityTotal, currencySymbol) => [ +const getColumns = (t, titleLabel, mobileView, totalAmount, quantityTotal, currencySymbol) => [ { id: 'title', - label: t('FINANCES.TRANSACTION.CROPS'), + label: t(titleLabel), format: (d) => mobileView ? (
@@ -70,7 +70,7 @@ const FooterCell = ({ t, quantityTotal, totalAmount }) => (
); -export default function CropSaleTable({ data, currencySymbol, mobileView }) { +export default function EntitySaleTable({ data, currencySymbol, mobileView, titleLabel }) { const { t } = useTranslation(); const { items, amount, relatedId } = data; const quantityUnit = items?.[0]?.quantityUnit; @@ -85,7 +85,7 @@ export default function CropSaleTable({ data, currencySymbol, mobileView }) { return ( , REVENUE: (props) => , LABOUR_EXPENSE: (props) => , - CROP_REVENUE: (props) => , + CROP_REVENUE: (props) => , + ANIMAL_REVENUE: (props) => ( + + ), }; const getDetailPageLink = ({ transactionType, relatedId }) => { @@ -43,6 +46,7 @@ const getDetailPageLink = ({ transactionType, relatedId }) => { EXPENSE: createExpenseDetailsUrl(relatedId), REVENUE: createRevenueDetailsUrl(relatedId), CROP_REVENUE: createRevenueDetailsUrl(relatedId), + ANIMAL_REVENUE: createRevenueDetailsUrl(relatedId), }[transactionType]; }; From b9edf3ee4534808606a7b92287a05f979d1bef33 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 09:47:03 -0700 Subject: [PATCH 18/36] LF-5274 Remove max character part from notes placeholder and match maxChars limit to FarmNotes and MarketDirectoryInfo TextAreas --- .../src/components/Forms/RevenueForm/index.jsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index 93f7aecb22..052fbb45b0 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -102,16 +102,11 @@ const RevenueForm = ({ (rt) => rt.revenue_type_id === selectedTypeOption?.value, ); - const notesPlaceholder = (() => { - const maxCharsSuffix = ` - ${t('common:MAX_CHARS', { value: 125 })}`; - if (selectedRevenueType?.entity_type === 'crop') { - return t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') + maxCharsSuffix; - } - if (selectedRevenueType?.entity_type === 'animal') { - return t('SALE.ADD_SALE.ANIMAL_NOTES_PLACEHOLDER') + maxCharsSuffix; - } - return t('SALE.ADD_SALE.NOTES_PLACEHOLDER'); - })(); + const notesPlaceholder = isCropSale(selectedRevenueType) + ? t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') + : isAnimalSale(selectedRevenueType) + ? t('SALE.ADD_SALE.ANIMAL_NOTES_PLACEHOLDER') + : t('SALE.ADD_SALE.NOTES_PLACEHOLDER'); useEffect(() => { if (revenueTypeOptions?.length && !selectedTypeOption) { @@ -186,7 +181,7 @@ const RevenueForm = ({ style={{ marginBottom: '40px' }} label={t('LOG_COMMON.NOTES')} optional={true} - hookFormRegister={register(NOTE, { maxLength: hookFormMaxCharsValidation(125) })} + hookFormRegister={register(NOTE, { maxLength: hookFormMaxCharsValidation(3000) })} name={NOTE} placeholder={notesPlaceholder} minRows={5} From 180af0618e452659ee78ecdcfba6d602b4174e16 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 10:39:28 -0700 Subject: [PATCH 19/36] LF-5274 Restore customFormChildren and let containers own ternaries --- .../components/Forms/RevenueForm/index.jsx | 33 +++++-------------- .../src/containers/Finances/AddSale/index.jsx | 16 ++++++++- .../Finances/RevenueDetail/index.jsx | 23 ++++++++++++- .../stories/Finances/RevenueForm.stories.jsx | 16 +++++++-- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index 052fbb45b0..e8fa43a81e 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -35,13 +35,7 @@ import { REVENUE_TYPE_ID, } from './constants'; import PropTypes from 'prop-types'; -import { isAnimalSale, isCropSale, isGeneralSale } from '../../../containers/Finances/util'; -import AnimalSaleInputs, { - getAnimalSaleDefaultValues, -} from '../../../containers/Finances/EntitySaleInputs/AnimalSaleInputs'; -import CropSaleInputs, { - getCropSaleDefaultValues, -} from '../../../containers/Finances/EntitySaleInputs/CropSaleInputs'; +import { isAnimalSale, isCropSale } from '../../../containers/Finances/util'; const RevenueForm = ({ onSubmit, @@ -57,21 +51,14 @@ const RevenueForm = ({ buttonText, revenueTypes, onRetire, + CustomFormChildren, + customFormChildrenDefaultValues, }) => { const { t } = useTranslation(); const [isDeleting, setIsDeleting] = useState(false); const data = sale || persistedFormData; - const initialRevenueType = revenueTypes?.find( - (rt) => rt.revenue_type_id === data[REVENUE_TYPE_ID], - ); - const entitySaleDefaults = isCropSale(initialRevenueType) - ? getCropSaleDefaultValues(sale) - : isAnimalSale(initialRevenueType) - ? getAnimalSaleDefaultValues(sale) - : undefined; - const reactHookFormFunctions = useForm({ mode: 'onChange', defaultValues: { @@ -84,7 +71,7 @@ const RevenueForm = ({ }), [VALUE]: !isNaN(data[VALUE]) ? data[VALUE] : null, [NOTE]: data[NOTE] ?? null, - ...entitySaleDefaults, + ...customFormChildrenDefaultValues, }, }); @@ -209,13 +196,9 @@ const RevenueForm = ({ )} /> )} - {isCropSale(selectedRevenueType) && ( - - )} - {isAnimalSale(selectedRevenueType) && ( - - )} - {isGeneralSale(selectedRevenueType) && ( + {CustomFormChildren && selectedRevenueType?.entity_type ? ( + + ) : ( ); diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 59e2039597..145a9a986e 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -24,8 +24,15 @@ import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { setPersistedPaths } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import RevenueForm from '../../../components/Forms/RevenueForm'; import useHookFormPersist from '../../hooks/useHookFormPersist'; -import { mapRevenueFormDataToApiCallFormat, mapRevenueTypesToReactSelectOptions } from '../util'; +import { + mapRevenueFormDataToApiCallFormat, + mapRevenueTypesToReactSelectOptions, + isCropSale, + isAnimalSale, +} from '../util'; import useSortedRevenueTypes from '../AddSale/RevenueTypes/useSortedRevenueTypes'; +import CropSaleInputs, { getCropSaleDefaultValues } from '../EntitySaleInputs/CropSaleInputs'; +import AnimalSaleInputs, { getAnimalSaleDefaultValues } from '../EntitySaleInputs/AnimalSaleInputs'; import { REVENUE_TYPE_OPTION } from '../../../components/Forms/RevenueForm/constants'; import { createEditRevenueDetailsUrl } from '../../../util/siteMapConstants'; @@ -76,6 +83,18 @@ function RevenueDetail() { setValue(REVENUE_TYPE_OPTION, newType); }; + const CustomFormChildren = isCropSale(revenueType) + ? CropSaleInputs + : isAnimalSale(revenueType) + ? AnimalSaleInputs + : null; + + const customFormChildrenDefaultValues = isCropSale(revenueType) + ? getCropSaleDefaultValues(sale) + : isAnimalSale(revenueType) + ? getAnimalSaleDefaultValues(sale) + : undefined; + return ( ); } diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index fe11eaa49e..6dadcbbe6d 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -14,6 +14,12 @@ */ import { useState } from 'react'; import RevenueForm from '../../components/Forms/RevenueForm'; +import CropSaleInputs, { + getCropSaleDefaultValues, +} from '../../containers/Finances/EntitySaleInputs/CropSaleInputs'; +import AnimalSaleInputs, { + getAnimalSaleDefaultValues, +} from '../../containers/Finances/EntitySaleInputs/AnimalSaleInputs'; import { componentDecorators } from '../Pages/config/Decorators'; const cropSale = { @@ -157,6 +163,7 @@ AddCropSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 1 }, revenueTypeOptions, + CustomFormChildren: CropSaleInputs, }; export const AddAnimalSale = Template.bind({}); @@ -171,6 +178,7 @@ AddAnimalSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 3 }, revenueTypeOptions, + CustomFormChildren: AnimalSaleInputs, }; export const AddGeneralSale = Template.bind({}); @@ -187,9 +195,9 @@ AddGeneralSale.args = { revenueTypeOptions, }; -export const DetailGeneralSale = Template.bind({}); +export const GeneralSaleDetail = Template.bind({}); -DetailGeneralSale.args = { +GeneralSaleDetail.args = { title: 'General sale detail', currency: '$', sale: generalSale, @@ -206,6 +214,8 @@ CropSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, + CustomFormChildren: CropSaleInputs, + customFormChildrenDefaultValues: getCropSaleDefaultValues(cropSale), }; export const AnimalSaleDetail = Template.bind({}); @@ -216,4 +226,6 @@ AnimalSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, + CustomFormChildren: AnimalSaleInputs, + customFormChildrenDefaultValues: getAnimalSaleDefaultValues(animalSale), }; From f6d423f0c18556c6150cb1005000ae7309490be7 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 12:43:04 -0700 Subject: [PATCH 20/36] LF-5274 Map-based solution to type switching Most likely I will revert this and return to a wrapping component --- .../src/components/Forms/RevenueForm/index.jsx | 10 ++++++---- .../src/containers/Finances/AddSale/index.jsx | 17 ++++------------- .../containers/Finances/RevenueDetail/index.jsx | 10 +++------- .../stories/Finances/RevenueForm.stories.jsx | 10 ++++++---- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index e8fa43a81e..35fc7fa7a8 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -51,7 +51,7 @@ const RevenueForm = ({ buttonText, revenueTypes, onRetire, - CustomFormChildren, + entityTypeComponents, customFormChildrenDefaultValues, }) => { const { t } = useTranslation(); @@ -89,6 +89,8 @@ const RevenueForm = ({ (rt) => rt.revenue_type_id === selectedTypeOption?.value, ); + const DynamicChildren = entityTypeComponents?.[selectedRevenueType?.entity_type]; + const notesPlaceholder = isCropSale(selectedRevenueType) ? t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') : isAnimalSale(selectedRevenueType) @@ -196,8 +198,8 @@ const RevenueForm = ({ )} /> )} - {CustomFormChildren && selectedRevenueType?.entity_type ? ( - + {DynamicChildren ? ( + ) : ( ); diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 145a9a986e..677fce8a67 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -36,6 +36,8 @@ import AnimalSaleInputs, { getAnimalSaleDefaultValues } from '../EntitySaleInput import { REVENUE_TYPE_OPTION } from '../../../components/Forms/RevenueForm/constants'; import { createEditRevenueDetailsUrl } from '../../../util/siteMapConstants'; +const entityTypeComponents = { crop: CropSaleInputs, animal: AnimalSaleInputs }; + function RevenueDetail() { const history = useHistory(); const match = useRouteMatch(); @@ -83,12 +85,6 @@ function RevenueDetail() { setValue(REVENUE_TYPE_OPTION, newType); }; - const CustomFormChildren = isCropSale(revenueType) - ? CropSaleInputs - : isAnimalSale(revenueType) - ? AnimalSaleInputs - : null; - const customFormChildrenDefaultValues = isCropSale(revenueType) ? getCropSaleDefaultValues(sale) : isAnimalSale(revenueType) @@ -110,7 +106,7 @@ function RevenueDetail() { buttonText={isEditing ? t('common:SAVE') : t('common:EDIT')} onRetire={onRetire} revenueTypes={revenueTypesArray} - CustomFormChildren={CustomFormChildren} + entityTypeComponents={entityTypeComponents} customFormChildrenDefaultValues={customFormChildrenDefaultValues} /> ); diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index 6dadcbbe6d..c516b94938 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -22,6 +22,8 @@ import AnimalSaleInputs, { } from '../../containers/Finances/EntitySaleInputs/AnimalSaleInputs'; import { componentDecorators } from '../Pages/config/Decorators'; +const entityTypeComponents = { crop: CropSaleInputs, animal: AnimalSaleInputs }; + const cropSale = { sale_id: 17, customer_name: 'Name', @@ -163,7 +165,7 @@ AddCropSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 1 }, revenueTypeOptions, - CustomFormChildren: CropSaleInputs, + entityTypeComponents, }; export const AddAnimalSale = Template.bind({}); @@ -178,7 +180,7 @@ AddAnimalSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 3 }, revenueTypeOptions, - CustomFormChildren: AnimalSaleInputs, + entityTypeComponents, }; export const AddGeneralSale = Template.bind({}); @@ -214,7 +216,7 @@ CropSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - CustomFormChildren: CropSaleInputs, + entityTypeComponents, customFormChildrenDefaultValues: getCropSaleDefaultValues(cropSale), }; @@ -226,6 +228,6 @@ AnimalSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - CustomFormChildren: AnimalSaleInputs, + entityTypeComponents, customFormChildrenDefaultValues: getAnimalSaleDefaultValues(animalSale), }; From a2f8c1a290ed69061e8d4a4107c4db49845e449a Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 13:46:51 -0700 Subject: [PATCH 21/36] LF-5274 Fix select placeholders --- packages/webapp/public/locales/en/translation.json | 4 ++-- .../containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 2 +- .../containers/Finances/EntitySaleInputs/CropSaleInputs.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 233972960d..7af22085e5 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1881,12 +1881,12 @@ "ADD_SALE": { "ADD_CUSTOM_REVENUE_TYPE": "Add custom revenue type", "ADD_REVENUE": "Add revenue", - "ANIMAL": "Animal", + "SELECT_ANIMALS": "Select animals", "ANIMAL_NOTES_PLACEHOLDER": "Animal sale description", "ANIMAL_REQUIRED": "Required", "CROP_NOTES_PLACEHOLDER": "Crop sale description", "CROP_REQUIRED": "Required", - "CROP_VARIETY": "Crop variety", + "SELECT_CROPS": "Select crops", "FLOW": "revenue creation", "MANAGE_CUSTOM_REVENUE_TYPE": "Manage custom revenue types", "NOTES_PLACEHOLDER": "Sale description", diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index c5eacc71a8..de3ae03468 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -102,7 +102,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu savedSalesById={savedSalesById} fieldPrefix={ANIMAL_SALE} entityIdFieldKey={ANIMAL_KEY} - placeholder={t('SALE.ADD_SALE.ANIMAL')} + placeholder={t('SALE.ADD_SALE.SELECT_ANIMALS')} > {({ option, system, currency, disabledInput }) => ( {({ option, system, currency, disabledInput }) => ( Date: Wed, 13 May 2026 14:32:20 -0700 Subject: [PATCH 22/36] LF-5274 Refactor animal and batch formatting functions to use existing helpers --- .../EntitySaleInputs/AnimalSaleInputs.tsx | 38 +++++++++++-------- .../EntitySaleInputs/CropSaleInputs.tsx | 2 +- .../webapp/src/containers/Finances/util.js | 8 ++-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index de3ae03468..5fea677f72 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -21,7 +21,9 @@ import EntitySaleEntries from '../../../components/Forms/RevenueForm/EntitySaleE import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../../store/api/apiSlice'; import { chooseIdentification } from '../../Animals/utils'; import { getUnitOptionMap } from '../../../util/convert-units/getUnitOptionMap'; +import { generateInventoryId } from '../../../util/animal'; import type { Animal, AnimalBatch } from '../../../store/api/types'; +import { AnimalOrBatchKeys } from '../../Animals/types'; import type { SelectOption } from '../../../components/Form/ReactSelect/CheckboxMultiSelect'; interface AnimalSaleRecord { @@ -32,7 +34,7 @@ interface AnimalSaleRecord { sale_value: number; } -interface AnimalSale { +export interface AnimalSale { animal_sale?: AnimalSaleRecord[]; } @@ -41,13 +43,13 @@ interface AnimalSaleInputsProps { disabledInput: boolean; } -const animalOptionKey = (id: number) => `animal_${id}`; -const batchOptionKey = (id: number) => `batch_${id}`; +const saleRecordToOptionKey = (record: AnimalSaleRecord) => { + const isAnimal = record.animal_id !== null; + const key = isAnimal ? AnimalOrBatchKeys.ANIMAL : AnimalOrBatchKeys.BATCH; + const id = isAnimal ? record.animal_id : record.animal_batch_id; -const saleRecordToOptionKey = (record: AnimalSaleRecord) => - record.animal_id != null - ? animalOptionKey(record.animal_id) - : batchOptionKey(record.animal_batch_id as number); + return `${key}_${id}`; +}; export const getAnimalSaleDefaultValues = (sale: AnimalSale | undefined) => { const existingSales = sale?.animal_sale?.reduce< @@ -79,15 +81,19 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu const { data: animalBatches } = useGetAnimalBatchesQuery(); const options = useMemo(() => { - const list: SelectOption[] = []; - (animals ?? []).forEach((a: Animal) => { - list.push({ label: chooseIdentification(a), value: animalOptionKey(a.id) }); - }); - (animalBatches ?? []).forEach((b: AnimalBatch) => { - list.push({ label: chooseIdentification(b), value: batchOptionKey(b.id) }); - }); - list.sort((a, b) => String(a.label).localeCompare(String(b.label))); - return list; + const animalOptions = (animals ?? []).map((a: Animal) => ({ + label: chooseIdentification(a), + value: generateInventoryId(AnimalOrBatchKeys.ANIMAL, a), + })); + + const batchOptions = (animalBatches ?? []).map((b: AnimalBatch) => ({ + label: chooseIdentification(b), + value: generateInventoryId(AnimalOrBatchKeys.BATCH, b), + })); + + return [...animalOptions, ...batchOptions].sort((a, b) => + String(a.label).localeCompare(String(b.label)), + ); }, [animals, animalBatches]); const savedSalesById = sale?.animal_sale?.reduce>( diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx index f81ff1f32e..f5f4fd9bc4 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx @@ -59,7 +59,7 @@ interface CropVarietySaleRecord { sale_value: number; } -interface CropSale { +export interface CropSale { crop_variety_sale?: CropVarietySaleRecord[]; } diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index 1063007c25..cc33d71ca3 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -37,6 +37,8 @@ import { isSameDay } from '../../util/date-migrate-TS'; import { getLanguageFromLocalStorage } from '../../util/getLanguageFromLocalStorage'; import { LABOUR_ITEMS_GROUPING_OPTIONS, REVENUE_FORM_TYPES } from './constants'; import { transactionTypeEnum } from './useTransactions'; +import { parseInventoryId } from '../../util/animal'; +import { AnimalOrBatchKeys } from '../Animals/types'; // Polyfill for tests and older browsers const groupBy = typeof Object.groupBy === 'function' ? Object.groupBy : lodashGroupBy; @@ -284,10 +286,10 @@ export function mapRevenueFormDataToApiCallFormat(data, revenueTypes, sale_id, f }); } else if (revenueType?.entity_type === 'animal') { sale.value = undefined; + sale.animal_sale = Object.values(data[ANIMAL_SALE]).map((a) => { - const key = a[ANIMAL_KEY]; - const isBatch = typeof key === 'string' && key.startsWith('batch_'); - const id = parseInt(String(key).split('_')[1], 10); + const { kind, id } = parseInventoryId(a[ANIMAL_KEY]); + const isBatch = kind === AnimalOrBatchKeys.BATCH; return { sale_value: a[SALE_VALUE], quantity: a[QUANTITY], From 241e57c168f1f99081817d44fc24544be10ed829 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 14:59:43 -0700 Subject: [PATCH 23/36] LF-5274 Refactor getAnimalSaleDefaultValues --- .../EntitySaleInputs/AnimalSaleInputs.tsx | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index 5fea677f72..4c7241cc74 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -26,14 +26,18 @@ import type { Animal, AnimalBatch } from '../../../store/api/types'; import { AnimalOrBatchKeys } from '../../Animals/types'; import type { SelectOption } from '../../../components/Form/ReactSelect/CheckboxMultiSelect'; -interface AnimalSaleRecord { +interface BaseAnimalSaleRecord { animal_id: number | null; animal_batch_id: number | null; quantity: number; - quantity_unit: string | undefined; + quantity_unit: TQuantityUnit; sale_value: number; } +// API data returns a string for quantity_unit, but form data uses SelectOption +type AnimalSaleRecord = BaseAnimalSaleRecord; +type AnimalSaleDefaultRecord = BaseAnimalSaleRecord; + export interface AnimalSale { animal_sale?: AnimalSaleRecord[]; } @@ -52,26 +56,28 @@ const saleRecordToOptionKey = (record: AnimalSaleRecord) => { }; export const getAnimalSaleDefaultValues = (sale: AnimalSale | undefined) => { - const existingSales = sale?.animal_sale?.reduce< - Record & { quantity_unit?: SelectOption }> - >((acc, cur) => { - const key = saleRecordToOptionKey(cur); - acc[key] = { - animal_id: cur.animal_id, - animal_batch_id: cur.animal_batch_id, - quantity: cur.quantity, - quantity_unit: cur.quantity_unit - ? ((getUnitOptionMap() as Record)[cur.quantity_unit] ?? { - label: cur.quantity_unit, - value: cur.quantity_unit, - }) - : undefined, - sale_value: cur.sale_value, - }; - return acc; - }, {}); + if (!sale?.animal_sale) { + return { [ANIMAL_SALE]: undefined }; + } + + const unitMap = getUnitOptionMap() as Record; + + const existingSales = Object.fromEntries( + sale.animal_sale.map((record) => { + const key = saleRecordToOptionKey(record); + const unit = record.quantity_unit; + + const formattedEntry: AnimalSaleDefaultRecord = { + ...record, + quantity_unit: unit ? (unitMap[unit] ?? { label: unit, value: unit }) : undefined, + }; + + return [key, formattedEntry]; + }), + ); + return { - [ANIMAL_SALE]: existingSales ?? undefined, + [ANIMAL_SALE]: existingSales, }; }; From 5b6734035862f0f97fc208f12d92fceefa4b8c4c Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 15:01:59 -0700 Subject: [PATCH 24/36] LF-5274 Restore EntitySaleInputs wrapper component (sigh) --- .../components/Forms/RevenueForm/index.jsx | 27 +++++----- .../src/containers/Finances/AddSale/index.jsx | 5 -- .../Finances/EntitySaleInputs/index.tsx | 53 +++++++++++++++++++ .../Finances/RevenueDetail/index.jsx | 22 +++----- .../stories/Finances/RevenueForm.stories.jsx | 12 +---- 5 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index 35fc7fa7a8..4e9ba46628 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -35,7 +35,7 @@ import { REVENUE_TYPE_ID, } from './constants'; import PropTypes from 'prop-types'; -import { isAnimalSale, isCropSale } from '../../../containers/Finances/util'; +import EntitySaleInputs from '../../../containers/Finances/EntitySaleInputs'; const RevenueForm = ({ onSubmit, @@ -51,7 +51,6 @@ const RevenueForm = ({ buttonText, revenueTypes, onRetire, - entityTypeComponents, customFormChildrenDefaultValues, }) => { const { t } = useTranslation(); @@ -88,14 +87,15 @@ const RevenueForm = ({ const selectedRevenueType = revenueTypes?.find( (rt) => rt.revenue_type_id === selectedTypeOption?.value, ); + const selectedEntityType = selectedRevenueType?.entity_type; + const isEntitySale = selectedEntityType === 'crop' || selectedEntityType === 'animal'; - const DynamicChildren = entityTypeComponents?.[selectedRevenueType?.entity_type]; - - const notesPlaceholder = isCropSale(selectedRevenueType) - ? t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') - : isAnimalSale(selectedRevenueType) - ? t('SALE.ADD_SALE.ANIMAL_NOTES_PLACEHOLDER') - : t('SALE.ADD_SALE.NOTES_PLACEHOLDER'); + const notesPlaceholder = + selectedEntityType === 'crop' + ? t('SALE.ADD_SALE.CROP_NOTES_PLACEHOLDER') + : selectedEntityType === 'animal' + ? t('SALE.ADD_SALE.ANIMAL_NOTES_PLACEHOLDER') + : t('SALE.ADD_SALE.NOTES_PLACEHOLDER'); useEffect(() => { if (revenueTypeOptions?.length && !selectedTypeOption) { @@ -198,8 +198,12 @@ const RevenueForm = ({ )} /> )} - {DynamicChildren ? ( - + {isEntitySale ? ( + ) : ( ); diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx new file mode 100644 index 0000000000..b65ccb3c19 --- /dev/null +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/index.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2026 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +import CropSaleInputs, { CropSale, getCropSaleDefaultValues } from './CropSaleInputs'; +import AnimalSaleInputs, { AnimalSale, getAnimalSaleDefaultValues } from './AnimalSaleInputs'; +import type { EntityType } from '../types'; + +type EntitySale = CropSale | AnimalSale; + +interface EntitySaleInputsProps { + sale?: EntitySale; + disabledInput: boolean; + entityType?: EntityType; +} + +export const getEntityTypeDefaultValues = ( + sale: EntitySaleInputsProps['sale'], + entityType: EntityType, +) => { + if (entityType === 'crop') { + return getCropSaleDefaultValues(sale as CropSale); + } + if (entityType === 'animal') { + return getAnimalSaleDefaultValues(sale as AnimalSale); + } + return undefined; +}; + +export default function EntitySaleInputs({ + sale, + disabledInput, + entityType, +}: EntitySaleInputsProps) { + if (entityType === 'crop') { + return ; + } + if (entityType === 'animal') { + return ; + } + return null; +} diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 677fce8a67..0bee64b4fa 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -24,20 +24,12 @@ import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { setPersistedPaths } from '../../hooks/useHookFormPersist/hookFormPersistSlice'; import RevenueForm from '../../../components/Forms/RevenueForm'; import useHookFormPersist from '../../hooks/useHookFormPersist'; -import { - mapRevenueFormDataToApiCallFormat, - mapRevenueTypesToReactSelectOptions, - isCropSale, - isAnimalSale, -} from '../util'; +import { mapRevenueFormDataToApiCallFormat, mapRevenueTypesToReactSelectOptions } from '../util'; import useSortedRevenueTypes from '../AddSale/RevenueTypes/useSortedRevenueTypes'; -import CropSaleInputs, { getCropSaleDefaultValues } from '../EntitySaleInputs/CropSaleInputs'; -import AnimalSaleInputs, { getAnimalSaleDefaultValues } from '../EntitySaleInputs/AnimalSaleInputs'; +import { getEntityTypeDefaultValues } from '../EntitySaleInputs'; import { REVENUE_TYPE_OPTION } from '../../../components/Forms/RevenueForm/constants'; import { createEditRevenueDetailsUrl } from '../../../util/siteMapConstants'; -const entityTypeComponents = { crop: CropSaleInputs, animal: AnimalSaleInputs }; - function RevenueDetail() { const history = useHistory(); const match = useRouteMatch(); @@ -85,11 +77,10 @@ function RevenueDetail() { setValue(REVENUE_TYPE_OPTION, newType); }; - const customFormChildrenDefaultValues = isCropSale(revenueType) - ? getCropSaleDefaultValues(sale) - : isAnimalSale(revenueType) - ? getAnimalSaleDefaultValues(sale) - : undefined; + const customFormChildrenDefaultValues = getEntityTypeDefaultValues( + sale, + revenueType?.entity_type, + ); return ( ); diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index c516b94938..e9634695a0 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -14,16 +14,12 @@ */ import { useState } from 'react'; import RevenueForm from '../../components/Forms/RevenueForm'; -import CropSaleInputs, { +import { getCropSaleDefaultValues, -} from '../../containers/Finances/EntitySaleInputs/CropSaleInputs'; -import AnimalSaleInputs, { getAnimalSaleDefaultValues, -} from '../../containers/Finances/EntitySaleInputs/AnimalSaleInputs'; +} from '../../containers/Finances/EntitySaleInputs'; import { componentDecorators } from '../Pages/config/Decorators'; -const entityTypeComponents = { crop: CropSaleInputs, animal: AnimalSaleInputs }; - const cropSale = { sale_id: 17, customer_name: 'Name', @@ -165,7 +161,6 @@ AddCropSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 1 }, revenueTypeOptions, - entityTypeComponents, }; export const AddAnimalSale = Template.bind({}); @@ -180,7 +175,6 @@ AddAnimalSale.args = { revenueTypes, persistedFormData: { revenue_type_id: 3 }, revenueTypeOptions, - entityTypeComponents, }; export const AddGeneralSale = Template.bind({}); @@ -216,7 +210,6 @@ CropSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - entityTypeComponents, customFormChildrenDefaultValues: getCropSaleDefaultValues(cropSale), }; @@ -228,6 +221,5 @@ AnimalSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - entityTypeComponents, customFormChildrenDefaultValues: getAnimalSaleDefaultValues(animalSale), }; From b7d99d906b3ac973df4143ae42f9edfaa3454536 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 15:13:31 -0700 Subject: [PATCH 25/36] LF-5274 Pass animals and batches to ActualRevenue formatting function --- .../src/containers/Finances/ActualRevenue/index.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/containers/Finances/ActualRevenue/index.jsx b/packages/webapp/src/containers/Finances/ActualRevenue/index.jsx index 7327565745..78fb7f564d 100644 --- a/packages/webapp/src/containers/Finances/ActualRevenue/index.jsx +++ b/packages/webapp/src/containers/Finances/ActualRevenue/index.jsx @@ -27,6 +27,7 @@ import { FINANCES_HOME_URL, REVENUE_TYPES_URL, } from '../../../util/siteMapConstants'; +import { useGetAnimalBatchesQuery, useGetAnimalsQuery } from '../../../store/api/apiSlice'; export default function ActualRevenue() { const history = useHistory(); @@ -41,6 +42,8 @@ export default function ActualRevenue() { const sales = useSelector(salesSelector); const allRevenueTypes = useSelector(allRevenueTypesSelector); const cropVarieties = useSelector(cropVarietiesSelector); + const { data: animals } = useGetAnimalsQuery(); + const { data: animalBatches } = useGetAnimalBatchesQuery(); const { startDate: fromDate, endDate: toDate } = useFinancesDateRange({ weekStartDate: SUNDAY }); const filteredSales = useMemo( @@ -48,8 +51,9 @@ export default function ActualRevenue() { [sales, fromDate, toDate], ); const revenueItems = useMemo( - () => mapSalesToRevenueItems(filteredSales, allRevenueTypes, cropVarieties), - [filteredSales, allRevenueTypes, cropVarieties], + () => + mapSalesToRevenueItems(filteredSales, allRevenueTypes, cropVarieties, animals, animalBatches), + [filteredSales, allRevenueTypes, cropVarieties, animals, animalBatches], ); const revenueForWholeFarm = useMemo( () => calcActualRevenueFromRevenueItems(revenueItems), From 1c241aeb273b96cb13d7cec11795b9d2b220a6aa Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 15:49:49 -0700 Subject: [PATCH 26/36] LF-5274 Update footer of EntitySaleTable as discussed We looked at this in dev-design and realized 'DAILY TOTAL' should read 'TOTAL', and summing should only happen over values, not quantities --- .../ExpandedContent/EntitySaleTable.jsx | 21 ++++++------------- .../ExpandedContent/styles.module.scss | 4 ++++ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx index 31e50c974e..806a4261fc 100644 --- a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx +++ b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/EntitySaleTable.jsx @@ -20,7 +20,7 @@ import history from '../../../../history'; import styles from './styles.module.scss'; import { createRevenueDetailsUrl } from '../../../../util/siteMapConstants'; -const getColumns = (t, titleLabel, mobileView, totalAmount, quantityTotal, currencySymbol) => [ +const getColumns = (t, titleLabel, mobileView, totalAmount, currencySymbol) => [ { id: 'title', label: t(titleLabel), @@ -38,7 +38,7 @@ const getColumns = (t, titleLabel, mobileView, totalAmount, quantityTotal, curre columnProps: { style: { padding: `0 ${mobileView ? 8 : 12}px` }, }, - Footer: mobileView ? null : t('FINANCES.TRANSACTION.DAILY_TOTAL'), + Footer: mobileView ? null :
{t('common:TOTAL')}
, }, { id: mobileView ? null : 'quantity', @@ -48,7 +48,6 @@ const getColumns = (t, titleLabel, mobileView, totalAmount, quantityTotal, curre columnProps: { style: { width: '100px' }, }, - Footer: mobileView ? null :
{quantityTotal}
, }, { id: 'amount', @@ -62,10 +61,9 @@ const getColumns = (t, titleLabel, mobileView, totalAmount, quantityTotal, curre }, ]; -const FooterCell = ({ t, quantityTotal, totalAmount }) => ( +const FooterCell = ({ t, totalAmount }) => (
-
{t('FINANCES.TRANSACTION.DAILY_TOTAL')}
-
{quantityTotal}
+
{t('common:TOTAL')}
{totalAmount}
); @@ -73,9 +71,6 @@ const FooterCell = ({ t, quantityTotal, totalAmount }) => ( export default function EntitySaleTable({ data, currencySymbol, mobileView, titleLabel }) { const { t } = useTranslation(); const { items, amount, relatedId } = data; - const quantityUnit = items?.[0]?.quantityUnit; - const quantityTotal = items.reduce((total, { quantity }) => total + quantity, 0); - const quantityWithUnit = `${quantityTotal} ${quantityUnit}`; const totalAmount = `${currencySymbol}${amount.toFixed(2)}`; if (!items?.length) { @@ -85,15 +80,11 @@ export default function EntitySaleTable({ data, currencySymbol, mobileView, titl return (
- : null - } + FooterCell={mobileView ? () => : null} onClickMore={() => history.push(createRevenueDetailsUrl(relatedId))} /> ); diff --git a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/styles.module.scss b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/styles.module.scss index 37c1fee399..b4e0393dc2 100644 --- a/packages/webapp/src/components/Finances/Transaction/ExpandedContent/styles.module.scss +++ b/packages/webapp/src/components/Finances/Transaction/ExpandedContent/styles.module.scss @@ -69,6 +69,10 @@ button.toDetail { font-weight: bold; } +.uppercase { + text-transform: uppercase; +} + // Crop sales .mobileCrops { height: 100%; From 1692236b12881c8c3596a39727054cd84fb318d9 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Wed, 13 May 2026 16:14:17 -0700 Subject: [PATCH 27/36] LF-5274 Minor string cleanup --- packages/webapp/public/locales/en/translation.json | 4 +--- .../containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 2 +- .../containers/Finances/EntitySaleInputs/CropSaleInputs.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 7af22085e5..a9a2b9a691 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1881,12 +1881,10 @@ "ADD_SALE": { "ADD_CUSTOM_REVENUE_TYPE": "Add custom revenue type", "ADD_REVENUE": "Add revenue", - "SELECT_ANIMALS": "Select animals", "ANIMAL_NOTES_PLACEHOLDER": "Animal sale description", - "ANIMAL_REQUIRED": "Required", "CROP_NOTES_PLACEHOLDER": "Crop sale description", "CROP_REQUIRED": "Required", - "SELECT_CROPS": "Select crops", + "CROP_VARIETY": "Select crops", "FLOW": "revenue creation", "MANAGE_CUSTOM_REVENUE_TYPE": "Manage custom revenue types", "NOTES_PLACEHOLDER": "Sale description", diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index 4c7241cc74..c4b9523581 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -114,7 +114,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu savedSalesById={savedSalesById} fieldPrefix={ANIMAL_SALE} entityIdFieldKey={ANIMAL_KEY} - placeholder={t('SALE.ADD_SALE.SELECT_ANIMALS')} + placeholder={t('TASK.SELECT_ANIMALS')} > {({ option, system, currency, disabledInput }) => ( {({ option, system, currency, disabledInput }) => ( Date: Wed, 13 May 2026 16:25:20 -0700 Subject: [PATCH 28/36] LF-5274 Clean up RevenueForm stylesheet; lots of unused classes here --- .../Forms/RevenueForm/EntitySaleEntries.tsx | 2 +- .../Forms/RevenueForm/styles.module.scss | 95 +------------------ 2 files changed, 2 insertions(+), 95 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx index 039c1763b0..269bae1957 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx +++ b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx @@ -90,7 +90,7 @@ export default function EntitySaleEntries({ }; return ( -
+
Date: Thu, 14 May 2026 08:55:39 -0700 Subject: [PATCH 29/36] LF-5274 Rename customFormChildrenDefaultValues and fix story --- .../webapp/src/components/Forms/RevenueForm/index.jsx | 6 +++--- .../src/containers/Finances/RevenueDetail/index.jsx | 7 ++----- .../webapp/src/stories/Finances/RevenueForm.stories.jsx | 9 +++------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index 4e9ba46628..b1a0fc7412 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -51,7 +51,7 @@ const RevenueForm = ({ buttonText, revenueTypes, onRetire, - customFormChildrenDefaultValues, + entitySaleDefaultValues, }) => { const { t } = useTranslation(); const [isDeleting, setIsDeleting] = useState(false); @@ -70,7 +70,7 @@ const RevenueForm = ({ }), [VALUE]: !isNaN(data[VALUE]) ? data[VALUE] : null, [NOTE]: data[NOTE] ?? null, - ...customFormChildrenDefaultValues, + ...entitySaleDefaultValues, }, }); @@ -270,7 +270,7 @@ RevenueForm.propTypes = { buttonText: PropTypes.string.isRequired, revenueTypes: PropTypes.array.isRequired, onRetire: PropTypes.func, - customFormChildrenDefaultValues: PropTypes.object, + entitySaleDefaultValues: PropTypes.object, }; export default RevenueForm; diff --git a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx index 0bee64b4fa..f30c93c8e1 100644 --- a/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx +++ b/packages/webapp/src/containers/Finances/RevenueDetail/index.jsx @@ -77,10 +77,7 @@ function RevenueDetail() { setValue(REVENUE_TYPE_OPTION, newType); }; - const customFormChildrenDefaultValues = getEntityTypeDefaultValues( - sale, - revenueType?.entity_type, - ); + const entitySaleDefaultValues = getEntityTypeDefaultValues(sale, revenueType?.entity_type); return ( ); } diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index e9634695a0..f505d414f5 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -14,10 +14,7 @@ */ import { useState } from 'react'; import RevenueForm from '../../components/Forms/RevenueForm'; -import { - getCropSaleDefaultValues, - getAnimalSaleDefaultValues, -} from '../../containers/Finances/EntitySaleInputs'; +import { getEntityTypeDefaultValues } from '../../containers/Finances/EntitySaleInputs'; import { componentDecorators } from '../Pages/config/Decorators'; const cropSale = { @@ -210,7 +207,7 @@ CropSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - customFormChildrenDefaultValues: getCropSaleDefaultValues(cropSale), + entitySaleDefaultValues: getEntityTypeDefaultValues(cropSale, 'crop'), }; export const AnimalSaleDetail = Template.bind({}); @@ -221,5 +218,5 @@ AnimalSaleDetail.args = { revenueTypeOptions, onRetire: () => {}, revenueTypes, - customFormChildrenDefaultValues: getAnimalSaleDefaultValues(animalSale), + entitySaleDefaultValues: getEntityTypeDefaultValues(animalSale, 'animal'), }; From f826c4438d39fe2a0b71a02edf70399b4dcf8277 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Thu, 14 May 2026 09:21:20 -0700 Subject: [PATCH 30/36] LF-5274 Add missing label to the entity select; match Animal Sale description to Figma --- packages/webapp/public/locales/en/revenue.json | 2 +- packages/webapp/public/locales/en/translation.json | 3 ++- .../src/components/Forms/RevenueForm/EntitySaleEntries.tsx | 4 ++++ .../containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 1 + .../containers/Finances/EntitySaleInputs/CropSaleInputs.tsx | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/webapp/public/locales/en/revenue.json b/packages/webapp/public/locales/en/revenue.json index c4ff1016fc..586dc12455 100644 --- a/packages/webapp/public/locales/en/revenue.json +++ b/packages/webapp/public/locales/en/revenue.json @@ -1,7 +1,7 @@ { "ANIMAL_SALE": { "REVENUE_NAME": "Animal Sale", - "CUSTOM_DESCRIPTION": "Revenues associated with the sale of animals from this farm." + "CUSTOM_DESCRIPTION": "Revenues generated from the sales of animal products." }, "CROP_SALE": { "REVENUE_NAME": "Crop Sale", diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index a9a2b9a691..d4910fc5cf 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -1884,11 +1884,12 @@ "ANIMAL_NOTES_PLACEHOLDER": "Animal sale description", "CROP_NOTES_PLACEHOLDER": "Crop sale description", "CROP_REQUIRED": "Required", - "CROP_VARIETY": "Select crops", + "CROP_VARIETY": "Crop variety", "FLOW": "revenue creation", "MANAGE_CUSTOM_REVENUE_TYPE": "Manage custom revenue types", "NOTES_PLACEHOLDER": "Sale description", "SALE_VALUE_ERROR": "Sale value must be a positive number less than 999,999,999", + "SELECT_CROPS": "Select crops", "TABLE_HEADERS": { "TOTAL": "Total" }, diff --git a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx index 269bae1957..c81d0e9499 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx +++ b/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx @@ -23,6 +23,7 @@ import { QUANTITY, QUANTITY_UNIT, SALE_VALUE } from './constants'; import { CheckboxMultiSelect } from '../../Form/ReactSelect/CheckboxMultiSelect'; import type { SelectOption } from '../../Form/ReactSelect/CheckboxMultiSelect'; import { Error } from '../../Typography'; +import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import styles from './styles.module.scss'; import { useCurrencySymbol } from '../../../containers/hooks/useCurrencySymbol'; @@ -40,6 +41,7 @@ interface EntitySaleEntriesProps { savedSalesById: Record | null | undefined; fieldPrefix: string; entityIdFieldKey: string; + label: string; placeholder?: string; children: (props: EntitySaleItemProps) => ReactNode; } @@ -50,6 +52,7 @@ export default function EntitySaleEntries({ savedSalesById, fieldPrefix, entityIdFieldKey, + label, placeholder, children, }: EntitySaleEntriesProps): ReactNode { @@ -92,6 +95,7 @@ export default function EntitySaleEntries({ return (
+ {({ option, system, currency, disabledInput }) => ( diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx index 9b3546a6d7..19a7113b16 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx @@ -110,6 +110,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr savedSalesById={savedSalesById} fieldPrefix={CROP_VARIETY_SALE} entityIdFieldKey={CROP_VARIETY_ID} + label={t('SALE.ADD_SALE.CROP_VARIETY')} placeholder={t('SALE.ADD_SALE.CROP_VARIETY')} > {({ option, system, currency, disabledInput }) => ( From 296565c08a7e9e8e861dd7e6a3478fad1bd6f40c Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Thu, 14 May 2026 09:38:33 -0700 Subject: [PATCH 31/36] LF-5274 Remove unused REVENUE_FORM_TYPE and getRevenueFormType Must be from a previous implementation? Exported but not imported anywhere --- .../webapp/src/containers/Finances/constants.js | 6 ------ packages/webapp/src/containers/Finances/util.js | 14 +------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/webapp/src/containers/Finances/constants.js b/packages/webapp/src/containers/Finances/constants.js index 12dda46e0d..8814c5e5d8 100644 --- a/packages/webapp/src/containers/Finances/constants.js +++ b/packages/webapp/src/containers/Finances/constants.js @@ -33,12 +33,6 @@ export const UPDATE_SALE = 'UPDATE_SALE'; export const DELETE_EXPENSE = 'DELETE_EXPENSE'; export const SET_IS_FETCHING_DATA = 'SET_IS_FETCHING_DATA'; -export const REVENUE_FORM_TYPES = { - CROP_SALE: 'crop_sale', - ANIMAL_SALE: 'animal_sale', - GENERAL: 'general', -}; - export const LABOUR_ITEMS_GROUPING_OPTIONS = { EMPLOYEE: 'EMPLOYEE', TASK_TYPE: 'TASK_TYPE', diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index cc33d71ca3..b2f2bf5395 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -35,7 +35,7 @@ import i18n from '../../locales/i18n'; import { getMass, getMassUnit, roundToTwoDecimal } from '../../util'; import { isSameDay } from '../../util/date-migrate-TS'; import { getLanguageFromLocalStorage } from '../../util/getLanguageFromLocalStorage'; -import { LABOUR_ITEMS_GROUPING_OPTIONS, REVENUE_FORM_TYPES } from './constants'; +import { LABOUR_ITEMS_GROUPING_OPTIONS } from './constants'; import { transactionTypeEnum } from './useTransactions'; import { parseInventoryId } from '../../util/animal'; import { AnimalOrBatchKeys } from '../Animals/types'; @@ -99,16 +99,6 @@ export function calcActualRevenueFromRevenueItems(revenueItems) { return revenueItems.reduce((sum, curItem) => sum + curItem.totalAmount, 0); } -export const getRevenueFormType = (revenueType) => { - if (revenueType?.entity_type === 'crop') { - return REVENUE_FORM_TYPES.CROP_SALE; - } - if (revenueType?.entity_type === 'animal') { - return REVENUE_FORM_TYPES.ANIMAL_SALE; - } - return REVENUE_FORM_TYPES.GENERAL; -}; - export const mapTasksToLabourItems = (tasks, taskTypes, users) => { const groupingOptions = [ { @@ -286,7 +276,6 @@ export function mapRevenueFormDataToApiCallFormat(data, revenueTypes, sale_id, f }); } else if (revenueType?.entity_type === 'animal') { sale.value = undefined; - sale.animal_sale = Object.values(data[ANIMAL_SALE]).map((a) => { const { kind, id } = parseInventoryId(a[ANIMAL_KEY]); const isBatch = kind === AnimalOrBatchKeys.BATCH; @@ -348,4 +337,3 @@ export const getFinanceTypeSearchableStringFunc = (typeCategory) => (type) => { export const isCropSale = (revenueType) => revenueType?.entity_type === 'crop'; export const isAnimalSale = (revenueType) => revenueType?.entity_type === 'animal'; -export const isGeneralSale = (revenueType) => !revenueType?.entity_type; From b45719e3ceadba43174acdabac389ac19583585e Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Thu, 14 May 2026 10:07:04 -0700 Subject: [PATCH 32/36] LF-5274 Rename EntitySaleEntries to EntitySalePicker --- .../{EntitySaleEntries.tsx => EntitySalePicker.tsx} | 8 ++++---- .../src/components/Forms/RevenueForm/styles.module.scss | 2 +- .../Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 6 +++--- .../Finances/EntitySaleInputs/CropSaleInputs.tsx | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) rename packages/webapp/src/components/Forms/RevenueForm/{EntitySaleEntries.tsx => EntitySalePicker.tsx} (95%) diff --git a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx b/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx similarity index 95% rename from packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx rename to packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx index c81d0e9499..b2cc107d7b 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/EntitySaleEntries.tsx +++ b/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx @@ -35,7 +35,7 @@ export interface EntitySaleItemProps { disabledInput: boolean; } -interface EntitySaleEntriesProps { +interface EntitySalePickerProps { disabledInput: boolean; options: SelectOption[]; savedSalesById: Record | null | undefined; @@ -46,7 +46,7 @@ interface EntitySaleEntriesProps { children: (props: EntitySaleItemProps) => ReactNode; } -export default function EntitySaleEntries({ +export default function EntitySalePicker({ disabledInput, options, savedSalesById, @@ -55,7 +55,7 @@ export default function EntitySaleEntries({ label, placeholder, children, -}: EntitySaleEntriesProps): ReactNode { +}: EntitySalePickerProps): ReactNode { const { t } = useTranslation(); const system = useSelector(measurementSelector); const currency = useCurrencySymbol(); @@ -93,7 +93,7 @@ export default function EntitySaleEntries({ }; return ( -
+
)} - + ); } diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx index 19a7113b16..eb1d4dfcfb 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx @@ -22,7 +22,7 @@ import { } from '../../../components/Forms/RevenueForm/constants'; import CropSaleItem from '../../../components/Forms/RevenueForm/CropSaleItem'; import { selectManagementPlansForSale } from '../../managementPlanSlice'; -import EntitySaleEntries from '../../../components/Forms/RevenueForm/EntitySaleEntries'; +import EntitySalePicker from '../../../components/Forms/RevenueForm/EntitySalePicker'; import type { CropVarietySaleTileData } from '../../../components/CropTile/CropVarietySaleTile'; import { getUnitOptionMap } from '../../../util/convert-units/getUnitOptionMap'; import type { SelectOption } from '../../../components/Form/ReactSelect/CheckboxMultiSelect/index'; @@ -104,7 +104,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr ); return ( - )} - + ); } From 2443b18ec3490a40fded4eb7c68633f79c262392 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Thu, 14 May 2026 12:46:05 -0700 Subject: [PATCH 33/36] LF-5274 Minor cleanup and string fix --- .../src/components/Forms/RevenueForm/styles.module.scss | 2 -- .../containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 4 ---- .../containers/Finances/EntitySaleInputs/CropSaleInputs.tsx | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss b/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss index b4e51ca7c8..5a8212281d 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss +++ b/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss @@ -13,8 +13,6 @@ * GNU General Public License for more details, see . */ -@use '@assets/mixin' as *; - .entitySalePickerContainer { display: flex; flex-direction: column; diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index 8196fb2836..5c6d720201 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -66,12 +66,10 @@ export const getAnimalSaleDefaultValues = (sale: AnimalSale | undefined) => { sale.animal_sale.map((record) => { const key = saleRecordToOptionKey(record); const unit = record.quantity_unit; - const formattedEntry: AnimalSaleDefaultRecord = { ...record, quantity_unit: unit ? (unitMap[unit] ?? { label: unit, value: unit }) : undefined, }; - return [key, formattedEntry]; }), ); @@ -91,12 +89,10 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu label: chooseIdentification(a), value: generateInventoryId(AnimalOrBatchKeys.ANIMAL, a), })); - const batchOptions = (animalBatches ?? []).map((b: AnimalBatch) => ({ label: chooseIdentification(b), value: generateInventoryId(AnimalOrBatchKeys.BATCH, b), })); - return [...animalOptions, ...batchOptions].sort((a, b) => String(a.label).localeCompare(String(b.label)), ); diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx index eb1d4dfcfb..d907ca54b4 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/CropSaleInputs.tsx @@ -111,7 +111,7 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr fieldPrefix={CROP_VARIETY_SALE} entityIdFieldKey={CROP_VARIETY_ID} label={t('SALE.ADD_SALE.CROP_VARIETY')} - placeholder={t('SALE.ADD_SALE.CROP_VARIETY')} + placeholder={t('SALE.ADD_SALE.SELECT_CROPS')} > {({ option, system, currency, disabledInput }) => ( Date: Thu, 14 May 2026 17:48:42 -0700 Subject: [PATCH 34/36] LF-5274 Fix circular Redux store import causing Storybook crash Import chain is userFarm --> util/index.js (for getFirstNameWithLastInitial) --> getFromReduxStore. Not 100% sure, but I think the container layering in the new revenue components particularly revealed this with the calling of the measurementSelector from the inner EntitySalePicker component. Don't think this crash is relevant outside of Storybook though; depends on import of the store and the store provider is top-level in the actual app --- .../CardWithStatus/TaskCard/TaskCard.jsx | 2 +- .../src/components/RevisionInfoText.tsx | 2 +- .../webapp/src/containers/userFarmSlice.ts | 2 +- .../src/util/getFirstNameWithLastInitial.js | 19 +++++++++++++++++++ packages/webapp/src/util/index.js | 5 ----- 5 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 packages/webapp/src/util/getFirstNameWithLastInitial.js diff --git a/packages/webapp/src/components/CardWithStatus/TaskCard/TaskCard.jsx b/packages/webapp/src/components/CardWithStatus/TaskCard/TaskCard.jsx index 052081f44c..8f533c9c89 100644 --- a/packages/webapp/src/components/CardWithStatus/TaskCard/TaskCard.jsx +++ b/packages/webapp/src/components/CardWithStatus/TaskCard/TaskCard.jsx @@ -33,7 +33,7 @@ export const taskStatusTranslateKey = { import { languageCodes } from '../../../hooks/useLanguageOptions'; import { getIntlDate } from '../../../util/date-migrate-TS'; -import { getFirstNameWithLastInitial } from '../../../util'; +import { getFirstNameWithLastInitial } from '../../../util/getFirstNameWithLastInitial'; import RevisionInfoText from '../../RevisionInfoText'; export const PureTaskCard = ({ diff --git a/packages/webapp/src/components/RevisionInfoText.tsx b/packages/webapp/src/components/RevisionInfoText.tsx index 2c43774f1f..85ed7cefc1 100644 --- a/packages/webapp/src/components/RevisionInfoText.tsx +++ b/packages/webapp/src/components/RevisionInfoText.tsx @@ -14,7 +14,7 @@ */ import { Trans } from 'react-i18next'; -import { getFirstNameWithLastInitial } from '../util'; +import { getFirstNameWithLastInitial } from '../util/getFirstNameWithLastInitial'; import { getIntlDate } from '../util/date-migrate-TS'; /** diff --git a/packages/webapp/src/containers/userFarmSlice.ts b/packages/webapp/src/containers/userFarmSlice.ts index e4c40519ac..274d44e207 100644 --- a/packages/webapp/src/containers/userFarmSlice.ts +++ b/packages/webapp/src/containers/userFarmSlice.ts @@ -4,7 +4,7 @@ import { createSelector } from 'reselect'; import type { RootState } from '../store/store'; import { AxiosError } from 'axios'; import { CONSENT_VERSION } from '../util/constants'; -import { getFirstNameWithLastInitial } from '../util'; +import { getFirstNameWithLastInitial } from '../util/getFirstNameWithLastInitial'; export interface Units { currency: string; diff --git a/packages/webapp/src/util/getFirstNameWithLastInitial.js b/packages/webapp/src/util/getFirstNameWithLastInitial.js new file mode 100644 index 0000000000..619aab9660 --- /dev/null +++ b/packages/webapp/src/util/getFirstNameWithLastInitial.js @@ -0,0 +1,19 @@ +/* + * Copyright 2026 LiteFarm.org + * This file is part of LiteFarm. + * + * LiteFarm is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LiteFarm is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details, see . + */ + +export const getFirstNameWithLastInitial = (user) => { + const lastInitial = user.last_name?.[0]?.toUpperCase(); + return `${user.first_name}${lastInitial ? ` ${lastInitial}.` : ''}`; +}; diff --git a/packages/webapp/src/util/index.js b/packages/webapp/src/util/index.js index f980a79a4d..ae27f0eff1 100644 --- a/packages/webapp/src/util/index.js +++ b/packages/webapp/src/util/index.js @@ -177,8 +177,3 @@ export const sumObjectValues = (obj) => { export const toTranslationKey = (text) => { return text.toUpperCase().replaceAll(' ', '_'); }; - -export const getFirstNameWithLastInitial = (user) => { - const lastInitial = user.last_name?.[0]?.toUpperCase(); - return `${user.first_name}${lastInitial ? ` ${lastInitial}.` : ''}`; -}; From a5e5df76d99742126940243190a95fcf9593a597 Mon Sep 17 00:00:00 2001 From: Joyce Yuki <82857964+kathyavini@users.noreply.github.com> Date: Fri, 15 May 2026 10:00:43 -0700 Subject: [PATCH 35/36] LF-5274 Move selectors out of EntitySalePicker component Although the component > container > component sandwhich remains, I think this inner access of userFarm was the particular trigger that highlighted the circular Redux store import. --- .../components/Forms/RevenueForm/EntitySalePicker.tsx | 9 ++++----- .../Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 7 +++++++ .../Finances/EntitySaleInputs/CropSaleInputs.tsx | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx b/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx index b2cc107d7b..3dfedc500f 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx +++ b/packages/webapp/src/components/Forms/RevenueForm/EntitySalePicker.tsx @@ -17,15 +17,12 @@ import { ReactNode, useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { MultiValue } from 'react-select'; import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { measurementSelector } from '../../../containers/userFarmSlice'; import { QUANTITY, QUANTITY_UNIT, SALE_VALUE } from './constants'; import { CheckboxMultiSelect } from '../../Form/ReactSelect/CheckboxMultiSelect'; import type { SelectOption } from '../../Form/ReactSelect/CheckboxMultiSelect'; import { Error } from '../../Typography'; import InputBaseLabel from '../../Form/InputBase/InputBaseLabel'; import styles from './styles.module.scss'; -import { useCurrencySymbol } from '../../../containers/hooks/useCurrencySymbol'; export interface EntitySaleItemProps { option: SelectOption; @@ -43,6 +40,8 @@ interface EntitySalePickerProps { entityIdFieldKey: string; label: string; placeholder?: string; + system: string; + currency: string; children: (props: EntitySaleItemProps) => ReactNode; } @@ -54,11 +53,11 @@ export default function EntitySalePicker({ entityIdFieldKey, label, placeholder, + system, + currency, children, }: EntitySalePickerProps): ReactNode { const { t } = useTranslation(); - const system = useSelector(measurementSelector); - const currency = useCurrencySymbol(); const { register, unregister, getValues, setValue } = useFormContext(); const [selectedOptions, setSelectedOptions] = useState(() => diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index 5c6d720201..f9c172d01e 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -14,11 +14,14 @@ */ import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { ANIMAL_KEY, ANIMAL_SALE } from '../../../components/Forms/RevenueForm/constants'; import AnimalSaleItem from '../../../components/Forms/RevenueForm/AnimalSaleItem'; import EntitySalePicker from '../../../components/Forms/RevenueForm/EntitySalePicker'; import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../../store/api/apiSlice'; +import { measurementSelector } from '../../userFarmSlice'; +import { useCurrencySymbol } from '../../hooks/useCurrencySymbol'; import { chooseIdentification } from '../../Animals/utils'; import { getUnitOptionMap } from '../../../util/convert-units/getUnitOptionMap'; import { generateInventoryId } from '../../../util/animal'; @@ -81,6 +84,8 @@ export const getAnimalSaleDefaultValues = (sale: AnimalSale | undefined) => { export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInputsProps) { const { t } = useTranslation(); + const system = useSelector(measurementSelector); + const currency = useCurrencySymbol(); const { data: animals } = useGetAnimalsQuery(); const { data: animalBatches } = useGetAnimalBatchesQuery(); @@ -112,6 +117,8 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu entityIdFieldKey={ANIMAL_KEY} label={t('FINANCES.TRANSACTION.ANIMALS')} placeholder={t('TASK.SELECT_ANIMALS')} + system={system} + currency={currency} > {({ option, system, currency, disabledInput }) => ( selectManagementPlansForSale(state, sale?.crop_variety_sale), ); @@ -112,6 +116,8 @@ export default function CropSaleInputs({ sale, disabledInput }: CropSaleInputsPr entityIdFieldKey={CROP_VARIETY_ID} label={t('SALE.ADD_SALE.CROP_VARIETY')} placeholder={t('SALE.ADD_SALE.SELECT_CROPS')} + system={system} + currency={currency} > {({ option, system, currency, disabledInput }) => ( Date: Fri, 15 May 2026 12:06:24 -0700 Subject: [PATCH 36/36] LF-5274 Minor changes: copyright year formatting and more descriptive constant for ANIMAL_KEY --- .../webapp/src/components/Forms/RevenueForm/constants.js | 2 +- packages/webapp/src/components/Forms/RevenueForm/index.jsx | 2 +- .../src/components/Forms/RevenueForm/styles.module.scss | 2 +- .../Finances/EntitySaleInputs/AnimalSaleInputs.tsx | 6 +++--- packages/webapp/src/containers/Finances/util.js | 4 ++-- .../webapp/src/stories/Finances/RevenueForm.stories.jsx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/webapp/src/components/Forms/RevenueForm/constants.js b/packages/webapp/src/components/Forms/RevenueForm/constants.js index e163898501..0c89554be1 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/constants.js +++ b/packages/webapp/src/components/Forms/RevenueForm/constants.js @@ -33,4 +33,4 @@ export const SALE_VALUE = 'sale_value'; // animal sale export const ANIMAL_SALE = 'animal_sale'; -export const ANIMAL_KEY = 'animal_key'; +export const ANIMAL_INVENTORY_ID = 'animal_inventory_id'; diff --git a/packages/webapp/src/components/Forms/RevenueForm/index.jsx b/packages/webapp/src/components/Forms/RevenueForm/index.jsx index b1a0fc7412..76c267a8f9 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/index.jsx +++ b/packages/webapp/src/components/Forms/RevenueForm/index.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023-26 LiteFarm.org + * Copyright 2023-2026 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify diff --git a/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss b/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss index 5a8212281d..3fc3fc6454 100644 --- a/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss +++ b/packages/webapp/src/components/Forms/RevenueForm/styles.module.scss @@ -1,5 +1,5 @@ /* - * Copyright 2023 LiteFarm.org + * Copyright 2023-2026 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify diff --git a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx index f9c172d01e..111eb24e76 100644 --- a/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx +++ b/packages/webapp/src/containers/Finances/EntitySaleInputs/AnimalSaleInputs.tsx @@ -16,7 +16,7 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import { ANIMAL_KEY, ANIMAL_SALE } from '../../../components/Forms/RevenueForm/constants'; +import { ANIMAL_INVENTORY_ID, ANIMAL_SALE } from '../../../components/Forms/RevenueForm/constants'; import AnimalSaleItem from '../../../components/Forms/RevenueForm/AnimalSaleItem'; import EntitySalePicker from '../../../components/Forms/RevenueForm/EntitySalePicker'; import { useGetAnimalsQuery, useGetAnimalBatchesQuery } from '../../../store/api/apiSlice'; @@ -114,7 +114,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu options={options} savedSalesById={savedSalesById} fieldPrefix={ANIMAL_SALE} - entityIdFieldKey={ANIMAL_KEY} + entityIdFieldKey={ANIMAL_INVENTORY_ID} label={t('FINANCES.TRANSACTION.ANIMALS')} placeholder={t('TASK.SELECT_ANIMALS')} system={system} @@ -128,7 +128,7 @@ export default function AnimalSaleInputs({ sale, disabledInput }: AnimalSaleInpu system={system} currency={currency} fieldPrefix={ANIMAL_SALE} - entityIdFieldKey={ANIMAL_KEY} + entityIdFieldKey={ANIMAL_INVENTORY_ID} disabledInput={disabledInput} /> )} diff --git a/packages/webapp/src/containers/Finances/util.js b/packages/webapp/src/containers/Finances/util.js index b2f2bf5395..2a4bab62a7 100644 --- a/packages/webapp/src/containers/Finances/util.js +++ b/packages/webapp/src/containers/Finances/util.js @@ -17,7 +17,7 @@ import { groupBy as lodashGroupBy } from 'lodash-es'; import moment from 'moment'; import { useTranslation } from 'react-i18next'; import { - ANIMAL_KEY, + ANIMAL_INVENTORY_ID, ANIMAL_SALE, CROP_VARIETY_ID, CROP_VARIETY_SALE, @@ -277,7 +277,7 @@ export function mapRevenueFormDataToApiCallFormat(data, revenueTypes, sale_id, f } else if (revenueType?.entity_type === 'animal') { sale.value = undefined; sale.animal_sale = Object.values(data[ANIMAL_SALE]).map((a) => { - const { kind, id } = parseInventoryId(a[ANIMAL_KEY]); + const { kind, id } = parseInventoryId(a[ANIMAL_INVENTORY_ID]); const isBatch = kind === AnimalOrBatchKeys.BATCH; return { sale_value: a[SALE_VALUE], diff --git a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx index f505d414f5..2f7302c78d 100644 --- a/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx +++ b/packages/webapp/src/stories/Finances/RevenueForm.stories.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2023-26 LiteFarm.org + * Copyright 2023-2026 LiteFarm.org * This file is part of LiteFarm. * * LiteFarm is free software: you can redistribute it and/or modify