Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions src/apis/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ export function searchBarcode(id: string) {
export function getProductByBarcode(barcode: string) {
return apiClient.get(`/barcodes?id=${encodeURIComponent(barcode)}`);
}

export function updateProductIdentifier(id: string, type: string, value: string) {
return apiClient.put(`/mobile/products/${id}/identifiers`, {
identifier: { type, value }
});
}
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ export const appConfig = {
APP_HEADER_HEIGHT: 56,
LOCALE: 'en-US'
};

export const NOT_CHANGED_STATUS = 'no_change';
1 change: 1 addition & 0 deletions src/data/product/Product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface Product {
unitOfMeasure: string;
image?: any;
availableItems: any;
upc?: string;
}

export default Product;
17 changes: 17 additions & 0 deletions src/redux/actions/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const SEARCH_BARCODE_SUCCESS = 'SEARCH_BARCODE_SUCCESS';

export const GET_SORTATION_DETAILS_BY_BARCODE = 'GET_SORTATION_DETAILS_BY_BARCODE';

export const UPDATE_PRODUCT_IDENTIFIER_REQUEST = 'UPDATE_PRODUCT_IDENTIFIER_REQUEST';
export const UPDATE_PRODUCT_IDENTIFIER_SUCCESS = 'UPDATE_PRODUCT_IDENTIFIER_SUCCESS';
export const UPDATE_PRODUCT_IDENTIFIER_FAIL = 'UPDATE_PRODUCT_IDENTIFIER_FAIL';

export function getProductsAction(callback?: (products: any) => void) {
return {
type: GET_PRODUCTS_REQUEST,
Expand Down Expand Up @@ -111,3 +115,16 @@ export function getSortationDetailsByBarcode(barcode: string, callback: (data: a
callback
};
}

export function updateProductIdentifierAction(
id: string,
type: string,
value: string,
callback?: (response: any) => void
) {
return {
type: UPDATE_PRODUCT_IDENTIFIER_REQUEST,
payload: { id, type, value },
callback
};
}
31 changes: 30 additions & 1 deletion src/redux/sagas/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
SEARCH_PRODUCTS_BY_NAME_REQUEST,
SEARCH_PRODUCTS_BY_NAME_REQUEST_SUCCESS,
STOCK_ADJUSTMENT_REQUEST,
STOCK_ADJUSTMENT_REQUEST_SUCCESS
STOCK_ADJUSTMENT_REQUEST_SUCCESS,
UPDATE_PRODUCT_IDENTIFIER_REQUEST,
UPDATE_PRODUCT_IDENTIFIER_SUCCESS
} from '../actions/products';

import * as api from '../../apis';
Expand Down Expand Up @@ -245,6 +247,32 @@ function* getSortationDetailsSaga(action: any) {
}
}

function* updateProductIdentifierSaga(action: any) {
try {
yield put(showScreenLoading('Updating Product Identifier...'));
const response = yield call(
api.updateProductIdentifier,
action.payload.id,
action.payload.type,
action.payload.value
);
yield put({
type: UPDATE_PRODUCT_IDENTIFIER_SUCCESS,
payload: response.data
});
if (action.callback) action.callback(response.data);
yield put(hideScreenLoading());
} catch (error: any) {
yield put(hideScreenLoading());
if (action.callback) {
action.callback({
error: true,
errorMessage: error.message
});
}
}
}

export default function* watcher() {
yield takeLatest(GET_PRODUCTS_REQUEST, getProducts);
yield takeLatest(SEARCH_PRODUCTS_BY_NAME_REQUEST, searchProductsByName);
Expand All @@ -256,4 +284,5 @@ export default function* watcher() {
yield takeLatest(STOCK_ADJUSTMENT_REQUEST, stockAdjustments);
yield takeLatest(SEARCH_BARCODE, searchBarcode);
yield takeLatest(GET_SORTATION_DETAILS_BY_BARCODE, getSortationDetailsSaga);
yield takeLatest(UPDATE_PRODUCT_IDENTIFIER_REQUEST, updateProductIdentifierSaga);
}
74 changes: 74 additions & 0 deletions src/screens/ProductDetails/EditBarcodeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useEffect, useRef, useState } from 'react';
import { Modal, View, Keyboard } from 'react-native';
import { Button, Headline, Paragraph, TextInput } from 'react-native-paper';
import Theme from '../../utils/Theme';
import styles from './styles';

type EditBarcodeModalProps = {
visible: boolean;
currentBarcode?: string;
onSave: (barcode: string) => void;
onClose: () => void;
};

export default function EditBarcodeModal({ visible, currentBarcode, onSave, onClose }: EditBarcodeModalProps) {
const [barcode, setBarcode] = useState(currentBarcode || '');
const inputRef = useRef<any>(null);

useEffect(() => {
setBarcode(currentBarcode || '');
}, [currentBarcode]);

useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
inputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}
}, [visible]);

