Skip to content

Commit a19e002

Browse files
authored
Merge pull request #3899 from LiteFarmOrg/LF-4980/Show_valid_soil_amendment_products
LF-4980: Show valid soil amendment products
2 parents afffb39 + 845a2bd commit a19e002

File tree

13 files changed

+94
-31
lines changed

13 files changed

+94
-31
lines changed

packages/webapp/public/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"QUANTITY": "Quantity",
8080
"REMOVE": "Remove",
8181
"REMOVE_ITEM": "Remove item",
82+
"REMOVED": "Removed",
8283
"REQUIRED": "Required",
8384
"RETIRE": "Retire",
8485
"REVISION_INFO": "Revised on <strong>{{date}}</strong> <i>by</i> <strong>{{user}}</strong>",

packages/webapp/src/components/ProductInventory/ProductForm/PureSoilAmendmentProductForm.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
1414
*/
1515

16-
import { useEffect } from 'react';
16+
import { useEffect, useMemo } from 'react';
1717
import { useTranslation } from 'react-i18next';
1818
import { useFormContext } from 'react-hook-form';
1919
import Input, { getInputErrors } from '../../Form/Input';
2020
import ProductDetails, { type ProductDetailsProps } from '../../Form/ProductDetails';
2121
import { hookFormMaxCharsValidation } from '../../Form/hookformValidationUtils';
2222
import { getSoilAmendmentFormValues } from '../../Form/ProductDetails/utils';
23+
import { isLibraryProduct } from '../../../util/product';
2324
import { productDefaultValuesByType } from '../../../containers/ProductInventory/ProductForm/constants';
2425
import { TASK_TYPES } from '../../../containers/Task/constants';
2526
import { PRODUCT_FIELD_NAMES } from '../../Task/AddSoilAmendmentProducts/types';
@@ -79,7 +80,11 @@ const PureSoilAmendmentProductForm = ({
7980
}
8081
}, [mode]);
8182

82-
const productNames: SoilAmendmentProduct['name'][] = products.map(({ name }) => name);
83+
const customProductNames = useMemo(() => {
84+
return products
85+
.filter((product) => !product.removed && !isLibraryProduct(product))
86+
.map(({ name }) => name);
87+
}, [products]);
8388

