diff --git a/packages/webapp/public/locales/en/translation.json b/packages/webapp/public/locales/en/translation.json index 7f0669ec1d..fe73801958 100644 --- a/packages/webapp/public/locales/en/translation.json +++ b/packages/webapp/public/locales/en/translation.json @@ -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": { @@ -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", diff --git a/packages/webapp/src/components/Drawer/index.tsx b/packages/webapp/src/components/Drawer/index.tsx index ee5a354f3c..42f90abdbb 100644 --- a/packages/webapp/src/components/Drawer/index.tsx +++ b/packages/webapp/src/components/Drawer/index.tsx @@ -18,6 +18,7 @@ import ModalComponent from '../Modals/ModalComponent/v2'; import styles from './style.module.scss'; import { IconButton, useMediaQuery, useTheme } from '@mui/material'; import { Close } from '@mui/icons-material'; +import TextButton from '../Form/Button/TextButton'; export enum DesktopDrawerVariants { DRAWER = 'drawer', @@ -41,6 +42,7 @@ type CommonDrawerProps = { drawerContainer?: string; // applied to all drawers desktopSideDrawerContainer?: string; }; + closeButtonLabel?: string; }; type DrawerProps = CommonDrawerProps & @@ -81,12 +83,15 @@ const Drawer = ({ desktopSideDrawerDirection = 'right', isCompactSideMenu, addBackdrop = true, + closeButtonLabel, }: DrawerProps) => { const theme = useTheme(); const isDesktop = useMediaQuery(theme.breakpoints.up('sm')); const isDesktopSideDrawer = isDesktop && desktopVariant === DesktopDrawerVariants.SIDE_DRAWER; + const CloseButton = closeButtonLabel ? TextButton : IconButton; + return isDesktop && desktopVariant === DesktopDrawerVariants.MODAL && isOpen ? (
{title}
- + + {closeButtonLabel && {closeButtonLabel}} - +
{children} {buttonGroup} diff --git a/packages/webapp/src/components/Drawer/style.module.scss b/packages/webapp/src/components/Drawer/style.module.scss index ee549366f2..c47c7d6c1c 100644 --- a/packages/webapp/src/components/Drawer/style.module.scss +++ b/packages/webapp/src/components/Drawer/style.module.scss @@ -129,13 +129,26 @@ padding: 0 24px; } -.close { - width: 36px; - height: 36px; - +.closeButton { svg { width: 28px; height: 28px; color: var(--grey600); } + + &.iconButton { + width: 36px; + height: 36px; + } + + &.textButton { + display: flex; + align-items: center; + gap: 4px; + + span { + color: var(--Colors-Accent---singles-Blue-dark); + font-size: 16px; + } + } } diff --git a/packages/webapp/src/components/Modals/NoSoilAmendmentProductsModal/index.tsx b/packages/webapp/src/components/Modals/NoSoilAmendmentProductsModal/index.tsx new file mode 100644 index 0000000000..5cd3714f89 --- /dev/null +++ b/packages/webapp/src/components/Modals/NoSoilAmendmentProductsModal/index.tsx @@ -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 <.> + */ + +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 ( + + + + + } + > + ); +} diff --git a/packages/webapp/src/components/ProductInventory/index.tsx b/packages/webapp/src/components/ProductInventory/index.tsx index 4a5c7d2780..d40020d714 100644 --- a/packages/webapp/src/components/ProductInventory/index.tsx +++ b/packages/webapp/src/components/ProductInventory/index.tsx @@ -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; + export type PureProductInventory = { filteredInventory: TableProduct[]; zIndexBase: number; @@ -118,7 +120,7 @@ const PureProductInventory = ({ columns={productColumns} data={filteredInventory} shouldFixTableLayout={isDesktop} - minRows={totalInventoryCount} + minRows={TABLE_MIN_ROWS} dense={true} showHeader={isDesktop} selectedIds={selectedIds} diff --git a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx index 010fa6179c..532747689a 100644 --- a/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx +++ b/packages/webapp/src/components/Task/AddSoilAmendmentProducts/ProductCard/index.tsx @@ -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} diff --git a/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx b/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx index 92401bedbf..6f2040052a 100644 --- a/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx +++ b/packages/webapp/src/components/Task/PureTaskTypeSelection/PureTaskTypeSelection.jsx @@ -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: , @@ -72,6 +74,7 @@ export const PureTaskTypeSelection = ({ hasAnimalMovementLocations, hasAnimals, hasSoilSampleLocations, + hasSoilAmendmentProducts, }) => { const { t } = useTranslation(); const { watch, getValues, register, setValue } = useForm({ @@ -92,39 +95,32 @@ export const PureTaskTypeSelection = ({ onContinue(); }; - const [showPlantTaskModal, setShowPlantTaskModal] = useState(); + const [errorModal, setErrorModal] = useState(''); + 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); }; @@ -225,32 +221,35 @@ export const PureTaskTypeSelection = ({
)} - {showPlantTaskModal && shouldShowPlantTaskSpotLight && ( + {errorModal === 'PLANT_TASK' && shouldShowPlantTaskSpotLight && ( setShowPlantTaskModal(false)} + dismissModal={() => setErrorModal('')} updatePlantTaskSpotlight={updatePlantTaskSpotlight} /> )} - {showNoManagementPlanModal && ( + {['TRANSPLANT_TASK', 'HARVEST_TASK'].includes(errorModal) && ( setShowNoManagementPlanModal(false)} + dismissModal={() => setErrorModal('')} goToCatalogue={goToCatalogue} /> )} - {showNoAnimalLocationsModal && ( - setShowNoAnimalLocationsModal(false)} - goToMap={goToMap} - /> + {errorModal === 'MOVEMENT_TASK' && ( + setErrorModal('')} goToMap={goToMap} /> )} - {showNoSoilSampleLocationsModal && ( + {errorModal === 'SOIL_SAMPLE_TASK' && ( setShowNoSoilSampleLocationsModal(false)} + dismissModal={() => setErrorModal('')} goToMap={goToMap} isAdmin={isAdmin} /> )} + {errorModal === 'SOIL_AMENDMENT_TASK' && ( + setErrorModal('')} + goToInventory={goToInventory} + /> + )} ); }; diff --git a/packages/webapp/src/containers/ProductInventory/ProductForm/index.tsx b/packages/webapp/src/containers/ProductInventory/ProductForm/index.tsx index a9c4be2576..5b97a1eacf 100644 --- a/packages/webapp/src/containers/ProductInventory/ProductForm/index.tsx +++ b/packages/webapp/src/containers/ProductInventory/ProductForm/index.tsx @@ -55,9 +55,11 @@ const renderDrawerTitle = ( if (mode === FormMode.READ_ONLY) { return (
- onActionButtonClick(FormMode.EDIT)}> - - + {isAdmin && ( + onActionButtonClick(FormMode.EDIT)}> + + + )} onActionButtonClick(FormMode.DUPLICATE)}> @@ -133,6 +135,7 @@ export default function ProductForm({ desktopSideDrawerContainer: styles.sideDrawerContainer, drawerHeader: styles.drawerHeader, }} + closeButtonLabel={t('common:CANCEL')} >
{FormContent && ( diff --git a/packages/webapp/src/containers/Task/TaskTypeSelection/index.jsx b/packages/webapp/src/containers/Task/TaskTypeSelection/index.jsx index 7d46effd07..adb8c68be2 100644 --- a/packages/webapp/src/containers/Task/TaskTypeSelection/index.jsx +++ b/packages/webapp/src/containers/Task/TaskTypeSelection/index.jsx @@ -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(); @@ -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 ( <> @@ -71,6 +76,7 @@ function TaskTypeSelection() { hasAnimalMovementLocations={hasAnimalMovementLocations} hasAnimals={animalsExistOnFarm} hasSoilSampleLocations={hasSoilSampleLocations} + hasSoilAmendmentProducts={hasSoilAmendmentProducts} /> diff --git a/packages/webapp/src/containers/productSlice.js b/packages/webapp/src/containers/productSlice.js index 99a906b7ee..191b857309 100644 --- a/packages/webapp/src/containers/productSlice.js +++ b/packages/webapp/src/containers/productSlice.js @@ -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) =>