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
4 changes: 4 additions & 0 deletions src/apis/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
15 changes: 14 additions & 1 deletion src/redux/actions/picking.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: {
Expand Down
22 changes: 22 additions & 0 deletions src/redux/sagas/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
106 changes: 82 additions & 24 deletions src/screens/Picking/PickingPickTypeScreen.tsx
Original file line number Diff line number Diff line change
@@ -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<DeliveryType | null>(DELIVERY_TYPES[0]);
const [numberOfOrdersToGroup, setNumberOfOrdersToGroup] = React.useState<string>('');
const [counts, setCounts] = React.useState<Record<string, DeliveryTypeOrderCount>>({});

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<Record<string, DeliveryTypeOrderCount>>((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
Expand All @@ -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 (
Expand All @@ -64,44 +99,67 @@ export default function PickingPickTypeScreen() {
<Paragraph>Please select the appropriate pick type and specify the number of orders to group.</Paragraph>
</View>

<View>
{DELIVERY_TYPES.map((item) => (
<View key={item.label} style={styles.cardWrapper}>
<TouchableOpacity onPress={() => setDeliveryType(item)}>
<View style={[styles.typeCard, isSelected(item) && styles.selectedCard]}>
<View style={styles.contentWrapper}>
<Subheading style={styles.cardLabel}>{item.label}</Subheading>
<Badge visible={!!item.priority} style={styles.priorityBadge}>
{item.priority ? `P${item.priority}` : ''}
</Badge>
<View style={styles.optionsCard}>
{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 (
<TouchableOpacity
key={item.label}
activeOpacity={0.7}
style={[styles.optionRow, isLast && styles.optionRowLast, selected && styles.optionRowSelected]}
onPress={() => setDeliveryType(item)}
>
<RadioButton
value={item.code}
color={Theme.colors.primary}
status={selected ? 'checked' : 'unchecked'}
onPress={() => setDeliveryType(item)}
/>

<View style={styles.optionRowContent}>
<View>
<Paragraph style={styles.optionTitle}>{item.label}</Paragraph>
{priorityLabel ? <Paragraph style={styles.optionSubtitle}>{priorityLabel}</Paragraph> : null}
</View>

{isSelected(item) && <Icon name={Name.Check} size={20} color={Theme.colors.primary} />}
{/* Count is always rendered: shows 0 until counts load */}
<View style={styles.countWrapper}>
<Paragraph style={[styles.countValue, isEmpty && styles.countValueEmpty]}>
{count ? count.availableCount : 0}
</Paragraph>
<Paragraph style={styles.countCaption}>Orders</Paragraph>
</View>
</View>
</TouchableOpacity>
</View>
))}
);
})}
</View>

<View style={styles.formWrapper}>
<PaperTextInput
autoCompleteType="off"
style={styles.marginTopSmall}
label="Number of Orders to Group"
style={[styles.marginTopSmall, styles.whiteInput]}
label="Orders to Group"
mode="outlined"
keyboardType="numeric"
value={numberOfOrdersToGroup}
onChangeText={setNumberOfOrdersToGroup}
onSubmitEditing={handleConfirmQuantity}
onSubmitEditing={handleRetrievePickTasks}
/>

<Button
mode="contained"
style={styles.marginTop}
contentStyle={styles.ctaContent}
disabled={!deliveryType || !numberOfOrdersToGroup}
onPress={handleConfirmQuantity}
onPress={handleRetrievePickTasks}
>
Confirm Quantity
Retrieve Pick Tasks
</Button>
</View>
</ScrollView>
Expand Down
8 changes: 8 additions & 0 deletions src/screens/Picking/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
1: 'Highest',
2: 'High',
3: 'Medium',
4: 'Low',
5: 'Lowest'
};
94 changes: 70 additions & 24 deletions src/screens/Picking/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
6 changes: 6 additions & 0 deletions src/types/picking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down