diff --git a/packages/webapp/public/locales/en/common.json b/packages/webapp/public/locales/en/common.json
index b8fca7199c..a71ed790dc 100644
--- a/packages/webapp/public/locales/en/common.json
+++ b/packages/webapp/public/locales/en/common.json
@@ -79,6 +79,7 @@
"QUANTITY": "Quantity",
"REMOVE": "Remove",
"REMOVE_ITEM": "Remove item",
+ "REMOVED": "Removed",
"REQUIRED": "Required",
"RETIRE": "Retire",
"REVISION_INFO": "Revised on {{date}} by {{user}}",
diff --git a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx
index 0bc3363b6f..e605a4ea95 100644
--- a/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx
+++ b/packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx
@@ -13,13 +13,14 @@
* GNU General Public License for more details, see .
*/
-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';
@@ -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 (
@@ -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');
}
diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx
index 532747689a..4fa90de0b6 100644
--- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx
+++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx
@@ -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';
@@ -128,7 +135,8 @@ const SoilAmendmentProductCard = ({
const selectRef = useRef
(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(() => {
@@ -137,6 +145,11 @@ const SoilAmendmentProductCard = ({
}
}, [otherPurposeId]);
+ useEffect(() => {
+ const selectedProduct = findProduct(products, productId);
+ nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
+ }, []);
+
return (
{!isReadOnly && onRemove && (
@@ -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)}
diff --git a/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx b/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx
index 597fe3328d..a528b6daba 100644
--- a/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx
+++ b/packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx
@@ -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
& {
disabled: boolean;
- task?: { locations: { location_id: number }[] };
+ task?: {
+ locations: { location_id: number }[];
+ soil_amendment_task_products: SoilAmendmentProduct[];
+ };
locations: { location_id: number }[];
};
@@ -158,7 +170,7 @@ const PureSoilAmendmentTask = ({
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;
@@ -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}
/>
);
diff --git a/packages/webapp/src/containers/ProductInventory/index.tsx b/packages/webapp/src/containers/ProductInventory/index.tsx
index 8b926a10dd..c502eacd84 100644
--- a/packages/webapp/src/containers/ProductInventory/index.tsx
+++ b/packages/webapp/src/containers/ProductInventory/index.tsx
@@ -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';
@@ -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)
diff --git a/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx b/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx
index 524e5c2b90..82bff3b602 100644
--- a/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx
+++ b/packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx
@@ -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) => {
diff --git a/packages/webapp/src/containers/Task/TaskDetails/index.jsx b/packages/webapp/src/containers/Task/TaskDetails/index.jsx
index 32556505f6..7228a7fd09 100644
--- a/packages/webapp/src/containers/Task/TaskDetails/index.jsx
+++ b/packages/webapp/src/containers/Task/TaskDetails/index.jsx
@@ -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,
);
diff --git a/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx b/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx
index d6ba178c3b..608492ea3e 100644
--- a/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx
+++ b/packages/webapp/src/containers/Task/TaskReadOnly/index.jsx
@@ -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;
diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js
index 191b857309..5d05d2ecef 100644
--- a/packages/webapp/src/containers/productSlice.js
+++ b/packages/webapp/src/containers/productSlice.js
@@ -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, [
@@ -55,6 +56,7 @@ 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 }) => {
@@ -62,22 +64,25 @@ export const productsSelector = createSelector(
},
);
-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);
},
);
diff --git a/packages/webapp/src/store/api/types/index.ts b/packages/webapp/src/store/api/types/index.ts
index fa0046ad34..5f04b1d33c 100644
--- a/packages/webapp/src/store/api/types/index.ts
+++ b/packages/webapp/src/store/api/types/index.ts
@@ -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 {
diff --git a/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts b/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts
index 9be9816cd6..bd71430455 100644
--- a/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts
+++ b/packages/webapp/src/stories/Pages/Task/AddSoilAmendmentProducts/products.ts
@@ -30,6 +30,7 @@ export const products = [
k: 3,
elemental_unit: ElementalUnit.PERCENT,
},
+ removed: false,
},
{
product_id: 2,
@@ -45,6 +46,7 @@ export const products = [
k: 3.3,
elemental_unit: ElementalUnit.RATIO,
},
+ removed: false,
},
{
product_id: 3,
@@ -60,6 +62,7 @@ export const products = [
k: undefined,
elemental_unit: undefined,
},
+ removed: false,
},
{
product_id: 4,
@@ -75,6 +78,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
+ removed: false,
},
{
product_id: 5,
@@ -90,6 +94,7 @@ export const products = [
k: 1,
elemental_unit: ElementalUnit.PERCENT,
},
+ removed: false,
},
{
product_id: 6,
@@ -105,5 +110,6 @@ export const products = [
k: undefined,
elemental_unit: ElementalUnit.RATIO,
},
+ removed: false,
},
];
diff --git a/packages/webapp/src/util/product.ts b/packages/webapp/src/util/product.ts
new file mode 100644
index 0000000000..75869cd980
--- /dev/null
+++ b/packages/webapp/src/util/product.ts
@@ -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 .
+ */
+
+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;
+};