8489
return (
8590
<div className={styles.soilAmendmentProductForm}>
@@ -95,7 +100,7 @@ const PureSoilAmendmentProductForm = ({
95100
// Allow duplicate check to pass if keeping the original name during edit
96101
if (
97102
!(mode === FormMode.EDIT && value === product?.name) &&
98-
productNames.includes(value)
103+
customProductNames.includes(value)
99104
) {
100105
return t('ADD_TASK.DUPLICATE_NAME');
101106
}

packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import { hookFormMaxCharsValidation } from '../../../Form/hookformValidationUtil
3333
import { soilAmendmentProductDetailsDefaultValues } from '../../../../containers/ProductInventory/ProductForm/constants';
3434
import { getSoilAmendmentFormValues } from '../../../Form/ProductDetails/utils';
3535

36+
const findProduct = (
37+
products?: SoilAmendmentProduct[],
38+
productId?: SoilAmendmentProduct['product_id'],
39+
) => {
40+
return products?.find(({ product_id }) => product_id === productId);
41+
};
42+
3643
export type ProductCardProps = ProductDetailsProps & {
3744
namePrefix: string;
3845
system: 'metric' | 'imperial';
@@ -128,7 +135,8 @@ const SoilAmendmentProductCard = ({
128135

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

134142
useEffect(() => {
@@ -137,6 +145,11 @@ const SoilAmendmentProductCard = ({
137145
}
138146
}, [otherPurposeId]);
139147

148+
useEffect(() => {
149+
const selectedProduct = findProduct(products, productId);
150+
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
151+
}, []);
152+
140153
return (
141154
<div className={styles.productCard}>
142155
{!isReadOnly && onRemove && (
@@ -154,7 +167,7 @@ const SoilAmendmentProductCard = ({
154167
options={productOptions}
155168
onChange={(e) => {
156169
onChange(e?.value);
157-
const selectedProduct = products.find(({ product_id }) => product_id === e?.value);
170+
const selectedProduct = findProduct(products, e?.value);
158171
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
159172
}}
160173
value={productOptions.find(({ value: id }) => id === value)}

packages/webapp/src/components/Task/SoilAmendmentTask/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,22 @@ import { furrow_hole_depth } from '../../../util/convert-units/unit';
3333
import styles from './styles.module.scss';
3434
import { locationsSelector } from '../../../containers/locationSlice';
3535

36+
// Return products in inventory plus removed ones already used in the task.
37+
const getAvailableProducts = (
38+
usedProductsInTask: SoilAmendmentProduct[] | undefined,
39+
products: SoilAmendmentProduct[],
40+
) => {
41+
const usedProductIds = new Set(usedProductsInTask?.map(({ product_id }) => product_id));
42+
return products.filter((product) => !product.removed || usedProductIds.has(product.product_id));
43+
};
44+
3645
type PureSoilAmendmentTaskProps = UseFormReturn &
3746
Pick<ProductCardProps, 'farm' | 'system' | 'products'> & {
3847
disabled: boolean;
39-
task?: { locations: { location_id: number }[] };
48+
task?: {
49+
locations: { location_id: number }[];
50+
soil_amendment_task_products: SoilAmendmentProduct[];
51+
};
4052
locations: { location_id: number }[];
4153
};
4254

@@ -158,7 +170,7 @@ const PureSoilAmendmentTask = ({
158170
<AddSoilAmendmentProducts
159171
farm={farm}
160172
system={system}
161-
products={products}
173+
products={getAvailableProducts(task?.soil_amendment_task_products, products)}
162174
purposes={purposes}
163175
fertiliserTypes={fertiliserTypes}
164176
isReadOnly={disabled}

packages/webapp/src/containers/ProductInventory/ProductForm/SoilAmendmentProductForm.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import { useGetSoilAmendmentFertiliserTypesQuery } from '../../../store/api/apiS
1818
import PureSoilAmendmentProductForm from '../../../components/ProductInventory/ProductForm/PureSoilAmendmentProductForm';
1919
import { userFarmSelector } from '../../userFarmSlice';
2020
import { certifierSurveySelector } from '../../OrganicCertifierSurvey/slice';
21-
import { productsSelector } from '../../productSlice';
21+
import { productsForTaskTypeSelector } from '../../productSlice';
2222
import { TASK_TYPES } from '../../Task/constants';
2323
import { FormMode } from '..';
2424
import { FormContentProps } from '.';
2525

26+
const taskType = { task_translation_key: TASK_TYPES.SOIL_AMENDMENT.toUpperCase() };
27+
2628
export default function SoilAmendmentProductForm({ mode, productId }: FormContentProps) {
2729
const { t } = useTranslation();
2830

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

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

4847
const isReadOnly = mode === FormMode.READ_ONLY;
4948

@@ -53,7 +52,7 @@ export default function SoilAmendmentProductForm({ mode, productId }: FormConten
5352
isReadOnly={isReadOnly}
5453
farm={{ farm_id, interested, country_id }}
5554
fertiliserTypeOptions={fertiliserTypeOptions}
56-
products={soilAmendmentCustomProducts}
55+
products={soilAmendmentProducts}
5756
productId={productId}
5857
/>
5958
);

packages/webapp/src/containers/ProductInventory/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ReactComponent as BookIcon } from '../../assets/images/book-closed.svg'
2424
import useSearchFilter from '../../containers/hooks/useSearchFilter';
2525
import PureProductInventory from '../../components/ProductInventory';
2626
import { getProducts } from '../Task/saga';
27-
import { productsSelector } from '../productSlice';
27+
import { productInventorySelector } from '../productSlice';
2828
import { Product, SoilAmendmentProduct } from '../../store/api/types';
2929
import { TASK_TYPES } from '../Task/constants';
3030
import { SearchProps } from '../../components/Animals/Inventory';
@@ -65,7 +65,7 @@ export default function ProductInventory() {
6565
dispatch(getProducts());
6666
}, []);
6767

68-
const productInventory = useSelector(productsSelector);
68+
const productInventory = useSelector(productInventorySelector);
6969

7070
const inventory = productInventory
7171
.filter((product) => product.type === TASK_TYPES.SOIL_AMENDMENT)

packages/webapp/src/containers/Task/TaskComplete/StepOne.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function TaskCompleteStepOne() {
2222
const { task_id } = useParams();
2323
const task = useSelector(taskWithProductSelector(task_id));
2424
const selectedTaskType = task?.taskType;
25-
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
25+
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
2626
const persistedPaths = [`/tasks/${task_id}/complete`];
2727

2828
const onContinue = (data) => {

packages/webapp/src/containers/Task/TaskDetails/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function TaskDetails() {
2828
const { interested, farm_id } = useSelector(certifierSurveySelector, shallowEqual);
2929
const persistedFormData = useSelector(hookFormPersistSelector);
3030
const selectedTaskType = useSelector(taskTypeSelector(persistedFormData.task_type_id));
31-
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
31+
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
3232
const managementPlanIds = persistedFormData.managementPlans?.map(
3333
({ management_plan_id }) => management_plan_id,
3434
);

packages/webapp/src/containers/Task/TaskReadOnly/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function TaskReadOnly() {
5252
const system = useSelector(measurementSelector);
5353
const task = useReadonlyTask(task_id);
5454
const selectedTaskType = task?.taskType;
55-
const products = useSelector(productsForTaskTypeSelector(selectedTaskType));
55+
const products = useSelector((state) => productsForTaskTypeSelector(state, selectedTaskType));
5656
const isIrrigationTaskWithExternalPrescription =
5757
isTaskType(selectedTaskType, 'IRRIGATION_TASK') &&
5858
task?.irrigation_task?.irrigation_prescription_external_id != null;

packages/webapp/src/containers/productSlice.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
22
import { loginSelector, onLoadingFail, onLoadingStart } from './userFarmSlice';
33
import { pick } from '../util/pick';
44
import { createSelector } from 'reselect';
5+
import { isLibraryProduct } from '../util/product';
56

67
export const getProduct = (obj) => {
78
return pick(obj, [
@@ -55,29 +56,33 @@ const productSelectors = productAdapter.getSelectors(
5556
(state) => state.entitiesReducer[productSlice.name],
5657
);
5758

59+
// Select farm products including removed ones
5860
export const productsSelector = createSelector(
5961
[productSelectors.selectAll, loginSelector],
6062
(products, { farm_id }) => {
6163
return products.filter((product) => product.farm_id === farm_id);
6264
},
6365
);
6466

65-
export const productsForTaskTypeSelector = (taskType) => {
66-
return createSelector([productSelectors.selectAll, loginSelector], (products, { farm_id }) => {
67+
// Select farm products for a given type including removed ones
68+
export const productsForTaskTypeSelector = createSelector(
69+
[productsSelector, (_state, taskType) => taskType],
70+
(products, taskType) => {
6771
if (taskType === undefined) {
6872
return undefined;
6973
}
70-
return products.filter(
71-
(product) =>
72-
product.farm_id === farm_id && product.type === taskType.task_translation_key.toLowerCase(),
73-
);
74-
});
75-
};
74+
return products.filter(({ type }) => type === taskType.task_translation_key.toLowerCase());
75+
},
76+
);
77+
78+
export const productInventorySelector = createSelector([productsSelector], (products) => {
79+
return products.filter((product) => !product.removed);
80+
});
7681

7782
export const hasAvailableProductsSelector = createSelector(
78-
[productsSelector, (_state, type) => type],
79-
(products, type) => {
80-
return products.some((product) => !product.removed && (!type || product.type === type));
83+
[productInventorySelector, (_state, type) => type],
84+
(productInventory, type) => {
85+
return productInventory.some((product) => !type || product.type === type);
8186
},
8287
);
8388

0 commit comments

Comments
 (0)