Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
3 changes: 3 additions & 0 deletions packages/webapp/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"TYPE_OF_FIELD_WORK": "Type of field work"
},
"GO_TO_CATALOGUE": "Go to Crop Catalogue",
"GO_TO_INVENTORY": "Go to inventory",
"HARVEST_EVERYTHING": "Harvest everything that is ready",
"HARVESTING_INFO": "Each plan will generate an individual harvest task",
"HOURLY_WAGE": {
Expand Down Expand Up @@ -209,10 +210,12 @@
},
"NEED_ANIMAL_LOCATION_MOVEMENT": "You'll need a location or area where animals can be moved. Go to the map to create an animal location.",
"NEED_MANAGEMENT_PLAN": "You'll need an active or planned crop plan before you can schedule a harvest task or transplant task. Go to the crop catalogue to create a plan now.",
"NEED_SOIL_AMENDMENT_PRODUCTS": "Looks like you don’t have a soil amendment product yet. Head to the inventory to add one before creating your soil amendment task.",
"NEED_SOIL_SAMPLE_LOCATION": "You'll need a specific location designated to sample the soil. Go to the map to create a soil sampling location.",
"NEED_SOIL_SAMPLE_LOCATION_WORKER": "You’ll need management to create at least one soil sample location before you can create a soil sample task.",
"NO_ANIMAL_LOCATION": "No eligible animal locations",
"NO_MANAGEMENT_PLAN": "No eligible crop plans",
"NO_SOIL_AMENDMENT_PRODUCTS": "Add a soil amendment product first",
"NO_SOIL_SAMPLE_LOCATION": "No eligible soil sample locations",
"NOTES_LABEL": "Anything specific to add related to this task?",
"NOTES_PLACEHOLDER": "Add any instructions or specifics for the assignee",
Expand Down
11 changes: 8 additions & 3 deletions packages/webapp/src/components/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type CommonDrawerProps = {
drawerContainer?: string; // applied to all drawers
desktopSideDrawerContainer?: string;
};
closeButtonLabel?: string;
};

