diff --git a/src/apis/picking.ts b/src/apis/picking.ts index 719dd7b1..274f490b 100644 --- a/src/apis/picking.ts +++ b/src/apis/picking.ts @@ -43,6 +43,10 @@ export function getPickTasksApi(facilityId: string, params: PickTaskParams) { ); } +export function getPickTaskCountsApi(facilityId: string) { + return ApiClient.get(`/facilities/${facilityId}/pick-tasks/counts`); +} + export function patchPickTaskApi(facilityId: string, taskId: string, params: PickTaskActionParams) { return ApiClient.patch(`/facilities/${facilityId}/pick-tasks/${taskId}`, params); } diff --git a/src/redux/actions/picking.ts b/src/redux/actions/picking.ts index f36aa7ad..e3d76b12 100644 --- a/src/redux/actions/picking.ts +++ b/src/redux/actions/picking.ts @@ -1,10 +1,14 @@ import { PickTaskParams } from '../../apis'; -import { PickPageItem, PickTask, ReallocatePicklistItem } from '../../types/picking'; +import { DeliveryTypeOrderCount, PickPageItem, PickTask, ReallocatePicklistItem } from '../../types/picking'; export const GET_PICK_TASKS_REQUEST = 'GET_PICK_TASKS_REQUEST'; export const GET_PICK_TASKS_REQUEST_SUCCESS = 'GET_PICK_TASKS_REQUEST_SUCCESS'; export const GET_PICK_TASKS_REQUEST_FAIL = 'GET_PICK_TASKS_REQUEST_FAIL'; +export const GET_PICK_TASK_COUNTS_REQUEST = 'GET_PICK_TASK_COUNTS_REQUEST'; +export const GET_PICK_TASK_COUNTS_REQUEST_SUCCESS = 'GET_PICK_TASK_COUNTS_REQUEST_SUCCESS'; +export const GET_PICK_TASK_COUNTS_REQUEST_FAIL = 'GET_PICK_TASK_COUNTS_REQUEST_FAIL'; + export const START_PICK_TASK_REQUEST = 'START_PICK_TASK_REQUEST'; export const START_PICK_TASK_REQUEST_SUCCESS = 'START_PICK_TASK_REQUEST_SUCCESS'; export const START_PICK_TASK_REQUEST_FAIL = 'START_PICK_TASK_REQUEST_FAIL'; @@ -62,6 +66,15 @@ export function getPickTasksAction( }; } +export function getPickTaskCountsAction( + callback: (response: { response?: { data: DeliveryTypeOrderCount[] }; errorMessage?: string }) => void +) { + return { + type: GET_PICK_TASK_COUNTS_REQUEST, + callback + }; +} + export function getPickTasksByRequisitionAction( requisitionId: string, callback: (response: { diff --git a/src/redux/sagas/picking.ts b/src/redux/sagas/picking.ts index 266fff87..3509e190 100644 --- a/src/redux/sagas/picking.ts +++ b/src/redux/sagas/picking.ts @@ -15,6 +15,9 @@ import { GET_PICK_TASKS_BY_REQUISITION_REQUEST, GET_PICK_TASKS_BY_REQUISITION_REQUEST_SUCCESS, GET_PICK_TASKS_BY_REQUISITION_REQUEST_FAIL, + GET_PICK_TASK_COUNTS_REQUEST, + GET_PICK_TASK_COUNTS_REQUEST_FAIL, + GET_PICK_TASK_COUNTS_REQUEST_SUCCESS, GET_PICK_TASKS_REQUEST, GET_PICK_TASKS_REQUEST_FAIL, GET_PICK_TASKS_REQUEST_SUCCESS, @@ -58,6 +61,24 @@ function* getPickTasksAction(action: any) { } } +function* getPickTaskCountsAction(action: any) { + try { + // @ts-ignore + const currentLocation = yield select(userLocation); + if (!currentLocation) { + throw new Error('User Location Not Found'); + } + // @ts-ignore + const response = yield call(api.getPickTaskCountsApi, currentLocation.id); + yield action.callback({ response }); + yield put({ type: GET_PICK_TASK_COUNTS_REQUEST_SUCCESS, payload: response.data }); + } catch (error) { + const errorMessage = (error as any)?.message || 'Error Fetching Pick Task Counts'; + yield put({ type: GET_PICK_TASK_COUNTS_REQUEST_FAIL, payload: errorMessage }); + yield action.callback({ errorMessage }); + } +} + function* startPickTaskAction(action: any) { try { // @ts-ignore @@ -325,6 +346,7 @@ function* reallocatePickTaskAction(action: any) { export default function* watcher() { yield takeLatest(GET_PICK_TASKS_REQUEST, getPickTasksAction); + yield takeLatest(GET_PICK_TASK_COUNTS_REQUEST, getPickTaskCountsAction); yield takeLatest(START_PICK_TASK_REQUEST, startPickTaskAction); yield takeLatest(PICK_PICK_TASK_REQUEST, pickPickTaskAction); yield takeLatest(DROP_PICK_TASK_REQUEST, dropPickTaskAction); diff --git a/src/screens/Picking/PickingPickTypeScreen.tsx b/src/screens/Picking/PickingPickTypeScreen.tsx index 3580a523..cef0d93b 100644 --- a/src/screens/Picking/PickingPickTypeScreen.tsx +++ b/src/screens/Picking/PickingPickTypeScreen.tsx @@ -1,29 +1,57 @@ +import { useIsFocused } from '@react-navigation/native'; import * as React from 'react'; import { Alert, ScrollView, TouchableOpacity, View } from 'react-native'; -import { Badge, Button, TextInput as PaperTextInput, Paragraph, Subheading, Title } from 'react-native-paper'; -import Icon, { Name } from '../../components/Icon'; +import { Button, RadioButton, TextInput as PaperTextInput, Paragraph, Title } from 'react-native-paper'; +import { useDispatch } from 'react-redux'; + import { navigate } from '../../NavigationService'; -import { DeliveryType } from '../../types/picking'; +import { getPickTaskCountsAction } from '../../redux/actions/picking'; +import { DeliveryType, DeliveryTypeCode, DeliveryTypeOrderCount } from '../../types/picking'; import Theme from '../../utils/Theme'; -import { DELIVERY_TYPES } from './constants'; +import { DELIVERY_TYPES, PRIORITY_LABELS } from './constants'; import { usePickingContext } from './PickingContext'; import styles from './styles'; const NUMBER_OF_ORDERS_THRESHOLD = 10; export default function PickingPickTypeScreen() { + const dispatch = useDispatch(); + const isFocused = useIsFocused(); const { startSession, currentTaskIndex } = usePickingContext(); const [deliveryType, setDeliveryType] = React.useState(DELIVERY_TYPES[0]); const [numberOfOrdersToGroup, setNumberOfOrdersToGroup] = React.useState(''); + const [counts, setCounts] = React.useState>({}); + + const fetchCounts = React.useCallback(() => { + dispatch( + getPickTaskCountsAction(({ response, errorMessage }) => { + if (errorMessage || !response?.data) { + Alert.alert('Error', 'Failed to load pick task counts. Please try again.'); + return; + } + const byCode = response.data.reduce>((acc, item) => { + acc[item.deliveryTypeCode] = item; + return acc; + }, {}); + setCounts(byCode); + }) + ); + }, [dispatch]); React.useEffect(() => { setDeliveryType(DELIVERY_TYPES[0]); setNumberOfOrdersToGroup(''); }, [currentTaskIndex]); + React.useEffect(() => { + if (isFocused) { + fetchCounts(); + } + }, [isFocused, fetchCounts]); + const isSelected = (type: DeliveryType) => deliveryType?.label === type.label; - async function handleConfirmQuantity() { + async function handleRetrievePickTasks() { const ordersCount = Number(numberOfOrdersToGroup); // Validate that input is numeric and an integer within range @@ -50,7 +78,14 @@ export default function PickingPickTypeScreen() { // and returns whether the session was started successfully. const isValid = await startSession(deliveryType, ordersCount); - isValid && navigate('PickingPickLocation'); + if (!isValid) { + // Retrieval failed (e.g. no tasks found) — the screen stays focused, so + // refresh the counts here to correct any stale numbers + fetchCounts(); + return; + } + + navigate('PickingPickLocation'); } return ( @@ -64,44 +99,67 @@ export default function PickingPickTypeScreen() { Please select the appropriate pick type and specify the number of orders to group. - - {DELIVERY_TYPES.map((item) => ( - - setDeliveryType(item)}> - - - {item.label} - - {item.priority ? `P${item.priority}` : ''} - + + {DELIVERY_TYPES.map((item, index) => { + const selected = isSelected(item); + const count = counts[item.code]; + const isEmpty = !count || count.availableCount === 0; + const priorityLabel = item.code === DeliveryTypeCode.DEFAULT ? undefined : PRIORITY_LABELS[item.priority]; + const isLast = index === DELIVERY_TYPES.length - 1; + + return ( + setDeliveryType(item)} + > + setDeliveryType(item)} + /> + + + + {item.label} + {priorityLabel ? {priorityLabel} : null} - {isSelected(item) && } + {/* Count is always rendered: shows 0 until counts load */} + + + {count ? count.availableCount : 0} + + Orders + - - ))} + ); + })} diff --git a/src/screens/Picking/constants.ts b/src/screens/Picking/constants.ts index 7aa50263..b2462f5b 100644 --- a/src/screens/Picking/constants.ts +++ b/src/screens/Picking/constants.ts @@ -8,3 +8,11 @@ export const DELIVERY_TYPES: DeliveryType[] = [ { priority: 4, label: 'Ship To', code: DeliveryTypeCode.SHIP_TO }, { priority: 5, label: 'System Directed', code: DeliveryTypeCode.DEFAULT } ]; + +export const PRIORITY_LABELS: Record = { + 1: 'Highest', + 2: 'High', + 3: 'Medium', + 4: 'Low', + 5: 'Lowest' +}; diff --git a/src/screens/Picking/styles.ts b/src/screens/Picking/styles.ts index f2e2844d..4cc9b69a 100644 --- a/src/screens/Picking/styles.ts +++ b/src/screens/Picking/styles.ts @@ -16,39 +16,85 @@ export default StyleSheet.create({ paddingHorizontal: Theme.spacing.medium }, - cardWrapper: { - flex: 1, - paddingHorizontal: Theme.spacing.medium, - marginBottom: Theme.spacing.small / 2 + optionsCard: { + borderWidth: 1, + borderColor: '#E4E7EC', + borderRadius: Theme.roundness * 2, + overflow: 'hidden', + marginHorizontal: Theme.spacing.medium, + backgroundColor: 'white', + elevation: 2, + shadowColor: '#101828', + shadowOpacity: 0.06, + shadowRadius: 4, + shadowOffset: { width: 0, height: 2 } }, - typeCard: { + optionRow: { flexDirection: 'row', alignItems: 'center', - padding: Theme.spacing.small, - borderRadius: Theme.roundness, - backgroundColor: Theme.colors.surface, - elevation: 2 + paddingVertical: Theme.spacing.medium, + paddingHorizontal: Theme.spacing.small, + backgroundColor: 'white', + borderBottomWidth: 1, + borderBottomColor: '#EEF0F3', + borderLeftWidth: 3, + borderLeftColor: 'transparent' + }, + optionRowLast: { + borderBottomWidth: 0 }, - selectedCard: { - borderColor: Theme.colors.primary, - borderWidth: 2 + optionRowSelected: { + backgroundColor: '#E8F0FE', + borderLeftColor: Theme.colors.primary }, - contentWrapper: { + optionRowContent: { flex: 1, flexDirection: 'row', - alignItems: 'center' + justifyContent: 'space-between', + alignItems: 'center', + marginLeft: Theme.spacing.small }, - cardLabel: { - fontSize: 16, - fontWeight: '700', - color: Theme.colors.text + optionTitle: { + fontSize: 15, + letterSpacing: 0.1, + color: Theme.colors.text, + marginVertical: 0, + lineHeight: 20 + }, + optionSubtitle: { + fontSize: 13, + color: Theme.colors.disabled, + marginVertical: 0, + lineHeight: 16 + }, + countWrapper: { + alignItems: 'center', + paddingRight: Theme.spacing.small / 2 + }, + countValue: { + fontSize: 15, + fontWeight: 'bold', + fontVariant: ['tabular-nums'], + color: Theme.colors.primary, + marginVertical: 0, + lineHeight: 18 + }, + countValueEmpty: { + fontWeight: 'normal', + color: Theme.colors.secondaryForeground + }, + countCaption: { + fontSize: 11, + color: Theme.colors.disabled, + marginVertical: 0, + lineHeight: 13 + }, + + whiteInput: { + backgroundColor: 'white' }, - priorityBadge: { - marginLeft: Theme.spacing.small, - backgroundColor: Theme.colors.primary, - alignSelf: 'center', - paddingHorizontal: Theme.spacing.medium, - fontSize: 14 + ctaContent: { + height: 48 }, title: { diff --git a/src/types/picking.ts b/src/types/picking.ts index 69291946..65d7bb5c 100644 --- a/src/types/picking.ts +++ b/src/types/picking.ts @@ -72,6 +72,12 @@ export type DeliveryType = { code: DeliveryTypeCode; }; +export type DeliveryTypeOrderCount = { + deliveryTypeCode: DeliveryTypeCode; + availableCount: number; + totalCount: number; +}; + export type AvailableItem = { 'inventoryItem.id': string | null; 'product.name': string | null;