return (
<Modal transparent animationType="slide" visible={visible} onDismiss={onClose}>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Headline>Edit Barcode</Headline>
<Paragraph style={{ marginBottom: Theme.spacing.large }}>
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.
</Paragraph>

<TextInput
blurOnSubmit
ref={inputRef}
autoCompleteType="off"
label="Barcode (UPC)"
mode="outlined"
value={barcode}
style={styles.bottomSeparator}
returnKeyType="done"
onChangeText={setBarcode}
onSubmitEditing={() => {
Keyboard.dismiss();
onSave(barcode);
}}
/>

<View style={styles.actionButtons}>
<Button onPress={onClose}>Cancel</Button>
<Button
style={styles.topSeparator}
mode="contained"
onPress={() => {
Keyboard.dismiss();
onSave(barcode);
}}
>
Save
</Button>
</View>
</View>
</View>
</Modal>
);
}
7 changes: 7 additions & 0 deletions src/screens/ProductDetails/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ export interface DispatchProps {
getProductByIdAction: (id: any, callback?: (data: any) => void) => void;
showScreenLoading: () => void;
hideScreenLoading: () => void;
updateProductIdentifierAction: (
id: string,
identifierType: string,
identifierValue: string,
callback?: (data: any) => void
) => void;
}
export interface State {
visible: boolean;
productDetails: Product | any;
editBarcodeVisible: boolean;
}
export type Props = OwnProps & StateProps & DispatchProps & State;
1 change: 1 addition & 0 deletions src/screens/ProductDetails/VM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface VM {
unitOfMeasure: string;
image?: any;
availableItems: [];
upc: string;
}

export interface DetailsItemVM {
Expand Down
14 changes: 7 additions & 7 deletions src/screens/ProductDetails/VMMapper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Props, State } from './types';
import { DetailsItemVM, VM } from './VM';
import { HYPHEN } from '../../constants';
import { ProductCategory } from '../../data/product/category/ProductCategory';
import Product from '../../data/product/Product';
import { DetailsItemVM, VM } from './VM';

export function vmMapper(product: Product, state: State): VM {
// eslint-disable-next-line complexity
export function vmMapper(product: Product): VM {
return {
header: 'Product Details',
name: product?.name ?? '',
Expand Down Expand Up @@ -32,7 +33,8 @@ export function vmMapper(product: Product, state: State): VM {
url: ''
}
],
availableItems: product.availableItems
availableItems: product.availableItems,
upc: product.upc || HYPHEN
};
}

Expand Down Expand Up @@ -60,8 +62,6 @@ function getDetailsCategoryItem(product: Product): DetailsItemVM {
}

function getCategoryText(category: ProductCategory): string {
const prefix = category?.parentCategory
? `${getCategoryText(category?.parentCategory)} > `
: '';
const prefix = category?.parentCategory ? `${getCategoryText(category?.parentCategory)} > ` : '';
return `${prefix}${category?.name}`;
}
84 changes: 70 additions & 14 deletions src/screens/ProductDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React from 'react';
import { ScrollView, TouchableOpacity, View } from 'react-native';
import { Caption, Card, Chip, Divider, Paragraph, Subheading } from 'react-native-paper';
import { Caption, Card, Chip, Divider, Button as PaperButton, Paragraph, Subheading } from 'react-native-paper';
import { connect } from 'react-redux';

import Button from '../../components/Button';
import showPopup from '../../components/Popup';
import PrintModal from '../../components/PrintModal';
import { HYPHEN } from '../../constants';
import { HYPHEN, NOT_CHANGED_STATUS } from '../../constants';
import { hideScreenLoading, showScreenLoading } from '../../redux/actions/main';
import { getProductByIdAction } from '../../redux/actions/products';
import { getProductByIdAction, updateProductIdentifierAction } from '../../redux/actions/products';
import { RootState } from '../../redux/reducers';
import EditBarcodeModal from './EditBarcodeModal';
import styles from './styles';
import { DispatchProps, Props, State } from './Types';
import { vmMapper } from './VMMapper';
Expand Down Expand Up @@ -39,7 +40,8 @@ class ProductDetails extends React.Component<Props, State> {
super(props);
this.state = {
visible: false,
productDetails: {}
productDetails: {},
editBarcodeVisible: false
};
}

