Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b9b61c
LF-4980 Add filteredProductsSelector
SayakaOno Oct 10, 2025
fae28cd
LF-4980 Replace products selector for inventory
SayakaOno Oct 10, 2025
7985513
LF-4980 Add isLibraryProduct function
SayakaOno Oct 10, 2025
1497067
LF-4980 Use isLibraryProduct in products selector
SayakaOno Oct 10, 2025
4b7b938
LF-4980 Adjust products for SoilAmdnementProductForm read-only
SayakaOno Oct 10, 2025
f597a1b
LF-4980 Adjust new product name validation
SayakaOno Oct 10, 2025
70e9d7c
LF-4980 Adjust products selector for task creation
SayakaOno Oct 10, 2025
fb48e8d
LF-4980 Reset product form values when mounting SoilAmendmentProductCard
SayakaOno Oct 10, 2025
dcd6947
LF-4980 Add translation string
SayakaOno Oct 10, 2025
0bcaadc
LF-4980 Update Product type
SayakaOno Oct 10, 2025
f8158c3
LF-4980 Adjust productOptions generation in SoilAmendmentProductCard
SayakaOno Oct 10, 2025
8568d8f
LF-4980 Fix productOptions generation logic
SayakaOno Oct 15, 2025
731f927
LF-4980 Fix products selector factory and memoize selectors
SayakaOno Oct 16, 2025
b46644c
LF-4980 Tweaks
SayakaOno Oct 16, 2025
efc6632
LF-4980 Add missing includeLibrary default value in makeFilteredProdu…
SayakaOno Oct 16, 2025
02147f7
LF-4980 Update ProductForm's productsSelectorArgs
SayakaOno Oct 16, 2025
aa6e712
LF-4980 Improve makeFilteredProductsSelector
SayakaOno Oct 17, 2025
cacdef4
LF-4980 Fix productsSelectorArgs in TaskDetails
SayakaOno Oct 17, 2025
a390fbd
LF-4980 Add comment
SayakaOno Oct 17, 2025
87c7cfd
LF-4980 Add ProductInventorySelector and use it in ProductInventory
SayakaOno Oct 20, 2025
d63722f
LF-4980 Replace makeFilteredProductsSelector with global selector
SayakaOno Oct 20, 2025
6eda9e6
LF-4980 Update productsForTaskTypeSelector from factory to global sel…
SayakaOno Oct 20, 2025
0ab2718
Merge branch 'integration' into LF-4980/Show_valid_soil_amendment_pro…
SayakaOno Oct 23, 2025
abc5e21
LF-4980 Filter products in PureSoilAmendmentTask
SayakaOno Oct 23, 2025
3a692e6
LF-4980 Update hasAvailableProductsSelector to use productInventorySe…
SayakaOno Oct 23, 2025
4945585
LF-4980 Remove products filter in TaskDetails
SayakaOno Oct 23, 2025
845a2bd
LF-4980 Simplify customProductNames in PureSoilAmendmentProductForm
SayakaOno Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/webapp/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"QUANTITY": "Quantity",
"REMOVE": "Remove",
"REMOVE_ITEM": "Remove item",
"REMOVED": "Removed",
"REQUIRED": "Required",
"RETIRE": "Retire",
"REVISION_INFO": "Revised on <strong>{{date}}</strong> <i>by</i> <strong>{{user}}</strong>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFormContext } from 'react-hook-form';
import Input, { getInputErrors } from '../../Form/Input';
import ProductDetails, { type ProductDetailsProps } from '../../Form/ProductDetails';
import { hookFormMaxCharsValidation } from '../../Form/hookformValidationUtils';
import { getSoilAmendmentFormValues } from '../../Form/ProductDetails/utils';
import { isLibraryProduct } from '../../../util/product';
import { productDefaultValuesByType } from '../../../containers/ProductInventory/ProductForm/constants';
import { TASK_TYPES } from '../../../containers/Task/constants';
import { PRODUCT_FIELD_NAMES } from '../../Task/AddSoilAmendmentProducts/types';
Expand Down Expand Up @@ -79,7 +80,11 @@ const PureSoilAmendmentProductForm = ({
}
}, [mode]);

const productNames: SoilAmendmentProduct['name'][] = products.map(({ name }) => name);
const customProductNames = useMemo(() => {
return products
.filter((product) => !product.removed && !isLibraryProduct(product))
.map(({ name }) => name);
}, [products]);

