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
23 changes: 16 additions & 7 deletions src/screens/Picking/DiscretePickingListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function DiscretePickingListScreen() {
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [isPullRefreshing, setIsPullRefreshing] = useState<boolean>(false);
const [hasLoaded, setHasLoaded] = useState<boolean>(false);
const [isStartingOrder, setIsStartingOrder] = useState<boolean>(false);

const fetchOrders = useCallback(
(excludeAssignedRequisitionsParam: boolean, fromPull = false) => {
Expand All @@ -61,6 +62,7 @@ export default function DiscretePickingListScreen() {

useFocusEffect(
useCallback(() => {
setIsStartingOrder(false);
fetchOrders(excludeAssignedRequisitions);
}, [fetchOrders, excludeAssignedRequisitions])
);
Expand All @@ -75,14 +77,19 @@ export default function DiscretePickingListScreen() {
);

const handleOrderPress = (order: DiscretePickingOrder) => {
// startOrderSession shows a full-screen loader, so suppress the search bar spinner.
setIsStartingOrder(true);
startOrderSession(order.requisitionId).then((success) => {
if (success) {
navigate('PickingPickLocation');
return;
}
setIsStartingOrder(false);
});
};

const isLoadingList = !hasLoaded || isPullRefreshing;
// Skeleton covers the first load only, later refreshes keep the list on screen.
const isLoadingList = !hasLoaded;

const chips: { value: QueueTypeFilter; label: string; count: number }[] = [
{ value: ALL_QUEUE_TYPES, label: 'All', count: sortedOrders.length },
Expand All @@ -97,7 +104,7 @@ export default function DiscretePickingListScreen() {
placeholder="Search by order, customer, or product"
resetSearch={() => setSearchTerm('')}
accessibilityLabel="Search open orders"
loading={hasLoaded && isRefreshing}
loading={hasLoaded && isRefreshing && !isPullRefreshing && !isStartingOrder}
onSearchTermSubmit={setSearchTerm}
/>

Expand Down Expand Up @@ -134,7 +141,7 @@ export default function DiscretePickingListScreen() {
{!isLoadingList && (
<View style={styles.showAssignedToggle}>
<ToggleRow
title="Show assigned orders"
title="Show Assigned Orders"
value={!excludeAssignedRequisitions}
onValueChange={(value) => setExcludeAssignedRequisitions(!value)}
/>
Expand All @@ -149,14 +156,16 @@ export default function DiscretePickingListScreen() {
data={visibleOrders}
keyExtractor={(order) => order.requisitionId}
renderItem={({ item }) => (
<DiscretePickingOrderCard order={item} showAssignee={!excludeAssignedRequisitions} onPress={handleOrderPress} />
<DiscretePickingOrderCard
order={item}
showAssignee={!excludeAssignedRequisitions}
onPress={handleOrderPress}
/>
)}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
contentContainerStyle={styles.listContent}
// The skeleton above owns the pull-to-refresh loading state, so the spinner
// only needs to retract once the gesture hands off to it.
refreshing={false}
refreshing={isPullRefreshing}
ListEmptyComponent={
<EmptyView
title="Orders"
Expand Down
51 changes: 28 additions & 23 deletions src/screens/Picking/DiscretePickingOrderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,39 @@ const DELIVERY_TYPE_LABELS: Record<string, string> = DELIVERY_TYPES.reduce<Recor
return acc;
}, {});

function OrderStatusChip({ inProgress }: { inProgress: boolean }) {
return (
<Chip
icon={inProgress ? 'progress-clock' : 'check-circle-outline'}
selectedColor={inProgress ? Theme.colors.infoForeground : Theme.colors.successForeground}
style={[styles.chipDefault, styles.statusChip, inProgress ? styles.statusChipInProgress : styles.statusChipReady]}
>
<Text
style={[
styles.chipText,
styles.fontBold,
inProgress ? styles.statusChipTextInProgress : styles.statusChipTextReady
]}
>
{inProgress ? 'In progress' : 'Ready'}
</Text>
</Chip>
);
}

type Props = {
order: DiscretePickingOrder;
showAssignee?: boolean;
onPress: (order: DiscretePickingOrder) => void;
};

export default function DiscretePickingOrderCard({ order, onPress }: Props) {
export default function DiscretePickingOrderCard({ order, showAssignee = false, onPress }: Props) {
const deliveryTypeLabel = order.deliveryTypeCode
? DELIVERY_TYPE_LABELS[order.deliveryTypeCode] ?? order.deliveryTypeCode
: null;
const lineCountLabel = order.taskCount === 1 ? 'Line' : 'Lines';
const lineCountValue =
order.openTaskCount < order.taskCount ? `${order.openTaskCount} / ${order.taskCount} Left` : `${order.taskCount}`;
const assigneeName = order.assignee ? `${order.assignee.firstName} ${order.assignee.lastName}`.trim() : null;

return (
Expand All @@ -33,25 +56,7 @@ export default function DiscretePickingOrderCard({ order, onPress }: Props) {
<Chip icon="identifier" style={[styles.chipDefault, styles.orderNumberChip]}>
<Text style={[styles.orderNumberText, styles.fontBold]}>{order.requisitionNumber ?? HYPHEN}</Text>
</Chip>
<Chip
icon={order.inProgress ? 'progress-clock' : 'check-circle-outline'}
selectedColor={order.inProgress ? Theme.colors.infoForeground : Theme.colors.successForeground}
style={[
styles.chipDefault,
styles.statusChip,
order.inProgress ? styles.statusChipInProgress : styles.statusChipReady
]}
>
<Text
style={[
styles.chipText,
styles.fontBold,
order.inProgress ? styles.statusChipTextInProgress : styles.statusChipTextReady
]}
>
{order.inProgress ? 'In progress' : 'Ready'}
</Text>
</Chip>
<OrderStatusChip inProgress={order.inProgress} />
</View>

<Divider style={styles.contentDivider} />
Expand All @@ -73,13 +78,13 @@ export default function DiscretePickingOrderCard({ order, onPress }: Props) {
) : null}
<Chip icon="package-variant-closed" style={styles.chipDefault}>
<Text style={styles.chipText}>
{lineCountLabel}: <Text style={[styles.chipText, styles.fontBold]}>{order.taskCount}</Text>
{lineCountLabel}: <Text style={[styles.chipText, styles.fontBold]}>{lineCountValue}</Text>
</Text>
</Chip>
{assigneeName ? (
{showAssignee && assigneeName ? (
<Chip icon="account" style={styles.chipDefault}>
<Text style={styles.chipText}>
Assigned to: <Text style={[styles.chipText, styles.fontBold]}>{assigneeName}</Text>
Assigned To: <Text style={[styles.chipText, styles.fontBold]}>{assigneeName}</Text>
</Text>
</Chip>
) : null}
Expand Down
19 changes: 19 additions & 0 deletions src/screens/Picking/PickingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import {
} from '../../redux/actions/picking';
import { DeliveryType, PickTask } from '../../types/picking';

// Which screen the session was started from, used to send the picker back there when it ends.
export type PickingEntryPoint = 'BATCH' | 'DISCRETE';

const HOME_ROUTE_BY_ENTRY_POINT: Record<PickingEntryPoint, string> = {
BATCH: 'PickingPickType',
DISCRETE: 'DiscretePickingList'
};

type PickingContextType = {
/** The list of all tasks for this session */
tasks: PickTask[];
Expand All @@ -27,6 +35,10 @@ type PickingContextType = {
currentTask: PickTask | undefined;
/** Total number of tasks in the session */
allTasksCount: number;
/** The screen this session was started from */
entryPoint: PickingEntryPoint;
/** Route to return to when the session ends, derived from the entry point */
homeRoute: string;
/** Starts a new picking session, returns whether it was successful */
startSession: (deliveryType: DeliveryType, ordersCount: number) => Promise<boolean>;
/** Starts a picking session for a single order (discrete picking), returns whether it was successful */
Expand Down Expand Up @@ -65,11 +77,14 @@ const PickingContext = React.createContext<PickingContextType | undefined>(undef
export function PickingProvider({ children }: { children: React.ReactNode }) {
const [tasks, setTasks] = React.useState<PickTask[]>([]);
const [currentTaskIndex, setCurrentTaskIndex] = React.useState<number>(0);
const [entryPoint, setEntryPoint] = React.useState<PickingEntryPoint>('BATCH');
const dispatch = useDispatch();
const homeRoute = HOME_ROUTE_BY_ENTRY_POINT[entryPoint];
const allTasksCount = tasks.length;
const currentTask = allTasksCount > 0 && currentTaskIndex < allTasksCount ? tasks[currentTaskIndex] : undefined;

const startSession = async (deliveryType: DeliveryType, ordersCount: number): Promise<boolean> => {
setEntryPoint('BATCH');
return new Promise((resolve) => {
dispatch(
getPickTasksAction({ deliveryTypeCode: deliveryType.code, ordersCount }, ({ response, errorMessage }) => {
Expand Down Expand Up @@ -101,6 +116,7 @@ export function PickingProvider({ children }: { children: React.ReactNode }) {
};

const startOrderSession = async (requisitionId: string): Promise<boolean> => {
setEntryPoint('DISCRETE');
return new Promise((resolve) => {
dispatch(
getPickTasksByRequisitionAction(requisitionId, (res) => {
Expand Down Expand Up @@ -210,6 +226,7 @@ export function PickingProvider({ children }: { children: React.ReactNode }) {
onPress: () =>
resetToRoutes([
{ name: 'Drawer', params: { screen: 'Dashboard' } },
{ name: homeRoute },
{ name: 'PickingPickStagingLocation' }
])
}
Expand Down Expand Up @@ -288,6 +305,8 @@ export function PickingProvider({ children }: { children: React.ReactNode }) {
setCurrentTaskIndex,
currentTask,
allTasksCount,
entryPoint,
homeRoute,
startSession,
startOrderSession,
pickCurrentTask,
Expand Down
16 changes: 12 additions & 4 deletions src/screens/Picking/PickingPickLocationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ScannerInput } from '../../components/ScannerInput';
import { SearchButton } from '../../components/SearchButton';
import { useSearchButton } from '../../components/SearchButton/useSearchButton';
import { EMPTY_STRING, HYPHEN } from '../../constants';
import { navigate } from '../../NavigationService';
import { navigate, resetToRoutes } from '../../NavigationService';
import { RootState } from '../../redux/reducers';
import { parseFromISODateToLocaleString } from '../../utils/utils';
import { CustomerDetails } from './CustomerDetails';
Expand All @@ -17,8 +17,15 @@ import { ReallocateModal } from './ReallocateModal';
import styles from './styles';

export default function PickingPickLocationScreen() {
const { currentTask, currentTaskIndex, allTasksCount, startPickTask, revalidateCurrentTask, resetSession } =
usePickingContext();
const {
currentTask,
currentTaskIndex,
allTasksCount,
startPickTask,
revalidateCurrentTask,
resetSession,
homeRoute
} = usePickingContext();
const [pickLocationBarcode, setPickLocationBarcode] = React.useState<string>(EMPTY_STRING);
const [isReallocateModalOpen, setIsReallocateModalOpen] = React.useState(false);
const { allowReallocationDuringPicking } = useSelector((state: RootState) => state.settingsReducer);
Expand Down Expand Up @@ -150,7 +157,8 @@ export default function PickingPickLocationScreen() {
onAllocated={() => {
setIsReallocateModalOpen(false);
resetSession();
navigate('PickingPickType');
// Reset so the finished task screens are not left behind the back arrow.
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: homeRoute }]);
}}
/>
)}
Expand Down
10 changes: 6 additions & 4 deletions src/screens/Picking/PickingPickOutboundContainerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export default function PickingPickOutboundContainerScreen() {
allTasksCount,
revalidateCurrentTask,
goToNextTask,
revalidateTasksForRequisition
revalidateTasksForRequisition,
homeRoute
} = usePickingContext();
const { params } = useRoute<PickingPickOutboundContainerScreenProps>();
const parsedQuantityPicked = params?.quantityPicked ? Number(params.quantityPicked) : undefined;
Expand Down Expand Up @@ -72,13 +73,14 @@ export default function PickingPickOutboundContainerScreen() {
const omitStagingLocationStep =
currentTask.quantityPicked + parsedQuantityPicked < currentTask.quantityRequired;

revalidateTaskAndProceed(
revalidateTaskAndProceed({
revalidateCurrentTask,
currentTaskIndex,
allTasksCount,
goToNextTask,
homeRoute,
omitStagingLocationStep
);
});
}
},
params?.reasonCode?.name
Expand All @@ -93,7 +95,7 @@ export default function PickingPickOutboundContainerScreen() {
return;
}

revalidateTaskAndProceed(revalidateCurrentTask, currentTaskIndex, allTasksCount, goToNextTask);
revalidateTaskAndProceed({ revalidateCurrentTask, currentTaskIndex, allTasksCount, goToNextTask, homeRoute });
});

setOutboundContainerId(EMPTY_STRING);
Expand Down
11 changes: 9 additions & 2 deletions src/screens/Picking/PickingPickQuantityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { usePickingContext } from './PickingContext';
import styles from './styles';

export default function PickingPickQuantityScreen() {
const { tasks, currentTask, currentTaskIndex, allTasksCount, shortPickTask, goToNextTask } = usePickingContext();
const { tasks, currentTask, currentTaskIndex, allTasksCount, shortPickTask, goToNextTask, homeRoute } =
usePickingContext();
const dispatch = useDispatch();
const isFocused = useIsFocused();

Expand Down Expand Up @@ -105,7 +106,13 @@ export default function PickingPickQuantityScreen() {
index !== currentTaskIndex && task.quantityPicked < task.quantityRequired && !task.reasonCode
);
// Skip revalidation: task is closed server-side, and GET /pick-tasks/:id 404s if the requisition is canceled.
proceedToNextOrComplete(currentTaskIndex, allTasksCount, goToNextTask, omitStagingLocationStep);
proceedToNextOrComplete({
currentTaskIndex,
allTasksCount,
goToNextTask,
homeRoute,
omitStagingLocationStep
});
},
reasonCode?.name
);
Expand Down
10 changes: 5 additions & 5 deletions src/screens/Picking/PickingPickStagingLocationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import styles from './styles';
const SKIP_STAGING_LOCATION_VALIDATION = true;

export default function PickingPickStagingLocationScreen() {
const { tasks, dropCurrentTask, dropCurrentTaskAtStagingLocation, resetSession, setCurrentTaskIndex } =
const { tasks, dropCurrentTask, dropCurrentTaskAtStagingLocation, resetSession, setCurrentTaskIndex, homeRoute } =
usePickingContext();
const [stagingLocationNumber, setStagingLocationNumber] = React.useState(EMPTY_STRING);
const [currentUniqueIndex, setCurrentUniqueIndex] = React.useState(0);
Expand All @@ -37,9 +37,9 @@ export default function PickingPickStagingLocationScreen() {
if (!currentTask) {
// No tasks left at all, return to home
Alert.alert('Staging', 'No more tasks available for staging drop.');
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: 'PickingPickType' }]);
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: homeRoute }]);
}
}, [currentTask, tasks.length, setCurrentTaskIndex, uniqueTasks.length, tasks]);
}, [currentTask, tasks.length, setCurrentTaskIndex, uniqueTasks.length, tasks, homeRoute]);

// Requires the scanned location to match the one suggested by the task. Used when SKIP_STAGING_LOCATION_VALIDATION is false.
function handleScan(locationId: string) {
Expand Down Expand Up @@ -78,7 +78,7 @@ export default function PickingPickStagingLocationScreen() {
text: 'OK',
onPress: () => {
resetSession();
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: 'PickingPickType' }]);
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: homeRoute }]);
}
}
]);
Expand Down Expand Up @@ -117,7 +117,7 @@ export default function PickingPickStagingLocationScreen() {
text: 'OK',
onPress: () => {
resetSession();
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: 'PickingPickType' }]);
resetToRoutes([{ name: 'Drawer', params: { screen: 'Dashboard' } }, { name: homeRoute }]);
}
}
]);
Expand Down
Loading