diff --git a/src/Main.tsx b/src/Main.tsx index 8862c79c..17779310 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -4,10 +4,9 @@ import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { Component } from 'react'; import { SafeAreaView } from 'react-native'; -import { Provider } from 'react-native-paper'; import SplashScreen from 'react-native-splash-screen'; import { connect } from 'react-redux'; - +import { Providers } from './components/Providers'; import FullScreenLoadingIndicator from './components/FullScreenLoadingIndicator'; import showPopup from './components/Popup'; import { appConfig } from './constants'; @@ -74,7 +73,11 @@ import PickingPickOutboundContainerScreen from './screens/Picking/PickingPickOut import PickingPickStagingLocationScreen from './screens/Picking/PickingPickStagingLocationScreen'; import PickingMoveToStagingScreen from './screens/Picking/PickingMoveToStaging'; import PickingStagingDropScreen from './screens/Picking/PickingStagingDrop'; -import { PickingProvider } from './screens/Picking/PickingContext'; +import { ReplenishmentLocationScreen } from './screens/Replenishment/ReplenishmentLocationScreen'; +import { ReplenishmentProductScreen } from './screens/Replenishment/ReplenishmentProductScreen'; +import ReplenishmentOutboundContainerScreen from './screens/Replenishment/ReplenishmentOutboundContainerScreen'; +import ReplenishmentPickQuantityScreen from './screens/Replenishment/ReplenishmentPickQuantityScreen'; +import { ReplenishmentStagingLocationScreen } from './screens/Replenishment/ReplenishmentStagingLocationScreen'; import Transfer from './screens/Transfer'; import Transfers from './screens/Transfers'; import TransferDetails from './screens/TransfersDetails'; @@ -163,255 +166,270 @@ class Main extends Component { const { loggedIn } = this.props; const initialRouteName = !loggedIn ? 'Login' : 'Choose Location'; return ( - - - - - - ({ - headerRight: () => , - headerTintColor: Theme.colors.surface, - headerStyle: { - backgroundColor: Theme.colors.primary, - height: appConfig.APP_HEADER_HEIGHT - } - })} - > - - - - - - - - - - ({ title: route.params?.subroutesScreenName })} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + ({ + headerRight: () => , + headerTintColor: Theme.colors.surface, + headerStyle: { + backgroundColor: Theme.colors.primary, + height: appConfig.APP_HEADER_HEIGHT + } + })} + > + + + + + + + + + + ({ title: route.params?.subroutesScreenName })} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } } diff --git a/src/screens/Picking/PickingShortAndReasonModal.tsx b/src/components/PickingShortAndReasonModal/index.tsx similarity index 100% rename from src/screens/Picking/PickingShortAndReasonModal.tsx rename to src/components/PickingShortAndReasonModal/index.tsx diff --git a/src/components/PickingShortAndReasonModal/styles.ts b/src/components/PickingShortAndReasonModal/styles.ts new file mode 100644 index 00000000..f4e765cc --- /dev/null +++ b/src/components/PickingShortAndReasonModal/styles.ts @@ -0,0 +1,55 @@ +import { StyleSheet } from 'react-native'; +import Theme from '../../utils/Theme'; + +export default StyleSheet.create({ + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.5)' + }, + modalContent: { + width: '90%', + backgroundColor: 'white', + padding: Theme.spacing.large, + borderRadius: Theme.roundness * 2 + }, + modalSurface: { + width: '90%', + borderRadius: 8, + padding: 16, + elevation: 4, + backgroundColor: 'white' + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8 + }, + modalContainer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0 + }, + modalTitleText: { + fontSize: 18, + fontWeight: 'bold', + color: Theme.colors.text, + flexShrink: 1 + }, + modalDescription: { + fontSize: 14, + color: Theme.colors.text, + marginBottom: Theme.spacing.medium + }, + dialogActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: Theme.spacing.medium + } +}); diff --git a/src/screens/Picking/ProductDetails.tsx b/src/components/ProductDetails/index.tsx similarity index 100% rename from src/screens/Picking/ProductDetails.tsx rename to src/components/ProductDetails/index.tsx diff --git a/src/components/ProductDetails/styles.ts b/src/components/ProductDetails/styles.ts new file mode 100644 index 00000000..f6741d80 --- /dev/null +++ b/src/components/ProductDetails/styles.ts @@ -0,0 +1,40 @@ +import { StyleSheet } from 'react-native'; +import Theme from '../../utils/Theme'; + +export default StyleSheet.create({ + productDetails: { + backgroundColor: Theme.colors.surface, + padding: Theme.spacing.large, + display: 'flex', + flexDirection: 'column' + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + chipDefault: { + height: 28, + justifyContent: 'flex-start', + borderRadius: 4, + alignItems: 'center' + }, + chipText: { + fontSize: 12, + color: Theme.colors.text + }, + fontBold: { + fontWeight: 'bold' + }, + title: { + fontSize: 18, + color: Theme.colors.text, + fontWeight: 'bold' + }, + divider: { + marginVertical: Theme.spacing.small + }, + marginTopSmall: { + marginTop: Theme.spacing.small + } +}); diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx new file mode 100644 index 00000000..d61b79ee --- /dev/null +++ b/src/components/Providers.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Provider } from 'react-native-paper'; + +import { PickingProvider } from '../screens/Picking/PickingContext'; +import { ReplenishmentProvider } from '../screens/Replenishment/ReplenishmentContext'; +import Theme from '../utils/Theme'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/redux/sagas/others.ts b/src/redux/sagas/others.ts index 17a41953..b84d907f 100644 --- a/src/redux/sagas/others.ts +++ b/src/redux/sagas/others.ts @@ -23,9 +23,10 @@ function* getReasonCodes(action: any) { if (action.callback) { yield call(action.callback, { error: true, - errorMessage: error.message + errorMessage: error.message ?? 'Failed To Load Reason Codes' }); } + yield put(hideScreenLoading()); } } diff --git a/src/screens/Dashboard/dashboardData.ts b/src/screens/Dashboard/dashboardData.ts index c001f3d0..c18a9783 100644 --- a/src/screens/Dashboard/dashboardData.ts +++ b/src/screens/Dashboard/dashboardData.ts @@ -145,6 +145,13 @@ const dashboardEntries: DashboardEntry[] = [ entryDescription: 'Enter a List ID to select the Cycle Count', icon: IconInventory, navigationScreenName: 'CycleCountListEntry' + }, + { + key: 'replenishment', + screenName: 'Replenishment', + entryDescription: 'Manage inventory replenishment tasks', + icon: IconProducts, + navigationScreenName: 'ReplenishmentPickingLocation' } ] }, diff --git a/src/screens/Picking/PickingPickLocationScreen.tsx b/src/screens/Picking/PickingPickLocationScreen.tsx index 9f756e55..43ed69ae 100644 --- a/src/screens/Picking/PickingPickLocationScreen.tsx +++ b/src/screens/Picking/PickingPickLocationScreen.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { Alert, View } from 'react-native'; import { Divider, Paragraph, Subheading } from 'react-native-paper'; +import { ProductDetails } from '../../components/ProductDetails'; import { ScannerInput } from '../../components/ScannerInput'; import { EMPTY_STRING, HYPHEN } from '../../constants'; import { navigate } from '../../NavigationService'; import { usePickingContext } from './PickingContext'; -import { ProductDetails } from './ProductDetails'; import styles from './styles'; export default function PickingPickLocationScreen() { diff --git a/src/screens/Picking/PickingPickOutboundContainerScreen.tsx b/src/screens/Picking/PickingPickOutboundContainerScreen.tsx index 4fafd249..1e4c37a0 100644 --- a/src/screens/Picking/PickingPickOutboundContainerScreen.tsx +++ b/src/screens/Picking/PickingPickOutboundContainerScreen.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import { Alert, View } from 'react-native'; import { Divider, Paragraph, Subheading } from 'react-native-paper'; +import { ProductDetails } from '../../components/ProductDetails'; import { ScannerInput } from '../../components/ScannerInput'; import { EMPTY_STRING } from '../../constants'; import { navigate } from '../../NavigationService'; import { ReasonCode } from '../../types/picking'; import { usePickingContext } from './PickingContext'; -import { ProductDetails } from './ProductDetails'; import styles from './styles'; import { revalidateTaskAndProceed } from './lib'; diff --git a/src/screens/Picking/PickingPickProductScreen.tsx b/src/screens/Picking/PickingPickProductScreen.tsx index c9a59928..48989a85 100644 --- a/src/screens/Picking/PickingPickProductScreen.tsx +++ b/src/screens/Picking/PickingPickProductScreen.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { Alert, View } from 'react-native'; import { Divider, Paragraph, Subheading } from 'react-native-paper'; +import { ProductDetails } from '../../components/ProductDetails'; import { ScannerInput } from '../../components/ScannerInput'; import { EMPTY_STRING, HYPHEN } from '../../constants'; import { navigate } from '../../NavigationService'; import { usePickingContext } from './PickingContext'; -import { ProductDetails } from './ProductDetails'; import styles from './styles'; import { isProductBarcodeValid } from '../../utils/utils'; diff --git a/src/screens/Picking/PickingPickQuantityScreen.tsx b/src/screens/Picking/PickingPickQuantityScreen.tsx index d04c4911..b938e350 100644 --- a/src/screens/Picking/PickingPickQuantityScreen.tsx +++ b/src/screens/Picking/PickingPickQuantityScreen.tsx @@ -4,13 +4,13 @@ import { Alert, TextInput, View } from 'react-native'; import { Button, Divider, TextInput as PaperTextInput, Paragraph, Subheading } from 'react-native-paper'; import { useDispatch } from 'react-redux'; +import PickingShortAndReasonModal from '../../components/PickingShortAndReasonModal'; +import { ProductDetails } from '../../components/ProductDetails'; import { HYPHEN, INPUT_FOCUS_DELAY_TIME_IN_MS } from '../../constants'; import { navigate } from '../../NavigationService'; import { getReasonCodesAction } from '../../redux/actions/others'; import { ReasonCode } from '../../types/picking'; import { usePickingContext } from './PickingContext'; -import PickingShortAndReasonModal from './PickingShortAndReasonModal'; -import { ProductDetails } from './ProductDetails'; import styles from './styles'; import { revalidateTaskAndProceed } from './lib'; diff --git a/src/screens/Picking/PickingPickStagingLocationScreen.tsx b/src/screens/Picking/PickingPickStagingLocationScreen.tsx index 765f0c71..8a51dee2 100644 --- a/src/screens/Picking/PickingPickStagingLocationScreen.tsx +++ b/src/screens/Picking/PickingPickStagingLocationScreen.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { Alert, View } from 'react-native'; import { Divider, Paragraph, Subheading } from 'react-native-paper'; +import { ProductDetails } from '../../components/ProductDetails'; import { ScannerInput } from '../../components/ScannerInput'; import { EMPTY_STRING } from '../../constants'; import { navigate } from '../../NavigationService'; import { usePickingContext } from './PickingContext'; -import { ProductDetails } from './ProductDetails'; import styles from './styles'; export default function PickingPickStagingLocationScreen() { diff --git a/src/screens/Picking/styles.ts b/src/screens/Picking/styles.ts index 008b98c7..b2073462 100644 --- a/src/screens/Picking/styles.ts +++ b/src/screens/Picking/styles.ts @@ -51,28 +51,6 @@ export default StyleSheet.create({ fontSize: 14 }, - productDetails: { - backgroundColor: Theme.colors.surface, - padding: Theme.spacing.large, - display: 'flex', - flexDirection: 'column' - }, - headerRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center' - }, - chipDefault: { - height: 28, - justifyContent: 'flex-start', - borderRadius: 4, - alignItems: 'center' - }, - chipText: { - fontSize: 12, - color: Theme.colors.text - }, - title: { fontSize: 18, color: Theme.colors.text, @@ -109,91 +87,34 @@ export default StyleSheet.create({ marginRight: Theme.spacing.small }, - modalOverlay: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - width: '100%', - height: '100%', - backgroundColor: 'rgba(0, 0, 0, 0.5)' - }, - modalContent: { - width: '90%', - backgroundColor: 'white', - padding: Theme.spacing.large, - borderRadius: Theme.roundness * 2 - }, - modalSurface: { - width: '90%', - borderRadius: 8, - padding: 16, - elevation: 4, - backgroundColor: 'white' - }, - modalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8 - }, - modalContainer: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0 - }, - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center' - }, - modalTitleText: { - fontSize: 18, - fontWeight: 'bold', - color: Theme.colors.text, - flexShrink: 1 - }, - modalDescription: { - fontSize: 14, - color: Theme.colors.text, - marginBottom: Theme.spacing.medium - }, - - /** Dialog & actions **/ - dialogActions: { - flexDirection: 'row', - justifyContent: 'flex-end', - marginTop: Theme.spacing.medium - }, - dialogActionButton: { - marginLeft: Theme.spacing.small - }, - dialogSuggestButton: { - marginVertical: Theme.spacing.medium - }, - dropdownContainer: { - width: '50%', - alignSelf: 'flex-end' - }, flex1: { flex: 1 }, fullWidth: { width: '100%' }, - actionsWrapper: { - padding: Theme.spacing.medium, - alignItems: 'center', - borderTopWidth: 1, - borderTopColor: '#E0E0E0', - backgroundColor: 'white' - }, itemCard: { padding: Theme.spacing.large, backgroundColor: 'white', marginBottom: Theme.spacing.small, borderRadius: Theme.roundness, elevation: 2 + }, + + productDetails: { + backgroundColor: Theme.colors.surface, + padding: Theme.spacing.large, + display: 'flex', + flexDirection: 'column' + }, + chipDefault: { + height: 28, + justifyContent: 'flex-start', + borderRadius: 4, + alignItems: 'center' + }, + chipText: { + fontSize: 12, + color: Theme.colors.text } }); diff --git a/src/screens/Replenishment/ReplenishmentContext.tsx b/src/screens/Replenishment/ReplenishmentContext.tsx new file mode 100644 index 00000000..095411f1 --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentContext.tsx @@ -0,0 +1,131 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { createContext, ReactNode, useContext, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { DUMMY_REPLENISHMENT_TASKS } from './mock-data'; +import { ReplenishmentTask } from './types'; + +type ReplenishmentContextType = { + allTasks: ReplenishmentTask[]; + tasksCount: number; + currentTask?: ReplenishmentTask; + currentTaskIndex: number; + startReplenishment: (callback: (response: { errorMessage?: string }) => void) => void; + goToNextTask: () => void; + pickCurrentTask: (outboundContainerId: string, callback: (response: { errorMessage?: string }) => void) => void; + shortPickTask: ( + outboundContainerId: string, + parsedQuantityPicked: number, + callback: (response: { errorMessage?: string }) => void, + reasonCodeName?: string + ) => void; + revalidateCurrentTask: (callback: (revalidatedTask?: ReplenishmentTask) => void) => void; + revalidateTasksForOrder: (orderId: string, callback: () => void) => void; + dropCurrentTaskAtStagingLocation: ( + task: ReplenishmentTask, + callback: (response: { errorMessage?: string }) => void + ) => void; + resetReplenishmentSession: () => void; +}; + +const ReplenishmentContext = createContext(undefined); + +export function ReplenishmentProvider({ children }: { children: ReactNode }) { + const dispatch = useDispatch(); + + // @ts-ignore + const [allTasks, setAllTasks] = useState(DUMMY_REPLENISHMENT_TASKS); + const [currentTaskIndex, setCurrentTaskIndex] = useState(0); + const tasksCount = allTasks.length; + const currentTask = DUMMY_REPLENISHMENT_TASKS[0]; + // const currentTask = tasksCount > 0 ? allTasks[currentTaskIndex] : undefined; + + const startReplenishment = (callback: (response: { errorMessage?: string }) => void) => { + // TODO: Implement the logic for starting the replenishment process + // dispatch(startReplenishmentProcess(currentTask.id, callback)); + }; + + const pickCurrentTask = (outboundContainerId: string, callback: (response: { errorMessage?: string }) => void) => { + // TODO: Implement the logic for picking the current task + // dispatch(pickCurrentTask(currentTask.id, outboundContainerId, callback)); + }; + + const shortPickTask = ( + outboundContainerId: string, + parsedQuantityPicked: number, + callback: (response: { errorMessage?: string }) => void, + reasonCodeName?: string + ) => { + // TODO: Implement the logic for short picking the current task + // dispatch(shortPickTask(currentTask.id, outboundContainerId, parsedQuantityPicked, callback, reasonCodeName)); + }; + + const revalidateCurrentTask = (callback: (revalidatedTask?: ReplenishmentTask) => void) => { + // TODO: Implement the logic for revalidating the current task + // dispatch(getTaskById(currentTask.id, ({ response }) => { + // if (response.errorCode || !response.data) { + // Alert.alert('Failed To Revalidate', 'Could not revalidate the current task.'); + // return; + // } + // + // const revalidatedTask = response.data; + // setAllTasks((prevTasks) => prevTasks.map((task, index) => (index === currentTaskIndex ? revalidatedTask : task))); + // callback?.(revalidatedTask); + // }); + }; + + const revalidateTasksForOrder = (orderId: string, callback: () => void) => { + // TODO: Implement the logic for revalidating all tasks for the order + // dispatch(revalidateTasksForOrderAction(orderId, ({ response }) => { + // TODO: Handle response and update tasks accordingly + //})); + }; + + const dropCurrentTaskAtStagingLocation = ( + task: ReplenishmentTask, + callback: (response: { errorMessage?: string }) => void + ) => { + // TODO: Implement the logic for dropping the current task at the staging location + // dispatch(dropTaskAtStagingLocation(task.outboundContainer.id, task.stagingLocation.id, callback)); + }; + + const resetReplenishmentSession = () => { + setAllTasks([]); + setCurrentTaskIndex(0); + }; + + const goToNextTask = () => { + setCurrentTaskIndex((prevIndex) => prevIndex + 1); + }; + + return ( + + {children} + + ); +} + +export function useReplenishmentContext() { + const context = useContext(ReplenishmentContext); + if (!context) { + throw new Error('useReplenishmentContext must be used within a ReplenishmentProvider'); + } + return context; +} diff --git a/src/screens/Replenishment/ReplenishmentLocationScreen.tsx b/src/screens/Replenishment/ReplenishmentLocationScreen.tsx new file mode 100644 index 00000000..7dcaad68 --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentLocationScreen.tsx @@ -0,0 +1,104 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React from 'react'; +import { Divider, Paragraph, Subheading } from 'react-native-paper'; + +import { Alert, View } from 'react-native'; +import { ProductDetails } from '../../components/ProductDetails'; +import { ScannerInput } from '../../components/ScannerInput'; +import { EMPTY_STRING, HYPHEN } from '../../constants'; +import { navigate } from '../../NavigationService'; +import { DUMMY_REPLENISHMENT } from './mock-data'; +import { useReplenishmentContext } from './ReplenishmentContext'; +import styles from './styles'; + +export function ReplenishmentLocationScreen() { + const { currentTask, currentTaskIndex, tasksCount, startReplenishment } = useReplenishmentContext(); + + const [locationBarcode, setLocationBarcode] = React.useState(''); + + if (!currentTask) { + Alert.alert('No Replenishment Task', 'There is no current replenishment task available. Try again later.', [ + { + text: 'OK', + onPress: () => { + navigate('Dashboard'); + } + } + ]); + return null; + } + + function handleSubmit() { + const isValid = locationBarcode === currentTask?.location?.locationNumber; + + if (!isValid) { + Alert.alert( + 'Invalid Barcode', + `Incorrect location scanned. Expected: ${currentTask?.location?.locationNumber}. Try again.` + ); + setLocationBarcode(''); + return; + } + + // startReplenishment((response) => { + // if ('errorMessage' in response) { + // Alert.alert('Error', response.errorMessage); + // setLocationBarcode(''); + // return; + // } + + // navigate('ReplenishmentProduct'); + // }); + + setLocationBarcode(EMPTY_STRING); + navigate('ReplenishmentProduct'); + } + + return ( + // @ts-ignore + + + + + {DUMMY_REPLENISHMENT.product.productCode} + + + {`${currentTaskIndex + 1} / ${tasksCount}`} + + + + + + + + + + + + + Scan Pick Location Barcode + + Point your barcode scanner at the pick location barcode or type the code manually. + + + + + + ); +} diff --git a/src/screens/Replenishment/ReplenishmentOutboundContainerScreen.tsx b/src/screens/Replenishment/ReplenishmentOutboundContainerScreen.tsx new file mode 100644 index 00000000..d6c8c310 --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentOutboundContainerScreen.tsx @@ -0,0 +1,173 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { RouteProp, useRoute } from '@react-navigation/native'; +import * as React from 'react'; +import { Alert, View } from 'react-native'; +import { Divider, Paragraph, Subheading } from 'react-native-paper'; + +import { ProductDetails } from '../../components/ProductDetails'; +import { ScannerInput } from '../../components/ScannerInput'; +import { navigate } from '../../NavigationService'; +import { ReasonCode } from '../../types/picking'; +import { useReplenishmentContext } from './ReplenishmentContext'; +import styles from './styles'; + +type ReplenishmentOutboundContainerScreenProps = RouteProp< + { ReplenishmentOutboundContainer: { reasonCode?: ReasonCode; quantityPicked?: string } }, + 'ReplenishmentOutboundContainer' +>; + +export default function ReplenishmentOutboundContainerScreen() { + const { + currentTask, + pickCurrentTask, + shortPickTask, + currentTaskIndex, + tasksCount, + revalidateCurrentTask, + goToNextTask, + revalidateTasksForOrder + } = useReplenishmentContext(); + const { params } = useRoute(); + + const parsedQuantityPicked = params?.quantityPicked ? Number(params.quantityPicked) : undefined; + const [outboundContainerId, setOutboundContainerId] = React.useState(''); + + if (!currentTask) { + Alert.alert('No Pick Task', 'There is no current pick task available. Try again later.', [ + { + text: 'OK', + onPress: () => { + navigate('Dashboard'); + } + } + ]); + return null; + } + + function revalidateTaskAndProceed() { + revalidateCurrentTask((revalidatedTask) => { + if (!revalidatedTask) { + Alert.alert('Error', 'Failed to revalidate the current pick task after picking.'); + return; + } + + if (currentTaskIndex + 1 >= tasksCount) { + // Last Task -> Navigate to staging location drop + Alert.alert('All Picks Complete', 'You have completed all picks. Proceeding to staging location drop.', [ + { + text: 'OK', + onPress: () => navigate('ReplenishmentStagingLocation') + } + ]); + } else { + // More Tasks -> Start over with next pick task + goToNextTask(); + navigate('ReplenishmentPickingLocation'); + } + }); + } + + function handleSubmit() { + if (!outboundContainerId) { + Alert.alert('Missing Input', 'Please scan or enter a valid Outbound Container ID.'); + setOutboundContainerId(''); + return; + } + + if (!currentTask) { + Alert.alert('Error', 'No current pick task available.'); + navigate('Dashboard'); + return; + } + + if (parsedQuantityPicked !== undefined && parsedQuantityPicked < currentTask.quantityRequired) { + // Handle Short Pick + shortPickTask( + outboundContainerId, + parsedQuantityPicked, + (response) => { + if ('errorMessage' in response) { + Alert.alert('Short Pick Error', response.errorMessage); + setOutboundContainerId(''); + return; + } + + if (params?.reasonCode?.id) { + // Revalidate all tasks for the order to get updated pick tasks + revalidateTasksForOrder(currentTask.orderId, () => { + navigate('ReplenishmentPickingLocation'); + }); + } else { + revalidateTaskAndProceed(); + } + }, + params?.reasonCode?.name + ); + return; + } + + // pickCurrentTask(outboundContainerId, ({ errorMessage }) => { + // if (errorMessage) { + // Alert.alert('Pick Error', errorMessage); + // setOutboundContainerId(''); + // return; + // } + + // revalidateTaskAndProceed(); + // }); + + // TODO: Temporarily bypass picking for outbound container scanning flow + navigate('ReplenishmentStagingLocation'); + } + + return ( + + + + + {currentTask.product.productCode} + + + {`${currentTaskIndex + 1} / ${tasksCount}`} + + + + + + + + + + + + + Scan Outbound Container + + Point your barcode scanner at the outbound container or type the code manually. + + + + + + ); +} diff --git a/src/screens/Replenishment/ReplenishmentPickQuantityScreen.tsx b/src/screens/Replenishment/ReplenishmentPickQuantityScreen.tsx new file mode 100644 index 00000000..e693261a --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentPickQuantityScreen.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useState } from 'react'; +import { Alert, TextInput, View } from 'react-native'; +import { Button, Divider, TextInput as PaperTextInput, Paragraph, Subheading } from 'react-native-paper'; +import { useDispatch } from 'react-redux'; + +import PickingShortAndReasonModal from '../../components/PickingShortAndReasonModal'; +import { ProductDetails } from '../../components/ProductDetails'; +import { HYPHEN } from '../../constants'; +import { useInputFocus } from '../../hooks/useInputFocus'; +import { navigate } from '../../NavigationService'; +import { getReasonCodesAction } from '../../redux/actions/others'; +import { ReasonCode } from '../../types/picking'; +import { DUMMY_REPLENISHMENT } from './mock-data'; +import { useReplenishmentContext } from './ReplenishmentContext'; +import styles from './styles'; + +export default function ReplenishmentPickQuantityScreen() { + const { currentTask, currentTaskIndex, tasksCount } = useReplenishmentContext(); + const dispatch = useDispatch(); + const inputRef = React.useRef(null); + + const [quantityPicked, setQuantityPicked] = useState(''); + const [reasonCodes, setReasonCodes] = useState([]); + const [selectedReasonCode, setSelectedReasonCode] = useState(undefined); + const [isShortModalVisible, setIsShortModalVisible] = useState(false); + + // Focus input when screen is focused + useInputFocus(inputRef); + + useEffect(() => { + dispatch( + getReasonCodesAction('PICKING_SHORTAGE', (data: any) => { + if ('errorMessage' in data) { + Alert.alert('Error', data.errorMessage); + return; + } + + setReasonCodes(data); + }) + ); + }, [dispatch]); + + if (!currentTask) { + Alert.alert('No Replenishment Task', 'There is no current replenishment task available. Try again later.'); + navigate('Dashboard'); + return null; + } + + function handleSubmit() { + const qty = Number(quantityPicked); + const isValid = !isNaN(qty) && qty >= 0 && currentTask?.quantityRequired; + + if (!isValid) { + Alert.alert('Invalid Quantity', 'Incorrect quantity picked. Please try again.'); + return; + } + + const qtyRemaining = currentTask?.quantityRequired - (currentTask?.quantityPicked || 0); + const isFullPicked = qty === qtyRemaining; + + if (qty > qtyRemaining) { + Alert.alert('Invalid Quantity', `Picked quantity cannot exceed remaining quantity to pick (${qtyRemaining}).`); + return; + } + + if (isFullPicked) { + navigate('ReplenishmentOutboundContainer'); + return; + } + + setIsShortModalVisible(true); + } + + function handleConfirmShort(reasonCode: ReasonCode | undefined) { + setIsShortModalVisible(false); + + navigate('PickingPickOutboundContainer', { reasonCode, quantityPicked }); + } + + return ( + <> + {/* @ts-ignore */} + + + + + {currentTask.product.productCode} + + + {`${currentTaskIndex + 1} / ${tasksCount}`} + + + + + + + + + + + + + Enter Quantity Picked + + Please enter the quantity of the product that you have picked from the location. + + + + + + + + + setIsShortModalVisible(false)} + onConfirm={handleConfirmShort} + /> + + ); +} diff --git a/src/screens/Replenishment/ReplenishmentProductScreen.tsx b/src/screens/Replenishment/ReplenishmentProductScreen.tsx new file mode 100644 index 00000000..1ec20ebb --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentProductScreen.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Divider, Paragraph, Subheading } from 'react-native-paper'; + +import { Alert, View } from 'react-native'; +import { ProductDetails } from '../../components/ProductDetails'; +import { ScannerInput } from '../../components/ScannerInput'; +import { HYPHEN } from '../../constants'; +import { navigate } from '../../NavigationService'; +import { DUMMY_REPLENISHMENT } from './mock-data'; +import { useReplenishmentContext } from './ReplenishmentContext'; +import styles from './styles'; + +export function ReplenishmentProductScreen() { + const { currentTask, currentTaskIndex, tasksCount } = useReplenishmentContext(); + const [productBarcode, setProductBarcode] = React.useState(''); + + if (!currentTask) { + Alert.alert('No Replenishment Task', 'There is no current replenishment task available. Try again later.'); + navigate('Dashboard'); + return null; + } + + function handleSubmit() { + // TODO: Replace with actual barcode validation logic + const isValid = true; + + if (!isValid) { + Alert.alert( + 'Invalid Barcode', + `Incorrect product scanned. Expected: ${currentTask?.product?.productCode}. Try again.` + ); + setProductBarcode(''); + return; + } + + // TODO: Implement the logic for proceeding to the next step + navigate('ReplenishmentPickQuantity'); + } + + return ( + // @ts-ignore + + + + + {DUMMY_REPLENISHMENT.product.productCode} + + + {`${currentTaskIndex + 1} / ${tasksCount}`} + + + + + + + + + + + + + Scan Pick Product Barcode + + Point your barcode scanner at the product barcode or type the code manually. + + + + + + ); +} diff --git a/src/screens/Replenishment/ReplenishmentStagingLocationScreen.tsx b/src/screens/Replenishment/ReplenishmentStagingLocationScreen.tsx new file mode 100644 index 00000000..5a4e4df2 --- /dev/null +++ b/src/screens/Replenishment/ReplenishmentStagingLocationScreen.tsx @@ -0,0 +1,135 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import * as React from 'react'; +import { Alert, View } from 'react-native'; +import { Divider, Paragraph, Subheading } from 'react-native-paper'; + +import { ProductDetails } from '../../components/ProductDetails'; +import { ScannerInput } from '../../components/ScannerInput'; +import { navigate } from '../../NavigationService'; +import { useReplenishmentContext } from './ReplenishmentContext'; +import styles from './styles'; + +export function ReplenishmentStagingLocationScreen() { + const { allTasks, dropCurrentTaskAtStagingLocation, resetReplenishmentSession } = useReplenishmentContext(); + + const [stagingLocationNumber, setStagingLocationNumber] = React.useState(''); + const [currentUniqueIndex, setCurrentUniqueIndex] = React.useState(0); + + const uniqueTasks = React.useMemo( + () => Array.from(new Map(allTasks.map((task) => [task.outboundContainer?.id, task])).values()), + [allTasks] + ); + + const currentTask = uniqueTasks[currentUniqueIndex]; + + if (!currentTask) { + Alert.alert('Error', 'No current replenishment task available.'); + navigate('Dashboard'); + return null; + } + + function handleSubmit() { + if (!stagingLocationNumber) { + Alert.alert('Missing Input', 'Please scan or enter a valid Staging Location ID.'); + setStagingLocationNumber(''); + return; + } + + navigate('ReplenishmentPickingLocation'); + + // TODO: Implement the staging location validation and task dropping logic + // const expected = currentTask.stagingLocation?.locationNumber; + + // if (!expected || stagingLocationNumber !== expected) { + // Alert.alert( + // 'Invalid Staging Location', + // `Expected: ${expected ?? '-'}, but got: ${stagingLocationNumber}. Please try again.` + // ); + // setStagingLocationNumber(''); + // return; + // } + + // dropCurrentTaskAtStagingLocation(currentTask, (response) => { + // if ('errorMessage' in response) { + // Alert.alert('Error', response.errorMessage); + // setStagingLocationNumber(''); + // return; + // } + + // const nextIndex = currentUniqueIndex + 1; + // if (nextIndex < uniqueTasks.length) { + // Alert.alert('Success', 'Staging Location confirmed. Proceeding to the next container.', [ + // { + // text: 'OK', + // onPress: () => { + // // Proceed to next unique task + // setCurrentUniqueIndex(nextIndex); + // setStagingLocationNumber(''); + // } + // } + // ]); + // } else { + // Alert.alert('Picking Session Complete', 'You have completed all staging confirmations.', [ + // { + // text: 'OK', + // onPress: () => { + // resetReplenishmentSession(); + // navigate('Dashboard'); + // } + // } + // ]); + // } + // }); + } + + return ( + + + + + {currentTask.product.productCode} + + + {`${currentUniqueIndex + 1} / ${uniqueTasks.length}`} + + + + + + + + + + + + + Scan Staging Location + + Point your barcode scanner at the staging location or type the ID manually. + + + + + + ); +} diff --git a/src/screens/Replenishment/mock-data.ts b/src/screens/Replenishment/mock-data.ts new file mode 100644 index 00000000..8ac18878 --- /dev/null +++ b/src/screens/Replenishment/mock-data.ts @@ -0,0 +1,32 @@ +export const DUMMY_REPLENISHMENT = { + id: 'rep-001', + product: { + id: 'prod-001', + productCode: 'PROD-001', + name: 'Sample Product' + } +}; + +export const DUMMY_REPLENISHMENT_TASKS = [ + { + id: 'task-001', + location: { + id: 'loc-001', + locationNumber: 'LOC-001', + name: 'Sample Location' + }, + quantityPicked: 0, + quantityRequired: 10, + product: { + id: 'prod-001', + productCode: 'PROD-001', + name: 'Sample Product' + }, + replenishment: DUMMY_REPLENISHMENT, + outboundContainer: { + id: 'container-001', + locationNumber: 'CONT-001', + name: 'Outbound Container 1' + } + } +]; diff --git a/src/screens/Replenishment/styles.ts b/src/screens/Replenishment/styles.ts new file mode 100644 index 00000000..ebb303f4 --- /dev/null +++ b/src/screens/Replenishment/styles.ts @@ -0,0 +1,61 @@ +import { StyleSheet } from 'react-native'; +import Theme from '../../utils/Theme'; + +export default StyleSheet.create({ + rootWrapper: { + backgroundColor: Theme.colors.surface, + padding: Theme.spacing.large, + display: 'flex', + flexDirection: 'column' + }, + wrapperWithPadding: { + padding: Theme.spacing.medium + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + chipDefault: { + height: 28, + justifyContent: 'flex-start', + borderRadius: 4, + alignItems: 'center' + }, + chipText: { + fontSize: 12, + color: Theme.colors.text + }, + + title: { + fontSize: 18, + color: Theme.colors.text, + fontWeight: 'bold' + }, + subheading: { + fontSize: 16, + color: Theme.colors.text, + fontWeight: 'bold' + }, + caption: { fontSize: 12 }, + paragraph: { + fontSize: 14, + color: Theme.colors.text, + fontWeight: 'normal' + }, + fontBold: { + fontWeight: 'bold' + }, + marginTop: { + marginTop: Theme.spacing.medium + }, + marginTopSmall: { + marginTop: Theme.spacing.small + }, + marginBottom: { + marginBottom: Theme.spacing.medium + }, + divider: { + marginVertical: Theme.spacing.small + } +}); diff --git a/src/screens/Replenishment/types.ts b/src/screens/Replenishment/types.ts new file mode 100644 index 00000000..c845512c --- /dev/null +++ b/src/screens/Replenishment/types.ts @@ -0,0 +1,49 @@ +import { Container } from '../../data/container/Shipment'; +import Location from '../../data/location/Location'; +import InventoryItem from '../../data/picklist/InventoryItem'; +import Product from '../../data/product/Product'; + +export enum ReplenishmentTaskStatus { + PENDING = 'PENDING', + PICKING = 'PICKING', + PICKED = 'PICKED', + STAGED = 'STAGED' +} + +export type ReplenishmentTask = { + id: string; + identifier: string; + + orderId: string; + orderNumber: string; + orderStatus: string; + orderType: string; + + facility?: Location; + location?: Location; + outboundContainer?: Container | null; + stagingLocation?: Location | null; + + product: Product; + inventoryItem: InventoryItem; + + quantityRequired: number; + quantityPicked: number; + + requestedBy?: string | null; + assignee?: string | null; + pickedBy?: string | null; + stagedBy?: string | null; + + priority?: number; + reasonCode?: string | null; + status: ReplenishmentTaskStatus; + + dateRequested?: string | null; + dateAssigned?: string | null; + dateStarted?: string | null; + datePicked?: string | null; + dateStaged?: string | null; + dateCreated: string; + lastUpdated: string; +};