type DrawerProps = CommonDrawerProps &
Expand Down Expand Up @@ -81,6 +82,7 @@ const Drawer = ({
desktopSideDrawerDirection = 'right',
isCompactSideMenu,
addBackdrop = true,
closeButtonLabel,
}: DrawerProps) => {
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
Expand Down Expand Up @@ -126,9 +128,12 @@ const Drawer = ({
>
<div className={clsx(styles.header, classes.drawerHeader)}>
<div className={styles.title}>{title}</div>
<IconButton className={styles.close} onClick={onClose}>
<Close />
</IconButton>
<div className={styles.closeButtonWrapper}>
{closeButtonLabel && <span> {closeButtonLabel}</span>}
<IconButton className={styles.close} onClick={onClose}>
<Close />
</IconButton>
</div>
</div>
<div className={clsx(styles.drawerContent, classes.drawerContent)}>
{children} {buttonGroup}
Expand Down
10 changes: 10 additions & 0 deletions packages/webapp/src/components/Drawer/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@
padding: 0 24px;
}

.closeButtonWrapper {
display: flex;
align-items: center;

span {
color: var(--Colors-Accent---singles-Blue-dark);
font-size: 16px;
}
}

.close {
width: 36px;
height: 36px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 { useTranslation } from 'react-i18next';
import ModalComponent from '../ModalComponent/v2';
import Button from '../../Form/Button';

interface NoSoilAmendmentProductsModalProps {
dismissModal: () => void;
goToInventory: () => void;
}

export function NoSoilAmendmentProductsModal({
dismissModal,
goToInventory,
}: NoSoilAmendmentProductsModalProps) {
const { t } = useTranslation();

return (
<ModalComponent
title={t('ADD_TASK.NO_SOIL_AMENDMENT_PRODUCTS')}
contents={[t('ADD_TASK.NEED_SOIL_AMENDMENT_PRODUCTS')]}
dismissModal={dismissModal}
buttonGroup={
<>
<Button
data-cy="tasks-noSoilAmendmentProductsCancel"
onClick={dismissModal}
color={'secondary'}
type={'button'}
sm
>
{t('common:GO_BACK')}
</Button>
<Button
data-cy="tasks-noSoilAmendmentProductsContinue"
onClick={goToInventory}
type={'submit'}
sm
>
{t('ADD_TASK.GO_TO_INVENTORY')}
</Button>
</>
}
></ModalComponent>
);
}
4 changes: 3 additions & 1 deletion packages/webapp/src/components/ProductInventory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { TableProduct } from '../../containers/ProductInventory';
import { Product } from '../../store/api/types';
import { TASK_TYPES } from '../../containers/Task/constants';

const TABLE_MIN_ROWS = 20;
Copy link
Collaborator

Choose a reason for hiding this comment

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

In Animal inventory we set it to totalInventoryCount. Is there a reason we want to limit to 20?

Copy link
Collaborator Author

@SayakaOno SayakaOno Oct 22, 2025

Choose a reason for hiding this comment

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

I chose 20 arbitrarily, but I thought it was reasonable.
When there are no products, totalInventoryCount is 0. After creating a new product, the "Load more" button appears because the number of rows to display is determined at render time based on totalInventoryCount, and it never updates dynamically.
In the Animal Inventory, totalInventoryCount doesn’t increase, so the button won’t appear unexpectedly. But in the Product Inventory, it can change, so we need a fixed number of rows. Should we set it higher?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we can't update the inventory count dynamically to re-render. Then 20 is fine by me!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah sorry, totalInventoryCount changes, but minRows determines the rowsPerPage in <Table /> at render time, and rowsPerPage can't be updated externally after that!


export type PureProductInventory = {
filteredInventory: TableProduct[];
zIndexBase: number;
Expand Down Expand Up @@ -118,7 +120,7 @@ const PureProductInventory = ({
columns={productColumns}
data={filteredInventory}
shouldFixTableLayout={isDesktop}
minRows={totalInventoryCount}
minRows={TABLE_MIN_ROWS}
dense={true}
showHeader={isDesktop}
selectedIds={selectedIds}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ const SoilAmendmentProductCard = ({
const selectedProduct = products.find(({ product_id }) => product_id === e?.value);
nestedFormMethods.reset(getSoilAmendmentFormValues(selectedProduct));
}}
placeholder={t('ADD_PRODUCT.PRESS_ENTER')}
value={productOptions.find(({ value: id }) => id === value)}
hasLeaf={true}
isDisabled={isReadOnly}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { ANIMAL_TASKS } from '../../../containers/Task/constants';
import { CantFindCustomType } from '../../Finances/PureFinanceTypeSelection/CantFindCustomType';
import { NoAnimalLocationsModal } from '../../Modals/NoAnimalLocationsModal';
import { NoSoilSampleLocationsModal } from '../../Modals/NoSoilSampleLocationsModal';
import { NoSoilAmendmentProductsModal } from '../../Modals/NoSoilAmendmentProductsModal';
import { PRODUCT_INVENTORY_URL } from '../../../util/siteMapConstants';

const icons = {
SOIL_AMENDMENT_TASK: <SoilAmendment />,
Expand Down Expand Up @@ -72,6 +74,7 @@ export const PureTaskTypeSelection = ({
hasAnimalMovementLocations,
hasAnimals,
hasSoilSampleLocations,
hasSoilAmendmentProducts,
}) => {
const { t } = useTranslation();
const { watch, getValues, register, setValue } = useForm({
Expand All @@ -92,39 +95,32 @@ export const PureTaskTypeSelection = ({
onContinue();
};

const [showPlantTaskModal, setShowPlantTaskModal] = useState();
const [errorModal, setErrorModal] = useState('');
Copy link
Collaborator

Choose a reason for hiding this comment

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

❤️


const goToCatalogue = () => history.push('/crop_catalogue');
const goToMap = () => history.push('/map');
const goToInventory = () => history.push(PRODUCT_INVENTORY_URL);
const onPlantTaskTypeClick = () => {
if (shouldShowPlantTaskSpotLight) {
setShowPlantTaskModal(true);
setErrorModal('PLANT_TASK');
} else {
goToCatalogue();
}
};
const [showNoManagementPlanModal, setShowNoManagementPlanModal] = useState();
const onHarvestTransplantTaskClick = (task_type_id) => {
hasCurrentManagementPlans ? onSelectTask(task_type_id) : setShowNoManagementPlanModal(true);
};

const [showNoAnimalLocationsModal, setShowNoAnimalLocationsModal] = useState();
const onMovementTaskClick = (task_type_id) => {
hasAnimalMovementLocations ? onSelectTask(task_type_id) : setShowNoAnimalLocationsModal(true);
};

const [showNoSoilSampleLocationsModal, setShowNoSoilSampleLocationsModal] = useState();
const onSoilSampleTaskClick = (task_type_id) => {
hasSoilSampleLocations ? onSelectTask(task_type_id) : setShowNoSoilSampleLocationsModal(true);
};

const onTileClick = (taskType) => {
if (isTaskType(taskType, 'PLANT_TASK')) return onPlantTaskTypeClick(taskType.task_type_id);
if (isTaskType(taskType, 'TRANSPLANT_TASK') || isTaskType(taskType, 'HARVEST_TASK')) {
return onHarvestTransplantTaskClick(taskType.task_type_id);
if (isTaskType(taskType, 'PLANT_TASK')) {
return onPlantTaskTypeClick(taskType.task_type_id);
}
if (
((isTaskType(taskType, 'TRANSPLANT_TASK') || isTaskType(taskType, 'HARVEST_TASK')) &&
!hasCurrentManagementPlans) ||
(isTaskType(taskType, 'MOVEMENT_TASK') && !hasAnimalMovementLocations) ||
(isTaskType(taskType, 'SOIL_SAMPLE_TASK') && !hasSoilSampleLocations) ||
(isTaskType(taskType, 'SOIL_AMENDMENT_TASK') && !hasSoilAmendmentProducts)
) {
return setErrorModal(taskType.task_translation_key);
}
if (isTaskType(taskType, 'MOVEMENT_TASK')) return onMovementTaskClick(taskType.task_type_id);
if (isTaskType(taskType, 'SOIL_SAMPLE_TASK'))
return onSoilSampleTaskClick(taskType.task_type_id);
return onSelectTask(taskType.task_type_id);
};

Expand Down Expand Up @@ -225,32 +221,35 @@ export const PureTaskTypeSelection = ({
</div>
)}
</Form>
{showPlantTaskModal && shouldShowPlantTaskSpotLight && (
{errorModal === 'PLANT_TASK' && shouldShowPlantTaskSpotLight && (
<PlantingTaskModal
goToCatalogue={goToCatalogue}
dismissModal={() => setShowPlantTaskModal(false)}
dismissModal={() => setErrorModal('')}
updatePlantTaskSpotlight={updatePlantTaskSpotlight}
/>
)}
{showNoManagementPlanModal && (
{['TRANSPLANT_TASK', 'HARVEST_TASK'].includes(errorModal) && (
<NoCropManagementPlanModal
dismissModal={() => setShowNoManagementPlanModal(false)}
dismissModal={() => setErrorModal('')}
goToCatalogue={goToCatalogue}
/>
)}
{showNoAnimalLocationsModal && (
<NoAnimalLocationsModal
dismissModal={() => setShowNoAnimalLocationsModal(false)}
goToMap={goToMap}
/>
{errorModal === 'MOVEMENT_TASK' && (
<NoAnimalLocationsModal dismissModal={() => setErrorModal('')} goToMap={goToMap} />
)}
{showNoSoilSampleLocationsModal && (
{errorModal === 'SOIL_SAMPLE_TASK' && (
<NoSoilSampleLocationsModal
dismissModal={() => setShowNoSoilSampleLocationsModal(false)}
dismissModal={() => setErrorModal('')}
goToMap={goToMap}
isAdmin={isAdmin}
/>
)}
{errorModal === 'SOIL_AMENDMENT_TASK' && (
<NoSoilAmendmentProductsModal
dismissModal={() => setErrorModal('')}
goToInventory={goToInventory}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ const renderDrawerTitle = (
if (mode === FormMode.READ_ONLY) {
return (
<div className={clsx(styles.buttons, styles.titleWrapper)}>
<TextButton onClick={() => onActionButtonClick(FormMode.EDIT)}>
<EditIcon />
</TextButton>
{isAdmin && (
<TextButton onClick={() => onActionButtonClick(FormMode.EDIT)}>
<EditIcon />
</TextButton>
)}
<TextButton onClick={() => onActionButtonClick(FormMode.DUPLICATE)}>
<CopyIcon />
</TextButton>
Expand Down Expand Up @@ -133,6 +135,7 @@ export default function ProductForm({
desktopSideDrawerContainer: styles.sideDrawerContainer,
drawerHeader: styles.drawerHeader,
}}
closeButtonLabel={t('common:CANCEL')}
>
<div className={styles.formWrapper}>
{FormContent && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { currentAndPlannedManagementPlansSelector } from '../../managementPlanSl
import useAnimalsExist from '../../Animals/Inventory/useAnimalsExist';
import { animalLocationsSelector } from '../../locationSlice';
import { soilSampleLocationsSelector } from '../../soilSampleLocationSlice';
import { hasAvailableProductsSelector } from '../../productSlice';
import { TASK_TYPES } from '../constants';

function TaskTypeSelection() {
const location = useLocation();
Expand Down Expand Up @@ -50,6 +52,9 @@ function TaskTypeSelection() {

const hasAnimalMovementLocations = useSelector(animalLocationsSelector)?.length > 0;
const hasSoilSampleLocations = useSelector(soilSampleLocationsSelector)?.length > 0;
const hasSoilAmendmentProducts = useSelector((state) =>
hasAvailableProductsSelector(state, TASK_TYPES.SOIL_AMENDMENT),
);

return (
<>
Expand All @@ -71,6 +76,7 @@ function TaskTypeSelection() {
hasAnimalMovementLocations={hasAnimalMovementLocations}
hasAnimals={animalsExistOnFarm}
hasSoilSampleLocations={hasSoilSampleLocations}
hasSoilAmendmentProducts={hasSoilAmendmentProducts}
/>
</HookFormPersistProvider>
</>
Expand Down
7 changes: 7 additions & 0 deletions packages/webapp/src/containers/productSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export const productsForTaskTypeSelector = (taskType) => {
});
};

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

export const productEntitiesSelector = productSelectors.selectEntities;

export const productSelector = (product_id) => (state) =>
Expand Down