From 00a60e7d0be0c11418f69f360b43ae4f9bcba580 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Mon, 3 Nov 2025 14:42:38 +0100 Subject: [PATCH 1/6] OBLS-284: allow to edit product barcode (upc) --- src/apis/products.ts | 4 + src/data/product/Product.ts | 1 + src/redux/actions/products.ts | 12 +++ src/redux/sagas/products.ts | 26 +++++- .../ProductDetails/EditBarcodeModal.tsx | 74 +++++++++++++++++ src/screens/ProductDetails/Types.ts | 2 + src/screens/ProductDetails/VM.ts | 1 + src/screens/ProductDetails/VMMapper.ts | 14 ++-- src/screens/ProductDetails/index.tsx | 82 ++++++++++++++++--- src/screens/ProductDetails/styles.ts | 26 +++++- 10 files changed, 219 insertions(+), 23 deletions(-) create mode 100644 src/screens/ProductDetails/EditBarcodeModal.tsx diff --git a/src/apis/products.ts b/src/apis/products.ts index 63f1f18f..4f9a0d1f 100644 --- a/src/apis/products.ts +++ b/src/apis/products.ts @@ -63,3 +63,7 @@ export function searchBarcode(id: string) { export function getProductByBarcode(barcode: string) { return apiClient.get(`/barcodes?id=${encodeURIComponent(barcode)}`); } + +export function updateProductBarcode(id: string, barcode: string) { + return apiClient.put(`/mobile/products/${id}/barcode`, { upc: barcode }); +} diff --git a/src/data/product/Product.ts b/src/data/product/Product.ts index 1f82a40f..c0d6aee3 100644 --- a/src/data/product/Product.ts +++ b/src/data/product/Product.ts @@ -23,6 +23,7 @@ interface Product { unitOfMeasure: string; image?: any; availableItems: any; + upc?: string; } export default Product; diff --git a/src/redux/actions/products.ts b/src/redux/actions/products.ts index 0f9ff25a..fb95f08e 100644 --- a/src/redux/actions/products.ts +++ b/src/redux/actions/products.ts @@ -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_BARCODE_REQUEST = 'UPDATE_PRODUCT_BARCODE_REQUEST'; +export const UPDATE_PRODUCT_BARCODE_SUCCESS = 'UPDATE_PRODUCT_BARCODE_SUCCESS'; +export const UPDATE_PRODUCT_BARCODE_FAIL = 'UPDATE_PRODUCT_BARCODE_FAIL'; + export function getProductsAction(callback?: (products: any) => void) { return { type: GET_PRODUCTS_REQUEST, @@ -111,3 +115,11 @@ export function getSortationDetailsByBarcode(barcode: string, callback: (data: a callback }; } + +export function updateProductBarcodeAction(id: string, barcode: string, callback?: (response: any) => void) { + return { + type: UPDATE_PRODUCT_BARCODE_REQUEST, + payload: { id, barcode }, + callback + }; +} diff --git a/src/redux/sagas/products.ts b/src/redux/sagas/products.ts index d8e9af28..fcf4fe9a 100644 --- a/src/redux/sagas/products.ts +++ b/src/redux/sagas/products.ts @@ -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_BARCODE_REQUEST, + UPDATE_PRODUCT_BARCODE_SUCCESS } from '../actions/products'; import * as api from '../../apis'; @@ -245,6 +247,27 @@ function* getSortationDetailsSaga(action: any) { } } +function* updateProductBarcodeSaga(action: any) { + try { + yield put(showScreenLoading('Updating Product Barcode...')); + const response = yield call(api.updateProductBarcode, action.payload.id, action.payload.barcode); + yield put({ + type: UPDATE_PRODUCT_BARCODE_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); @@ -256,4 +279,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_BARCODE_REQUEST, updateProductBarcodeSaga); } diff --git a/src/screens/ProductDetails/EditBarcodeModal.tsx b/src/screens/ProductDetails/EditBarcodeModal.tsx new file mode 100644 index 00000000..155d682e --- /dev/null +++ b/src/screens/ProductDetails/EditBarcodeModal.tsx @@ -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(null); + + useEffect(() => { + setBarcode(currentBarcode || ''); + }, [currentBarcode]); + + useEffect(() => { + if (visible) { + const timer = setTimeout(() => { + inputRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + } + }, [visible]); + + return ( + + + + Edit Barcode + + 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. + + + { + Keyboard.dismiss(); + onSave(barcode); + }} + /> + + + + + + + + + ); +} diff --git a/src/screens/ProductDetails/Types.ts b/src/screens/ProductDetails/Types.ts index 99a1e40b..673a7101 100644 --- a/src/screens/ProductDetails/Types.ts +++ b/src/screens/ProductDetails/Types.ts @@ -18,9 +18,11 @@ export interface DispatchProps { getProductByIdAction: (id: any, callback?: (data: any) => void) => void; showScreenLoading: () => void; hideScreenLoading: () => void; + updateProductBarcodeAction: (id: string, barcode: string, callback?: (data: any) => void) => void; } export interface State { visible: boolean; productDetails: Product | any; + editBarcodeVisible: boolean; } export type Props = OwnProps & StateProps & DispatchProps & State; diff --git a/src/screens/ProductDetails/VM.ts b/src/screens/ProductDetails/VM.ts index e58fb533..b51c40f7 100644 --- a/src/screens/ProductDetails/VM.ts +++ b/src/screens/ProductDetails/VM.ts @@ -22,6 +22,7 @@ export interface VM { unitOfMeasure: string; image?: any; availableItems: []; + upc: string; } export interface DetailsItemVM { diff --git a/src/screens/ProductDetails/VMMapper.ts b/src/screens/ProductDetails/VMMapper.ts index b6b5addb..8a1960e0 100644 --- a/src/screens/ProductDetails/VMMapper.ts +++ b/src/screens/ProductDetails/VMMapper.ts @@ -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 ?? '', @@ -32,7 +33,8 @@ export function vmMapper(product: Product, state: State): VM { url: '' } ], - availableItems: product.availableItems + availableItems: product.availableItems, + upc: product.upc || HYPHEN }; } @@ -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}`; } diff --git a/src/screens/ProductDetails/index.tsx b/src/screens/ProductDetails/index.tsx index a0ece0db..ea04e17a 100644 --- a/src/screens/ProductDetails/index.tsx +++ b/src/screens/ProductDetails/index.tsx @@ -1,6 +1,6 @@ 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'; @@ -8,8 +8,9 @@ import showPopup from '../../components/Popup'; import PrintModal from '../../components/PrintModal'; import { HYPHEN } from '../../constants'; import { hideScreenLoading, showScreenLoading } from '../../redux/actions/main'; -import { getProductByIdAction } from '../../redux/actions/products'; +import { getProductByIdAction, updateProductBarcodeAction } 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'; @@ -39,7 +40,8 @@ class ProductDetails extends React.Component { super(props); this.state = { visible: false, - productDetails: {} + productDetails: {}, + editBarcodeVisible: false }; } @@ -52,13 +54,53 @@ class ProductDetails extends React.Component { 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.updateProductBarcodeAction(id, newBarcode, (response: any) => { + if (response?.error) { + showPopup({ + title: 'Error', + message: response.errorMessage ?? 'Product Barcode update failed', + positiveButton: { text: 'OK' } + }); + return; + } + + // Check for no changes made + if (response?.code === 304) { + 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(); } @@ -146,7 +188,7 @@ class ProductDetails extends React.Component { }; 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 = @@ -161,11 +203,17 @@ class ProductDetails extends React.Component { type={'products'} defaultBarcodeLabelUrl={product?.defaultBarcodeLabelUrl} /> + - {vm.productCode} + {`Product Code: ${vm.productCode || HYPHEN}`} @@ -174,17 +222,23 @@ class ProductDetails extends React.Component { {vm.name} + + {/* UPC (Universal Product Code) — ensures consistency if the barcode differs from the product code */} + {`Barcode: ${vm.upc}`} + {`Items Available: ${filteredItems.length}`} -