diff --git a/src/Main.tsx b/src/Main.tsx index ce6b1e20..fb4f55aa 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -49,12 +49,16 @@ import PutawayList from './screens/PutawayList'; import Scan from './screens/Scan'; import Settings from './screens/Settings'; import ShipItemDetails from './screens/ShipItemDetails'; +import SortationEntryScreen from './screens/Sortation/SortationEntryScreen'; +import SortationQuantityScreen from './screens/Sortation/SortationQuantityScreen'; import Transfer from './screens/Transfer'; import Transfers from './screens/Transfers'; import TransferDetails from './screens/TransfersDetails'; import ViewAvailableItem from './screens/ViewAvailableItem'; import ApiClient from './utils/ApiClient'; import Theme from './utils/Theme'; +import SortationContainerScreen from './screens/Sortation/SortationContainerScreen'; +import SortationTaskSelectionListScreen from './screens/Sortation/SortationTaskSelectionListScreen'; const Stack = createStackNavigator(); export interface OwnProps { @@ -249,6 +253,26 @@ class Main extends Component { options={{ title: 'Packing Location' }} /> + + + + diff --git a/src/apis/products.ts b/src/apis/products.ts index 6f25e2d5..c1ca5dd6 100644 --- a/src/apis/products.ts +++ b/src/apis/products.ts @@ -59,3 +59,7 @@ export function stockAdjustments(requestBody: any) { export function searchBarcode(id: string) { return apiClient.get(`/globalSearch/${id}`, {}); } + +export function getProductByBarcode(barcode: string) { + return apiClient.get(`/barcodes?id=${encodeURIComponent(barcode)}`) +} diff --git a/src/apis/putaway.ts b/src/apis/putaway.ts index 2c7fbf98..22c9807a 100644 --- a/src/apis/putaway.ts +++ b/src/apis/putaway.ts @@ -18,3 +18,11 @@ export function getCandidates(locationId: string) { export function createPutawayOder(data: any) { return apiClient.post('/putaways', data); } + +export function getPutawayTasks(facilityId: string, productId: string) { + return apiClient.get(`/facilities/${facilityId}/putaway-tasks?statusCategory=OPEN&product.id=${productId}`); +} + +export function patchPutawayTask(facilityId: string, putawayItemId: string, payload: any) { + return apiClient.patch(`/facilities/${facilityId}/putaway-tasks/${putawayItemId}`, payload); +} diff --git a/src/assets/images/icon_sortation.svg b/src/assets/images/icon_sortation.svg new file mode 100644 index 00000000..32da12a0 --- /dev/null +++ b/src/assets/images/icon_sortation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index bcd64a4b..fcb87d21 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,7 +22,7 @@ export const DEFAULT_DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = { }; export const appConfig = { - DEFAULT_DEBOUNCE_TIME: 500, + DEFAULT_DEBOUNCE_TIME: 2000, APP_HEADER_HEIGHT: 56, LOCALE: 'en-US' }; diff --git a/src/redux/actions/products.ts b/src/redux/actions/products.ts index 1e4136f0..0f9ff25a 100644 --- a/src/redux/actions/products.ts +++ b/src/redux/actions/products.ts @@ -31,6 +31,9 @@ export const STOCK_ADJUSTMENT_REQUEST_SUCCESS = 'STOCK_ADJUSTMENT_REQUEST_SUCCES export const SEARCH_BARCODE = 'SEARCH_BARCODE'; export const SEARCH_BARCODE_SUCCESS = 'SEARCH_BARCODE_SUCCESS'; + +export const GET_SORTATION_DETAILS_BY_BARCODE = 'GET_SORTATION_DETAILS_BY_BARCODE'; + export function getProductsAction(callback?: (products: any) => void) { return { type: GET_PRODUCTS_REQUEST, @@ -100,3 +103,11 @@ export function searchBarcode(id: any, callback?: (data: any) => void) { callback }; } + +export function getSortationDetailsByBarcode(barcode: string, callback: (data: any) => void) { + return { + type: GET_SORTATION_DETAILS_BY_BARCODE, + payload: { barcode }, + callback + }; +} diff --git a/src/redux/actions/putaways.ts b/src/redux/actions/putaways.ts index 8ee34773..4ef190b4 100644 --- a/src/redux/actions/putaways.ts +++ b/src/redux/actions/putaways.ts @@ -6,6 +6,8 @@ export const CREATE_PUTAWAY_ORDER_REQUEST = 'CREATE_PUTAWAY_ORDER_REQUEST'; export const CREATE_PUTAWAY_ORDER_REQUEST_SUCCESS = 'CREATE_PUTAWAY_ORDER_REQUEST_SUCCESS'; export const SUBMIT_PUTAWAY_ITEM_BIN_LOCATION = 'SUBMIT_PUTAWAY_ITEM_BIN_LOCATION'; export const SUBMIT_PUTAWAY_ITEM_BIN_LOCATION_SUCCESS = 'SUBMIT_PUTAWAY_ITEM_BIN_LOCATION_SUCCESS'; +export const PATCH_PUTAWAY_TASK_REQUEST = 'PATCH_PUTAWAY_TASK_REQUEST'; +export const PATCH_PUTAWAY_TASK_REQUEST_SUCCESS = 'PATCH_PUTAWAY_TASK_REQUEST_SUCCESS'; export function fetchPutAwayFromOrderAction(q: string | null, callback: (data: any) => void) { return { @@ -37,3 +39,16 @@ export function createPutawayOderAction(data: any, callback?: (data: any) => voi callback }; } + +export function patchPutawayTaskAction( + facilityId: string, + putawayItemId: string, + payload: any, + callback?: (data: any) => void +) { + return { + type: PATCH_PUTAWAY_TASK_REQUEST, + payload: { facilityId, putawayItemId, payload }, + callback + }; +} diff --git a/src/redux/sagas/products.ts b/src/redux/sagas/products.ts index e454d9a6..d8e9af28 100644 --- a/src/redux/sagas/products.ts +++ b/src/redux/sagas/products.ts @@ -1,9 +1,10 @@ -import { call, put, takeLatest } from 'redux-saga/effects'; +import { call, put, select, takeLatest } from 'redux-saga/effects'; import { GET_PRODUCT_BY_ID_REQUEST, GET_PRODUCT_BY_ID_REQUEST_SUCCESS, GET_PRODUCTS_REQUEST, GET_PRODUCTS_REQUEST_SUCCESS, + GET_SORTATION_DETAILS_BY_BARCODE, PRINT_LABEL_REQUEST, PRINT_LABEL_REQUEST_SUCCESS, SEARCH_BARCODE, @@ -22,6 +23,7 @@ import { import * as api from '../../apis'; import { hideScreenLoading, showScreenLoading } from '../actions/main'; +import { userLocation } from '../selectors/auth'; function* getProducts(action: any) { try { @@ -67,10 +69,7 @@ function* searchProductsByName(action: any) { function* searchProductByCode(action: any) { try { yield showScreenLoading('Please wait...'); - const data = yield call( - api.searchProductByCode, - action.payload.productCode - ); + const data = yield call(api.searchProductByCode, action.payload.productCode); yield put({ type: SEARCH_PRODUCT_BY_CODE_REQUEST_SUCCESS, payload: data @@ -110,10 +109,7 @@ function* searchProductGlobally(action: any) { function* searchProductsByCategory(action: any) { try { yield showScreenLoading('Searching..'); - const data = yield call( - api.searchProductsByCategory, - action.payload.category - ); + const data = yield call(api.searchProductsByCategory, action.payload.category); yield put({ type: SEARCH_PRODUCTS_BY_CATEGORY_REQUEST_SUCCESS, payload: data @@ -213,17 +209,51 @@ 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 location = yield select(userLocation); + if (!location || !location.id) { + return; + } + + const tasksResponse: any = yield call(api.getPutawayTasks, location.id, product.id); + const tasks = tasksResponse?.data ?? []; + + if (!tasks || tasks.length === 0) { + yield action.callback({ + error: true, + errorMessage: 'Product found but there is not putaway task for it' + }); + return; + } + + yield action.callback({ product, tasks }); + } catch (error: any) { + if (error.code != 401) { + yield 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); yield takeLatest(SEARCH_PRODUCT_BY_CODE_REQUEST, searchProductByCode); yield takeLatest(SEARCH_PRODUCT_GLOBALY_REQUEST, searchProductGlobally); - yield takeLatest( - SEARCH_PRODUCTS_BY_CATEGORY_REQUEST, - searchProductsByCategory - ); + yield takeLatest(SEARCH_PRODUCTS_BY_CATEGORY_REQUEST, searchProductsByCategory); yield takeLatest(GET_PRODUCT_BY_ID_REQUEST, getProductById); yield takeLatest(PRINT_LABEL_REQUEST, printLabel); yield takeLatest(STOCK_ADJUSTMENT_REQUEST, stockAdjustments); yield takeLatest(SEARCH_BARCODE, searchBarcode); + yield takeLatest(GET_SORTATION_DETAILS_BY_BARCODE, getSortationDetailsSaga); } diff --git a/src/redux/sagas/putaway.ts b/src/redux/sagas/putaway.ts index a29e80f8..7eb04833 100644 --- a/src/redux/sagas/putaway.ts +++ b/src/redux/sagas/putaway.ts @@ -6,24 +6,20 @@ import { FETCH_PUTAWAY_FROM_ORDER_REQUEST_SUCCESS, GET_PUTAWAY_CANDIDATES_REQUEST, GET_PUTAWAY_CANDIDATES_REQUEST_SUCCESS, + PATCH_PUTAWAY_TASK_REQUEST, + PATCH_PUTAWAY_TASK_REQUEST_SUCCESS, SUBMIT_PUTAWAY_ITEM_BIN_LOCATION, SUBMIT_PUTAWAY_ITEM_BIN_LOCATION_SUCCESS } from '../actions/putaways'; import { hideScreenLoading, showScreenLoading } from '../actions/main'; import * as api from '../../apis'; -import { - GetPutAwaysApiResponse, - PostPutAwayItemApiResponse -} from '../../data/putaway/PutAway'; +import { GetPutAwaysApiResponse, PostPutAwayItemApiResponse } from '../../data/putaway/PutAway'; import * as Sentry from '@sentry/react-native'; function* fetchPutAwayFromOrder(action: any) { try { yield put(showScreenLoading('Loading..')); - const response: GetPutAwaysApiResponse = yield call( - api.fetchPutAwayFromOrder, - action.payload.q - ); + const response: GetPutAwaysApiResponse = yield call(api.fetchPutAwayFromOrder, action.payload.q); yield put({ type: FETCH_PUTAWAY_FROM_ORDER_REQUEST_SUCCESS, payload: response.data @@ -94,8 +90,28 @@ function* createPutawayOder(action: any) { } catch (error) { yield put(hideScreenLoading()); yield action.callback({ - errorMessage: error.message, - error: true + error: true, + errorMessage: error.message + }); + } +} + +function* patchPutawayTask(action: any) { + try { + yield put(showScreenLoading('Submitting...')); + const { facilityId, putawayItemId, payload } = action.payload; + const response = yield call(api.patchPutawayTask, facilityId, putawayItemId, payload); + yield put({ + type: PATCH_PUTAWAY_TASK_REQUEST_SUCCESS, + payload: response.data + }); + yield put(hideScreenLoading()); + yield action.callback({ success: true, data: response.data }); + } catch (error) { + yield put(hideScreenLoading()); + yield action.callback({ + error: true, + errorMessage: error.message }); } } @@ -105,4 +121,5 @@ export default function* watcher() { yield takeLatest(GET_PUTAWAY_CANDIDATES_REQUEST, getCandidates); yield takeLatest(CREATE_PUTAWAY_ORDER_REQUEST, createPutawayOder); yield takeLatest(SUBMIT_PUTAWAY_ITEM_BIN_LOCATION, submitPutawayItem); + yield takeLatest(PATCH_PUTAWAY_TASK_REQUEST, patchPutawayTask); } diff --git a/src/redux/selectors/auth.ts b/src/redux/selectors/auth.ts new file mode 100644 index 00000000..6be68158 --- /dev/null +++ b/src/redux/selectors/auth.ts @@ -0,0 +1 @@ +export const userLocation = (state: any) => state.mainReducer.currentLocation \ No newline at end of file diff --git a/src/screens/Dashboard/dashboardData.ts b/src/screens/Dashboard/dashboardData.ts index df2ece7e..0db973d1 100644 --- a/src/screens/Dashboard/dashboardData.ts +++ b/src/screens/Dashboard/dashboardData.ts @@ -9,6 +9,7 @@ import IconProducts from '../../assets/images/icon_products.svg'; import IconPutawayCandidates from '../../assets/images/icon_putaway_candidates.svg'; import IconReceiving from '../../assets/images/icon_receiving.svg'; import IconScan from '../../assets/images/icon_scan.svg'; +import IconSortation from '../../assets/images/icon_sortation.svg'; export type DashboardEntry = { key: string; @@ -20,6 +21,13 @@ export type DashboardEntry = { }; const dashboardEntries: DashboardEntry[] = [ + { + key: 'sortation', + screenName: 'Sortation', + entryDescription: 'Manage sortation tasks and workflows', + icon: IconSortation, + navigationScreenName: 'Sortation' + }, { key: 'picking', screenName: 'Picking', diff --git a/src/screens/Sortation/SortationContainerScreen.tsx b/src/screens/Sortation/SortationContainerScreen.tsx new file mode 100644 index 00000000..511bb5e0 --- /dev/null +++ b/src/screens/Sortation/SortationContainerScreen.tsx @@ -0,0 +1,143 @@ +import { RouteProp, useIsFocused, useRoute } from '@react-navigation/native'; +import React, { useEffect, useRef, useState } from 'react'; +import { Alert, TextInput, View } from 'react-native'; +import { Divider, TextInput as PaperTextInput, Paragraph, Subheading } from 'react-native-paper'; +import { useDispatch } from 'react-redux'; + +import Button from '../../components/Button'; +import EmptyView from '../../components/EmptyView'; +import { navigate } from '../../NavigationService'; +import { patchPutawayTaskAction } from '../../redux/actions/putaways'; +import SortationProductDetails, { DetailChip } from './SortationProductDetails'; +import styles from './styles'; +import { SortationProduct, SortationTask } from './types'; + +type ContainerRouteProp = RouteProp< + { SortationQuantity: { product: SortationProduct; quantitySorted: number; task: SortationTask } }, + 'SortationQuantity' +>; + +export default function SortationContainerScreen() { + const { params } = useRoute(); + const { product, quantitySorted, task } = params; + + const inputRef = useRef(null); + const isFocused = useIsFocused(); + const [putawayContainerBarcode, setPutawayContainerBarcode] = useState(''); + const dispatch = useDispatch(); + + useEffect(() => { + if (!isFocused) { + return; + } + const t = setTimeout(() => inputRef.current?.focus(), 100); + return () => clearTimeout(t); + }, [isFocused]); + + if (!product) { + return ( + + navigate('Sortation')} + /> + + ); + } + + if (!quantitySorted || quantitySorted <= 0) { + return ( + + navigate('SortationQuantity', { product })} + /> + + ); + } + + function handleSubmit() { + if (!putawayContainerBarcode) { + Alert.alert('Empty Barcode', 'You must scan a putaway container barcode to proceed.'); + return; + } + + const locationNumber = task?.destination?.locationNumber; + if (putawayContainerBarcode !== locationNumber) { + Alert.alert( + 'Wrong location number', + `Scanned location number: ${putawayContainerBarcode} is different from the expected one: ${locationNumber}` + ); + return; + } + + const payload = { + action: 'complete', + putawayContainerId: putawayContainerBarcode || null + }; + + dispatch( + patchPutawayTaskAction(task.facility.id, task.id, payload, (response) => { + if (response && !response.error) { + Alert.alert('Sortation Successful', 'The product has been sorted successfully.'); + navigate('Sortation'); + } else { + Alert.alert('Sortation Failed', response.errorMessage || 'Sortation Failed'); + } + }) + ); + } + + const productDetailsChips: DetailChip[] = [ + { + icon: 'package', + label: 'Quantity Sorted', + value: quantitySorted + }, + { + icon: 'map-search', + label: 'Putaway Zone', + value: task?.destination?.zoneName + }, + { + icon: 'map-marker', + label: 'Final Storage Location', + value: task?.destination?.name + } + ]; + + return ( + + + + + + + Scan Putaway Container ID + + Scan the barcode of the putaway container where you want to place this product. + + + + + + + + ); +} diff --git a/src/screens/Sortation/SortationEntryScreen.tsx b/src/screens/Sortation/SortationEntryScreen.tsx new file mode 100644 index 00000000..f9555bdd --- /dev/null +++ b/src/screens/Sortation/SortationEntryScreen.tsx @@ -0,0 +1,95 @@ +import { useIsFocused } from '@react-navigation/native'; +import debounce from 'lodash/debounce'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Alert, TextInput, View } from 'react-native'; +import { TextInput as PaperTextInput, Paragraph, Title } from 'react-native-paper'; +import { useDispatch } from 'react-redux'; + +import { appConfig } from '../../constants'; +import { navigate } from '../../NavigationService'; +import { getSortationDetailsByBarcode } from '../../redux/actions/products'; +import styles from './styles'; + +export default function SortationEntryScreen() { + const [barcode, setBarcode] = useState(''); + const isFocused = useIsFocused(); + const inputRef = useRef(null); + const dispatch = useDispatch(); + + useEffect(() => { + if (!isFocused) { + return; + } + const t = setTimeout(() => inputRef.current?.focus(), 100); + return () => clearTimeout(t); + }, [isFocused]); + + const performScan = useCallback( + (raw: string) => { + const code = raw.trim(); + if (!code) { + Alert.alert('Empty Barcode', 'You must scan a barcode or enter a code manually to proceed.'); + return; + } + + dispatch( + getSortationDetailsByBarcode(code, (response) => { + if (response && !response.error) { + const { product, tasks } = response || {}; + + if (tasks?.length === 1) { + navigate('SortationQuantity', { product, task: tasks[0] }); + return; + } + + navigate('SortationTaskList', { product, tasks }); + } else { + Alert.alert( + 'Sortation Failed', + response?.errorMessage || 'Could not find a product with the scanned barcode.' + ); + } + }) + ); + }, + [dispatch] + ); + + const debouncedScan = useMemo(() => debounce(performScan, appConfig.DEFAULT_DEBOUNCE_TIME), [performScan]); + + useEffect(() => { + return () => { + debouncedScan.cancel(); + }; + }, [debouncedScan]); + + const handleChange = (text: string) => { + setBarcode(text); + debouncedScan(text); + }; + + const handleSubmit = () => { + performScan(barcode); + }; + + return ( + + Scan Product Barcode For Sortation + + Point your barcode scanner at the product or type the code manually, then wait a moment for it to auto‐submit. + + + + + ); +} diff --git a/src/screens/Sortation/SortationProductDetails.tsx b/src/screens/Sortation/SortationProductDetails.tsx new file mode 100644 index 00000000..486a3246 --- /dev/null +++ b/src/screens/Sortation/SortationProductDetails.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { View } from 'react-native'; +import { Caption, Chip, Divider, Paragraph, Switch, Title } from 'react-native-paper'; + +import { HYPHEN } from '../../constants'; +import styles from './styles'; +import { SortationProduct } from './types'; +import Theme from '../../utils/Theme'; + +export type DetailChip = { + icon: string; + label: string; + value: string | null | number | undefined; +}; + +export type SortationProductDetailsProps = { + product: SortationProduct; + detailsChips: DetailChip[]; + showDirectPutawayRequired?: boolean; + directPutawayRequired?: boolean; + onToggleDirectPutaway?: (value: boolean) => void; +}; + +export default function SortationProductDetails({ + product, + detailsChips, + showDirectPutawayRequired = false, + directPutawayRequired = false, + onToggleDirectPutaway +}: SortationProductDetailsProps) { + const { productCode, name, description } = product; + + return ( + + + + {productCode} + + + + + + {name} + {description} + + {detailsChips.map(({ icon, value, label }) => ( + + {`${label}: ${value ?? HYPHEN}`} + + ))} + + {showDirectPutawayRequired && ( + <> + + + Direct Putaway Required + onToggleDirectPutaway?.(val)} + /> + + + )} + + ); +} diff --git a/src/screens/Sortation/SortationQuantityScreen.tsx b/src/screens/Sortation/SortationQuantityScreen.tsx new file mode 100644 index 00000000..0f467357 --- /dev/null +++ b/src/screens/Sortation/SortationQuantityScreen.tsx @@ -0,0 +1,129 @@ +import { RouteProp, useIsFocused, useRoute } from '@react-navigation/native'; +import React, { useEffect, useRef, useState } from 'react'; +import { Alert, TextInput, View } from 'react-native'; +import { Divider, TextInput as PaperTextInput, Paragraph, Subheading } from 'react-native-paper'; + +import Button from '../../components/Button'; +import EmptyView from '../../components/EmptyView'; +import { navigate } from '../../NavigationService'; +import SortationProductDetails, { DetailChip } from './SortationProductDetails'; +import styles from './styles'; +import { SortationProduct, SortationTask } from './types'; + +type QuantityRouteProp = RouteProp< + { SortationQuantity: { product: SortationProduct; task: SortationTask } }, + 'SortationQuantity' +>; + +export default function SortationQuantityScreen() { + const { params } = useRoute(); + const { product, task } = params; + + const inputRef = useRef(null); + const isFocused = useIsFocused(); + + const [quantitySorted, setQuantitySorted] = useState(); + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const [directPutawayRequired, setDirectPutawayRequired] = useState(true); + + useEffect(() => { + if (!isFocused) { + return; + } + const t = setTimeout(() => inputRef.current?.focus(), 100); + return () => clearTimeout(t); + }, [isFocused]); + + if (!product) { + return ( + + + + ); + } + + function handleChange(text: string) { + // Strip non-numeric characters and convert to number + const digitsOnly = text.replace(/[^0-9]/g, ''); + const num = digitsOnly.length > 0 ? parseInt(digitsOnly, 10) : undefined; + setQuantitySorted(num); + } + + function handleSubmit() { + if (!quantitySorted || quantitySorted <= 0) { + Alert.alert('Invalid Quantity', 'Please enter a valid quantity greater than zero.'); + return; + } + + if (quantitySorted > task.quantity) { + Alert.alert('Invalid Quantity', 'Quantity to sort can not be greater than quantity in total'); + return; + } + + navigate('SortationContainer', { + product, + quantitySorted, + task + }); + } + + const productDetailsChips: DetailChip[] = [ + { + icon: 'package', + label: 'Quantity Required', + value: task?.quantity + }, + { + icon: 'map-search', + label: 'Putaway Zone', + value: task?.destination?.zoneName + }, + { + icon: 'map-marker', + label: 'Final Storage Location', + value: task?.destination?.name + } + ]; + + return ( + + + + + + + Enter Quantity for Sortation + + Enter the quantity of this product that you want to sort. This will be used to update the inventory records. + + + + + + + + ); +} diff --git a/src/screens/Sortation/SortationTaskSelectionListScreen.tsx b/src/screens/Sortation/SortationTaskSelectionListScreen.tsx new file mode 100644 index 00000000..6b9c6f58 --- /dev/null +++ b/src/screens/Sortation/SortationTaskSelectionListScreen.tsx @@ -0,0 +1,93 @@ +import { RouteProp, useRoute } from '@react-navigation/native'; +import React, { useState } from 'react'; +import { Alert, FlatList, View } from 'react-native'; +import { Button, Caption, Card, Chip, Divider, Paragraph, Text, Title } from 'react-native-paper'; + +import { HYPHEN } from '../../constants'; +import styles from './styles'; +import { SortationProduct, SortationTask } from './types'; +import { navigate } from '../../NavigationService'; + +type TaskSelectionRouteProp = RouteProp< + { SortationQuantity: { product: SortationProduct; tasks: SortationTask[] } }, + 'SortationQuantity' +>; + +export default function SortationTaskSelectionListScreen() { + const { params } = useRoute(); + const { product, tasks } = params; + + const [selectedTask, setSelectedTask] = useState(null); + + const onContinue = () => { + if (!selectedTask) { + Alert.alert('Validation Error', 'Please select a task to continue.'); + return; + } + + if (!selectedTask.destination?.id) { + Alert.alert('Validation Error', 'Selected task does not have a valid destination location.'); + return; + } + + navigate('SortationQuantity', { product, task: selectedTask }); + }; + + return ( + + {`Multiple Putaways (${tasks.length})`} + + This{' '} + + {product.productCode} - {product.name} + {' '} + product is linked to more than one putaway. Choose the one you’d like to work with from the list below. + + + + + {/* Task List */} + item.id} + renderItem={({ item }) => { + const isSelected = selectedTask?.id === item.id; + return ( + setSelectedTask(item)}> + + + + {`${item.putaway?.putawayNumber ?? HYPHEN}`} + + + + {`${item.status ?? HYPHEN}`} + + + + + + {`Location Name: ${item.location.name}`} + {`Location Number: ${item.location.locationNumber}`} + + {`Quantity: ${item.quantity ?? HYPHEN}`} + + + {`Final Storage Location: ${item.destination?.name ?? HYPHEN}`} + + + + ); + }} + /> + + + + + + + + ); +} diff --git a/src/screens/Sortation/styles.ts b/src/screens/Sortation/styles.ts new file mode 100644 index 00000000..1f251557 --- /dev/null +++ b/src/screens/Sortation/styles.ts @@ -0,0 +1,101 @@ +import { StyleSheet } from 'react-native'; +import Theme from '../../utils/Theme'; + +export default StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: '#FFFFFF', + padding: Theme.spacing.large, + justifyContent: 'flex-start' + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + contentContainer: { + display: 'flex', + flex: 1, + flexDirection: 'column' + }, + 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' + }, + chipSuccess: { + backgroundColor: Theme.colors.success, + height: 28, + justifyContent: 'flex-start', + borderRadius: 4, + alignItems: 'center' + }, + chipText: { + fontSize: 12, + color: Theme.colors.text + }, + contentDivider: { + marginVertical: 8 + }, + title: { + fontSize: 18, + color: Theme.colors.text, + fontWeight: 'bold' + }, + subheading: { + fontSize: 16, + color: Theme.colors.text, + fontWeight: 'bold' + }, + caption: { fontSize: 12 }, + bold: { fontWeight: 'bold' }, + paragraph: { + fontSize: 14, + color: Theme.colors.text, + fontWeight: 'normal' + }, + topSpace: { marginTop: Theme.spacing.small }, + bottomSpace: { marginBottom: Theme.spacing.small }, + cardAnnotation: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center' + }, + formContainer: { + display: 'flex', + flex: 1, + flexDirection: 'column', + padding: Theme.spacing.large + }, + card: { + marginTop: Theme.spacing.small, + marginBottom: Theme.spacing.small, + borderRadius: Theme.roundness * 2, + borderColor: Theme.colors.disabled, + borderWidth: 0.5 + }, + cardContent: { + paddingVertical: Theme.spacing.medium, + paddingHorizontal: Theme.spacing.large + }, + cardSelected: { + borderColor: Theme.colors.primary, + backgroundColor: Theme.colors.surface, + borderWidth: 2 + }, + cardContainer: { + paddingVertical: Theme.spacing.large + } +}); diff --git a/src/screens/Sortation/types.ts b/src/screens/Sortation/types.ts new file mode 100644 index 00000000..ac4d41b6 --- /dev/null +++ b/src/screens/Sortation/types.ts @@ -0,0 +1,78 @@ +/* eslint-disable no-undef */ +import { Container } from '../../data/container/Shipment'; +import LocationType from '../../data/location/LocationType'; +import PutAwayItems from '../../data/putaway/PutAwayItems'; + +export type SortationProduct = { + active: boolean; + category: string; + color: string | null; + dateCreated: string; + description: string; + displayNames: { + default: string | null; + }; + handlingIcons: unknown[]; + id: string; + lastUpdated: string; + lotAndExpiryControl: boolean; + name: string; + pricePerUnit: number; + productCode: string; + unitOfMeasure: string; + updatedBy: string; +}; + +export type SortationFacility = { + active: boolean; + id: string; + locationNumber: string; + locationType: LocationType; + locationTypeCode: string; + name: string; +}; + +export type SortationLocation = { + active: boolean; + id: string; + locationNumber: string; + locationType: LocationType; + locationTypeCode: string; + name: string; + zoneId: string | null; + zoneName: string | null; +}; + +export type SortationInventoryItem = { + expirationDate: string; + id: string; + lotNumber: string; + product: SortationProduct; +}; + +export type SortationPutaway = { + dateCreated: string; + destination: unknown; + errors: unknown; + id: string; + orderedBy: unknown; + origin: unknown; + putawayAssignee: unknown; + putawayDate: string | null; + putawayItems: PutAwayItems[]; + putawayNumber: string; + putawayStatus: string; + sortBy: string | null; +}; + +export type SortationTask = { + container: Container; + destination: SortationLocation; + facility: SortationFacility; + id: string; + inventoryItem: SortationInventoryItem; + location: SortationLocation; + putaway: SortationPutaway | null; + quantity: number; + status: string; +}; diff --git a/src/utils/ApiClient.ts b/src/utils/ApiClient.ts index 6cdb91fe..6a85b4b9 100644 --- a/src/utils/ApiClient.ts +++ b/src/utils/ApiClient.ts @@ -34,6 +34,10 @@ class _ApiClient { return await this.client.delete(endpoint, config); } + async patch(endpoint: string, config = this.client.defaults) { + return await this.client.patch(endpoint, config); + } + handleApiSuccess = (response: AxiosResponse) => { const responseBody: string = JSON.stringify(response.data); return JSON.parse(responseBody);