diff --git a/src/redux/sagas/products.ts b/src/redux/sagas/products.ts index 67f04238..d7df4c64 100644 --- a/src/redux/sagas/products.ts +++ b/src/redux/sagas/products.ts @@ -242,21 +242,17 @@ function* stockAdjustments(action: any) { function* getSortationDetailsSaga(action: any) { try { const productResponse: any = yield call(api.getProductByBarcode, action.payload.barcode); - const product = productResponse.data; - - if (!product) { - throw new Error('Product not found.'); - } + const product = productResponse?.data; const location = yield select(userLocation); - if (!location || !location.id) { + if (!location?.id) { return; } const tasksResponse: any = yield call(api.getPutawayTasks, location.id, product.id); const tasks = tasksResponse?.data ?? []; - if (!tasks || tasks.length === 0) { + if (tasks.length === 0) { yield action.callback({ error: true, errorMessage: 'Product found but there is not putaway task for it' @@ -266,17 +262,18 @@ function* getSortationDetailsSaga(action: any) { yield action.callback({ product, tasks }); } catch (error: any) { - if (error.code != 401) { - yield action.callback({ - error: true, - errorMessage: error.message - }); - } + yield action.callback({ + error: true, + // The API returns a 500 status code when the product is not found + productNotFound: error?.code === 500, + errorMessage: error.message + }); } } function* updateProductIdentifierSaga(action: any) { try { + yield put(showScreenLoading('Saving...')); const response = yield call( api.updateProductIdentifier, action.payload.id, @@ -295,6 +292,8 @@ function* updateProductIdentifierSaga(action: any) { errorMessage: error.message }); } + } finally { + yield put(hideScreenLoading()); } } diff --git a/src/screens/ProductDetails/EditBarcodeModal.tsx b/src/screens/ProductDetails/EditBarcodeModal.tsx index d4fb2da2..c32767f4 100644 --- a/src/screens/ProductDetails/EditBarcodeModal.tsx +++ b/src/screens/ProductDetails/EditBarcodeModal.tsx @@ -1,41 +1,41 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Keyboard, Modal, View } from 'react-native'; -import { Button, Headline, Paragraph, Subheading, TextInput } from 'react-native-paper'; +import { Headline, Paragraph, Subheading } from 'react-native-paper'; +import Button from '../../components/Button'; +import { ScannerInput } from '../../components/ScannerInput'; +import { EMPTY_STRING } from '../../constants'; import Theme from '../../utils/Theme'; import styles from './styles'; type EditBarcodeModalProps = { visible: boolean; currentBarcode?: string; - onSave: (barcode: string) => void; + onSave: (barcode: string, clear: () => void) => void; onClose: () => void; }; export default function EditBarcodeModal({ visible, currentBarcode, onSave, onClose }: EditBarcodeModalProps) { - const [barcode, setBarcode] = useState(''); - const inputRef = useRef(null); - - useEffect(() => { - setBarcode(''); - }, [currentBarcode]); + const [barcode, setBarcode] = useState(EMPTY_STRING); useEffect(() => { if (visible) { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 100); - return () => clearTimeout(timer); + setBarcode(EMPTY_STRING); } }, [visible]); + const handleSave = () => { + Keyboard.dismiss(); + onSave(barcode, () => setBarcode(EMPTY_STRING)); + }; + return ( - Edit Barcode + Edit barcode - You'll be able to find and scan the product using either its internal product code or its barcode, depending + You'll be able to find and scan the product using either its internal product code or its barcode depending on what your scanner provides. @@ -45,34 +45,33 @@ export default function EditBarcodeModal({ visible, currentBarcode, onSave, onCl ) : null} - { - Keyboard.dismiss(); - onSave(barcode); - }} + onChange={setBarcode} + onSubmit={handleSave} /> - - + + + title="Save" + style={styles.modalButton} + onPress={handleSave} + /> diff --git a/src/screens/ProductDetails/index.tsx b/src/screens/ProductDetails/index.tsx index 60696aac..5743cd6e 100644 --- a/src/screens/ProductDetails/index.tsx +++ b/src/screens/ProductDetails/index.tsx @@ -1,20 +1,36 @@ import React from 'react'; import { Alert, ScrollView, TouchableOpacity, View } from 'react-native'; import { Caption, Card, Chip, Divider, Button as PaperButton, Paragraph, Subheading, Title } from 'react-native-paper'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { connect } from 'react-redux'; import Button from '../../components/Button'; -import { ProductDetailsSkeleton, CardSkeleton } from '../../components/ContentSkeleton'; +import { CardSkeleton, ProductDetailsSkeleton } from '../../components/ContentSkeleton'; import EmptyView from '../../components/EmptyView'; import PrintModal from '../../components/PrintModal'; import { HYPHEN } from '../../constants'; +import { resetToRoutes } from '../../NavigationService'; import { getProductByIdAction, updateProductIdentifierAction } from '../../redux/actions/products'; import { RootState } from '../../redux/reducers'; +import Theme from '../../utils/Theme'; import EditBarcodeModal from './EditBarcodeModal'; import styles from './styles'; import { DispatchProps, Props, State } from './Types'; import { vmMapper } from './VMMapper'; +function BackToSortationBanner() { + const handlePress = () => { + resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: 'Sortation' }]); + }; + + return ( + + + BACK TO SORTATION + + ); +} + function SectionCard({ title, children }: { title: string; children: React.ReactNode }) { return ( @@ -94,7 +110,7 @@ class ProductDetails extends React.Component { }); }; - handleSaveBarcode = (newBarcode: string) => { + handleSaveBarcode = (newBarcode: string, clear: () => void) => { const { productDetails } = this.state; const id = productDetails.id; if (!id) { @@ -102,8 +118,12 @@ class ProductDetails extends React.Component { } this.props.updateProductIdentifierAction(id, 'upc', newBarcode, (response: any) => { + if (!this.state.editBarcodeVisible) { + return; + } if (response?.error) { Alert.alert('Error', response.errorMessage ?? 'Product identifier update failed.'); + clear(); return; } this.setState({ editBarcodeVisible: false }); @@ -157,10 +177,16 @@ class ProductDetails extends React.Component { render() { const { isLoading, productDetails, visible, editBarcodeVisible } = this.state; const product = this.props.selectedProduct; + const fromSortation = !!this.props.route?.params?.fromSortation; if (isLoading) { return ( + {fromSortation && ( + + + + )} @@ -172,13 +198,20 @@ class ProductDetails extends React.Component { if (!productDetails?.id) { return ( - - + + {fromSortation && ( + + + + )} + + + ); } @@ -205,6 +238,13 @@ class ProductDetails extends React.Component { + {fromSortation && ( + <> + + + + )} + {vm.productCode || HYPHEN} diff --git a/src/screens/ProductDetails/styles.ts b/src/screens/ProductDetails/styles.ts index 0cd4f6ad..b6c45526 100644 --- a/src/screens/ProductDetails/styles.ts +++ b/src/screens/ProductDetails/styles.ts @@ -102,5 +102,26 @@ export default StyleSheet.create({ backgroundColor: 'white', padding: Theme.spacing.large, borderRadius: Theme.roundness * 2 + }, + backToSortationBanner: { + flexDirection: 'row', + alignItems: 'center' + }, + modalButtonRow: { + flexDirection: 'row', + marginTop: Theme.spacing.large + }, + modalButton: { + flex: 1 + }, + modalButtonSpacing: { + marginRight: Theme.spacing.small + }, + backToSortationText: { + marginLeft: Theme.spacing.small, + fontSize: 14, + fontWeight: 'bold', + color: Theme.colors.primary, + letterSpacing: 0.5 } }); diff --git a/src/screens/Products/index.tsx b/src/screens/Products/index.tsx index 02040368..bcb1a6f2 100644 --- a/src/screens/Products/index.tsx +++ b/src/screens/Products/index.tsx @@ -74,6 +74,13 @@ class Products extends React.Component { this.props.getProductsAction(actionCallback, true); }; + goToProductDetails = (product: any) => { + this.props.navigation.navigate('ProductDetails', { + product, + fromSortation: this.props.route?.params?.fromSortation + }); + }; + onSearchByProductNamePress = () => { this.setState({ searchBoxVisible: true @@ -142,7 +149,7 @@ class Products extends React.Component { error: emptyStateMessage('products', query, 'No products available') }); } else if (data.length === 1) { - this.props.navigation.navigate('ProductDetails', { product: data[0] }); + this.goToProductDetails(data[0]); } else { this.setState({ searchByName: { query, results: data }, @@ -190,7 +197,7 @@ class Products extends React.Component { error: emptyStateMessage('products', query, 'No products available') }); } else if (data.length === 1) { - this.props.navigation.navigate('ProductDetails', { product: data[0] }); + this.goToProductDetails(data[0]); } else { this.setState({ searchByProductCode: { query, results: data }, @@ -297,7 +304,7 @@ class Products extends React.Component { error: emptyStateMessage('products', query, 'No products available') }); } else if (productList.length === 1) { - this.props.navigation.navigate('ProductDetails', { product: productList[0] }); + this.goToProductDetails(productList[0]); } else { this.setState({ searchByProductCode: { query, results: data }, @@ -344,12 +351,7 @@ class Products extends React.Component { ) : ( <> - { - this.props.navigation.navigate('ProductDetails', { product }); - }} - /> + {vm?.list?.length === 0 && } )} diff --git a/src/screens/Products/types.ts b/src/screens/Products/types.ts index 3a70d22e..db1492c2 100644 --- a/src/screens/Products/types.ts +++ b/src/screens/Products/types.ts @@ -4,6 +4,7 @@ import { ProductCategory } from '../../data/product/category/ProductCategory'; export interface OwnProps { exit: () => void; navigation: any; + route: any; } export interface StateProps { diff --git a/src/screens/Sortation/BarcodeUnrecognizedDialog.tsx b/src/screens/Sortation/BarcodeUnrecognizedDialog.tsx new file mode 100644 index 00000000..8f937c8e --- /dev/null +++ b/src/screens/Sortation/BarcodeUnrecognizedDialog.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Modal, TouchableOpacity, View } from 'react-native'; +import { Divider, Text } from 'react-native-paper'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; + +import Button from '../../components/Button'; +import Theme from '../../utils/Theme'; +import styles from './styles'; + +type BarcodeUnrecognizedDialogProps = { + visible: boolean; + barcode: string; + onFindProduct: () => void; + onClose: () => void; +}; + +export function BarcodeUnrecognizedDialog({ + visible, + barcode, + onFindProduct, + onClose +}: BarcodeUnrecognizedDialogProps) { + return ( + + + + Barcode unrecognized + {`Product ${barcode} not found`} + + + + + Find a product for this barcode + + + + + +