return (
<div className={styles.soilAmendmentProductForm}>
Expand All @@ -95,7 +100,7 @@ const PureSoilAmendmentProductForm = ({
// Allow duplicate check to pass if keeping the original name during edit
if (
!(mode === FormMode.EDIT && value === product?.name) &&
productNames.includes(value)
customProductNames.includes(value)
) {
return t('ADD_TASK.DUPLICATE_NAME');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ import { hookFormMaxCharsValidation } from '../../../Form/hookformValidationUtil
import { soilAmendmentProductDetailsDefaultValues } from '../../../../containers/ProductInventory/ProductForm/constants';
import { getSoilAmendmentFormValues } from '../../../Form/ProductDetails/utils';

const findProduct = (
products?: SoilAmendmentProduct[],
productId?: SoilAmendmentProduct['product_id'],
) => {
return products?.find(({ product_id }) => product_id === productId);
};

export type ProductCardProps = ProductDetailsProps & {
namePrefix: string;
system: 'metric' | 'imperial';
Expand Down Expand Up @@ -128,7 +135,8 @@ const SoilAmendmentProductCard = ({

const selectRef = useRef<SelectRef>(null);
const productOptions = products.map(({ product_id, name, ...rest }) => {
return { value: product_id, label: name, data: rest };
const label = name + (rest.removed ? ` (${t('common:REMOVED')})` : '');
return { value: product_id, label, data: rest };
});

useEffect(() => {
Expand All @@ -137,6 +145,11 @@ const SoilAmendmentProductCard = ({
}
}, [otherPurposeId]);

useEffect(() => {
const selectedProduct = findProduct(products, productId);
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
}, []);

return (
<div className={styles.productCard}>
{!isReadOnly && onRemove && (
Expand All @@ -154,7 +167,7 @@ const SoilAmendmentProductCard = ({
options={productOptions}
onChange={(e) => {
onChange(e?.value);
const selectedProduct = products.find(({ product_id }) => product_id === e?.value);
const selectedProduct = findProduct(products, e?.value);
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
}}
value={productOptions.find(({ value: id }) => id === value)}
Expand Down
16 changes: 14 additions & 2 deletions packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,22 @@ import { furrow_hole_depth } from '../../../util/convert-units/unit';
import styles from './styles.module.scss';
import { locationsSelector } from '../../../containers/locationSlice';

// Return products in inventory plus removed ones already used in the task.
const getAvailableProducts = (
usedProductsInTask: SoilAmendmentProduct[] | undefined,
products: SoilAmendmentProduct[],
) => {
const usedProductIds = new Set(usedProductsInTask?.map(({ product_id }) => product_id));
return products.filter((product) => !product.removed || usedProductIds.has(product.product_id));
};

type PureSoilAmendmentTaskProps = UseFormReturn &
Pick<ProductCardProps, 'farm' | 'system' | 'products'> & {
disabled: boolean;
task?: { locations: { location_id: number }[] };
task?: {
locations: { location_id: number }[];
soil_amendment_task_products: SoilAmendmentProduct[];
};
locations: { location_id: number }[];
};

Expand Down Expand Up @@ -158,7 +170,7 @@ const PureSoilAmendmentTask = ({
<AddSoilAmendmentProducts
farm={farm}
system={system}
products={products}
products={getAvailableProducts(task?.soil_amendment_task_products, products)}
purposes={purposes}
fertiliserTypes={fertiliserTypes}
isReadOnly={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import { useGetSoilAmendmentFertiliserTypesQuery } from '../../../store/api/apiS
import PureSoilAmendmentProductForm from '../../../components/ProductInventory/ProductForm/PureSoilAmendmentProductForm';
import { userFarmSelector } from '../../userFarmSlice';
import { certifierSurveySelector } from '../../OrganicCertifierSurvey/slice';
import { productsSelector } from '../../productSlice';
import { productsForTaskTypeSelector } from '../../productSlice';
import { TASK_TYPES } from '../../Task/constants';
import { FormMode } from '..';
import { FormContentProps } from '.';

const taskType = { task_translation_key: TASK_TYPES.SOIL_AMENDMENT.toUpperCase() };

export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) {
const { t } = useTranslation();

Expand All @@ -38,12 +40,9 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten
label: t(`ADD_PRODUCT.${key}_FERTILISER`),
}));

const products = useSelector(productsSelector);

// TODO: Filter out removed products
const soilAmendmentCustomProducts = products.filter(
(product) => product.type === TASK_TYPES.SOIL_AMENDMENT,
);
const soilAmendmentProducts =
/* @ts-expect-error https://github.com/reduxjs/reselect/issues/550#issuecomment-999701108 */
useSelector((state) => productsForTaskTypeSelector(state, taskType)) || [];

const isReadOnly = mode === FormMode.READ_ONLY;

Expand All @@ -53,7 +52,7 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten
isReadOnly={isReadOnly}
farm={{ farm_id, interested, country_id }}
fertiliserTypeOptions={fertiliserTypeOptions}
products={soilAmendmentCustomProducts}
products={soilAmendmentProducts}
productId={productId}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/webapp/src/containers/ProductInventory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { ReactComponent as BookIcon } from '../../assets/images/book-closed.svg'
import useSearchFilter from '../../containers/hooks/useSearchFilter';
import PureProductInventory from '../../components/ProductInventory';
import { getProducts } from '../Task/saga';
import { productsSelector } from '../productSlice';
import { productInventorySelector } from '../productSlice';
import { Product, SoilAmendmentProduct } from '../../store/api/types';
import { TASK_TYPES } from '../Task/constants';
import { SearchProps } from '../../components/Animals/Inventory';
Expand Down Expand Up @@ -65,7 +65,7 @@ export default function ProductInventory() {
dispatch(getProducts());
}, []);

const productInventory = useSelector(productsSelector);
const productInventory = useSelector(productInventorySelector);

const inventory = productInventory
.filter((product) => product.type === TASK_TYPES.SOIL_AMENDMENT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function TaskCompleteStepOne() {
const { task_id } = useParams();
const task = useSelector(taskWithProductSelector(task_id));
const selectedTaskType = task?.taskType;
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
const persistedPaths = [`/tasks/${task_id}/complete`];

const onContinue = (data) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function TaskDetails() {
const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual);
const persistedFormData = useSelector(hookFormPersistSelector);
const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id));
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
const managementPlanIds = persistedFormData.managementPlans?.map(
({ management_plan_id }) => management_plan_id,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function TaskReadOnly() {
const system = useSelector(measurementSelector);
const task = useReadonlyTask(task_id);
const selectedTaskType = task?.taskType;
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
const isIrrigationTaskWithExternalPrescription =
isTaskType(selectedTaskType, 'IRRIGATION_TASK') &&
task?.irrigation_task?.irrigation_prescription_external_id != null;
Expand Down
27 changes: 16 additions & 11 deletions packages/webapp/src/containers/productSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { loginSelector, onLoadingFail, onLoadingStart } from './userFarmSlice';
import { pick } from '../util/pick';
import { createSelector } from 'reselect';
import { isLibraryProduct } from '../util/product';

export const getProduct = (obj) => {
return pick(obj, [
Expand Down Expand Up @@ -55,29 +56,33 @@ const productSelectors = productAdapter.getSelectors(
(state) => state.entitiesReducer[productSlice.name],
);

// Select farm products including removed ones
export const productsSelector = createSelector(
[productSelectors.selectAll, loginSelector],
(products, { farm_id }) => {
return products.filter((product) => product.farm_id === farm_id);
},
);

export const productsForTaskTypeSelector = (taskType) => {
return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => {
// Select farm products for a given type including removed ones
export const productsForTaskTypeSelector = createSelector(
[productsSelector, (_state, taskType) => taskType],
(products, taskType) => {
if (taskType === undefined) {
return undefined;
}
return products.filter(
(product) =>
product.farm_id === farm_id && product.type === taskType.task_translation_key.toLowerCase(),
);
});
};
return products.filter(({ type }) => type === taskType.task_translation_key.toLowerCase());
},
);

export const productInventorySelector = createSelector([productsSelector], (products) => {
return products.filter((product) => !product.removed);
});

export const hasAvailableProductsSelector = createSelector(
[productsSelector, (_state, type) => type],
(products, type) => {
return products.some((product) => !product.removed && (!type || product.type === type));
[productInventorySelector, (_state, type) => type],
(productInventory, type) => {
return productInventory.some((product) => !type || product.type === type);
},
);

Expand Down
1 change: 1 addition & 0 deletions packages/webapp/src/store/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export interface Product {
| typeof TASK_TYPES.PEST_CONTROL;
farm_id?: string;
on_permitted_substances_list?: 'YES' | 'NO' | 'NOT_SURE' | null;
removed: boolean;
}

export enum ElementalUnit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const products = [
k: 3,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 2,
Expand All @@ -45,6 +46,7 @@ export const products = [
k: 3.3,
elemental_unit: ElementalUnit.RATIO,
},
removed: false,
},
{
product_id: 3,
Expand All @@ -60,6 +62,7 @@ export const products = [
k: undefined,
elemental_unit: undefined,
},
removed: false,
},
{
product_id: 4,
Expand All @@ -75,6 +78,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 5,
Expand All @@ -90,6 +94,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
removed: false,
},
{
product_id: 6,
Expand All @@ -105,5 +110,6 @@ export const products = [
k: undefined,
elemental_unit: ElementalUnit.RATIO,
},
removed: false,
},
];
21 changes: 21 additions & 0 deletions packages/webapp/src/util/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2025 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 <https://www.gnu.org/licenses/>.
*/

import { Product } from '../store/api/types';

// LF-4963 - confirm property that will distinguish custom from library products
export const isLibraryProduct = (product: Product) => {
return !!product.product_translation_key;
};
Loading