Expand All @@ -52,13 +54,53 @@ class ProductDetails extends React.Component<Props, State> {
this.setState({ visible: false });
};

openEditBarcodeModal = () => {
this.setState({ editBarcodeVisible: true });
};

closeEditBarcodeModal = () => {
this.setState({ editBarcodeVisible: false });
};

handleClick = () => {
const { product } = this.props.route.params;
this.props.getProductByIdAction(product.id, (data) => {
this.props.getProductByIdAction(product.id, () => {
this.setState({ visible: true });
});
};

handleSaveBarcode = (newBarcode: string) => {
const { productDetails } = this.state;
const id = productDetails.id;
if (!id) {
return;
}

this.props.updateProductIdentifierAction(id, 'upc', newBarcode, (response: any) => {
if (response?.error) {
showPopup({
title: 'Error',
message: response.errorMessage ?? 'Product Identifier update failed',
positiveButton: { text: 'OK' }
});
return;
}

// Check for no changes made
if (response?.status === NOT_CHANGED_STATUS) {
showPopup({
title: 'No Changes Made',
message: 'The new barcode is the same as the current one. No changes were made.',
positiveButton: { text: 'OK' }
});
return;
}

this.setState({ editBarcodeVisible: false });
this.getProductDetails(id);
});
};

componentDidMount() {
this.getProduct();
}
Expand Down Expand Up @@ -146,7 +188,7 @@ class ProductDetails extends React.Component<Props, State> {
};

render() {
const vm = vmMapper(this.state.productDetails, this.state);
const vm = vmMapper(this.state.productDetails);
const product = this.props.selectedProduct;
const { visible } = this.state;
const filteredItems =
Expand All @@ -161,11 +203,17 @@ class ProductDetails extends React.Component<Props, State> {
type={'products'}
defaultBarcodeLabelUrl={product?.defaultBarcodeLabelUrl}
/>
<EditBarcodeModal
visible={this.state.editBarcodeVisible}
currentBarcode={this.state.productDetails?.upc}
onClose={this.closeEditBarcodeModal}
onSave={this.handleSaveBarcode}
/>
<View style={styles.contentContainer}>
<View style={styles.header}>
<View style={styles.headerRow}>
<Chip icon="barcode" style={styles.chipDefault} textStyle={styles.chipText}>
{vm.productCode}
{`Product Code: ${vm.productCode || HYPHEN}`}
</Chip>
</View>

Expand All @@ -174,17 +222,23 @@ class ProductDetails extends React.Component<Props, State> {
<Subheading style={styles.name}>{vm.name}</Subheading>

<View style={styles.additionalInfoRow}>
<Chip icon="barcode" style={styles.chipDefault} textStyle={styles.chipText}>
{/* UPC (Universal Product Code) — ensures consistency if the barcode differs from the product code */}
{`Barcode: ${vm.upc}`}
</Chip>
<Chip icon="package" style={styles.chipDefault} textStyle={styles.chipText}>
{`Items Available: ${filteredItems.length}`}
</Chip>
</View>

<Button
style={styles.refreshButton}
size="100%"
title="Refresh (Get Latest Stock)"
onPress={this.getProduct}
/>
<View style={styles.actionButtons}>
<PaperButton icon="barcode" mode="contained" onPress={this.openEditBarcodeModal}>
Edit Barcode
</PaperButton>
<PaperButton style={styles.topSeparator} icon="refresh" mode="contained" onPress={this.getProduct}>
Refresh Availability
</PaperButton>
</View>
</View>
<Divider />
<ScrollView style={styles.detailsSection}>
Expand Down Expand Up @@ -228,10 +282,12 @@ const mapStateToProps = (state: RootState) => ({
selectedProduct: state.productsReducer.selectedProduct,
productSummaryConfig: state.settingsReducer.productSummaryConfig
});

const mapDispatchToProps: DispatchProps = {
getProductByIdAction,
showScreenLoading,
hideScreenLoading
hideScreenLoading,
updateProductIdentifierAction
};

export default connect(mapStateToProps, mapDispatchToProps)(ProductDetails);
Loading