diff --git a/src/redux/actions/putaways.ts b/src/redux/actions/putaways.ts index 6b575127..b6fa6081 100644 --- a/src/redux/actions/putaways.ts +++ b/src/redux/actions/putaways.ts @@ -8,6 +8,7 @@ export const SUBMIT_PUTAWAY_ITEM_BIN_LOCATION = 'SUBMIT_PUTAWAY_ITEM_BIN_LOCATIO 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 const PUTAWAY_CANDIDATE_PUT_AWAY = 'PUTAWAY_CANDIDATE_PUT_AWAY'; export const GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST = 'GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST'; export const GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST_SUCCESS = 'GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST_SUCCESS'; @@ -66,6 +67,13 @@ export function patchPutawayTaskAction( }; } +export function markCandidatePutAway(key: string, remainingQuantity: number) { + return { + type: PUTAWAY_CANDIDATE_PUT_AWAY, + payload: { key, remainingQuantity } + }; +} + export function getPutawayDetailsByContainerId(containerId: string, callback?: (data: any) => void) { return { type: GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST, diff --git a/src/redux/reducers/putawayReducer.ts b/src/redux/reducers/putawayReducer.ts index de27bf61..86b212d4 100644 --- a/src/redux/reducers/putawayReducer.ts +++ b/src/redux/reducers/putawayReducer.ts @@ -1,8 +1,10 @@ import { SortationTask } from '../../types/sortation'; +import { putawayCandidateKey } from '../../utils/putawayCandidate'; import { FETCH_PUTAWAY_FROM_ORDER_REQUEST_SUCCESS, GET_PUTAWAY_CANDIDATES_REQUEST_SUCCESS, GET_PUTAWAY_DETAILS_BY_CONTAINER_ID_REQUEST_SUCCESS, + PUTAWAY_CANDIDATE_PUT_AWAY, SUBMIT_PUTAWAY_ITEM_BIN_LOCATION_SUCCESS } from '../actions/putaways'; @@ -11,13 +13,15 @@ export interface State { putAwayItem: any; candidates: any; putawayTasks: SortationTask[]; + putAwayOverrides: { [key: string]: number }; } const initialState: State = { putAway: null, putAwayItem: null, candidates: [], - putawayTasks: [] + putawayTasks: [], + putAwayOverrides: {} }; function reducer(state = initialState, action: any) { @@ -28,10 +32,27 @@ function reducer(state = initialState, action: any) { putAway: action.payload.data }; } + case PUTAWAY_CANDIDATE_PUT_AWAY: { + const { key, remainingQuantity } = action.payload; + return { + ...state, + putAwayOverrides: { ...state.putAwayOverrides, [key]: remainingQuantity } + }; + } case GET_PUTAWAY_CANDIDATES_REQUEST_SUCCESS: { + const candidates = action.payload || []; + // Drop overrides the server has caught up with, so they cannot go stale + const putAwayOverrides = { ...state.putAwayOverrides }; + Object.keys(putAwayOverrides).forEach((key) => { + const match = candidates.find((candidate: any) => putawayCandidateKey(candidate) === key); + if (!match || Number(match.quantity) <= putAwayOverrides[key]) { + delete putAwayOverrides[key]; + } + }); return { ...state, - candidates: action.payload + candidates, + putAwayOverrides }; } case SUBMIT_PUTAWAY_ITEM_BIN_LOCATION_SUCCESS: { diff --git a/src/screens/PutawayCandidates/index.tsx b/src/screens/PutawayCandidates/index.tsx index 41db5dad..001f0e23 100644 --- a/src/screens/PutawayCandidates/index.tsx +++ b/src/screens/PutawayCandidates/index.tsx @@ -1,8 +1,8 @@ -import _ from 'lodash'; -import React, { Component } from 'react'; -import { Alert, FlatList, RefreshControl, SafeAreaView, View } from 'react-native'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import React, { useCallback, useMemo, useState } from 'react'; +import { FlatList, RefreshControl, SafeAreaView, View } from 'react-native'; import { Caption, Card, Chip, Divider, Subheading } from 'react-native-paper'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { LayoutStyle } from '../../assets/styles'; import BarcodeSearchHeader from '../../components/BarcodeSearchHeader/BarcodeSearchHeader'; @@ -10,83 +10,134 @@ import Button from '../../components/Button'; import EmptyView from '../../components/EmptyView'; import ListLoadingSkeleton from '../../components/ListLoadingSkeleton'; import showPopup from '../../components/Popup'; +import { EMPTY_STRING } from '../../constants'; import { getCandidates } from '../../redux/actions/putaways'; import { RootState } from '../../redux/reducers'; import { emptyStateMessage } from '../../utils/emptyStateMessage'; +import { putawayCandidateKey } from '../../utils/putawayCandidate'; import PutawayCandidateCardSkeleton from './PutawayCandidateCardSkeleton'; import styles from './styles'; -import { DispatchProps, Props, State } from './types'; - -class PutawayCandidates extends Component { - constructor(props: Props) { - super(props); - - this.state = { - refreshing: false, - putawayCandidates: [], - filteredPutawayCandidates: [], - initialLoading: true, - searchTerm: '' - }; - } - - componentDidMount() { - this.getScreenData(); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.candidates !== this.props.candidates) { - const putawayCandidates = this.props.candidates - .filter((candidate: any) => candidate.putawayStatus === 'READY') - .sort((a: any, b: any) => - a['currentLocation.name'].toLowerCase().localeCompare(b['currentLocation.name'].toLowerCase()) - ); - - this.setState({ - refreshing: false, - initialLoading: false, - putawayCandidates - }); +import { PutawayCandidate } from './types'; + +const SKELETON_CARD_COUNT = 6; + +function matchesSearchTerm(candidate: PutawayCandidate, term: string): boolean { + return ( + (candidate['inventoryItem.lotNumber']?.toLowerCase().includes(term) ?? false) || + (candidate['currentLocation.name']?.toLowerCase().includes(term) ?? false) || + (candidate['currentLocation.id']?.toLowerCase().includes(term) ?? false) + ); +} + +export default function PutawayCandidates() { + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const candidates = useSelector((state: RootState) => state.putawayReducer.candidates); + const putAwayOverrides = useSelector((state: RootState) => state.putawayReducer.putAwayOverrides); + const currentLocation = useSelector((state: RootState) => state.mainReducer.currentLocation); + const productSummaryConfig = useSelector((state: RootState) => state.settingsReducer.productSummaryConfig); + + const [searchTerm, setSearchTerm] = useState(EMPTY_STRING); + const [refreshing, setRefreshing] = useState(false); + const [initialLoading, setInitialLoading] = useState(true); + + const showLotNumber = productSummaryConfig?.lotNumber !== false; + const showExpirationDate = productSummaryConfig?.expirationDate !== false; + + const navigateToPutawayItem = useCallback( + (item: PutawayCandidate) => navigation.navigate('PutawayItem', { item }), + [navigation] + ); + + const getScreenData = useCallback(() => { + if (!currentLocation?.id) { + setRefreshing(false); + setInitialLoading(false); + return; } - } - - getScreenData = async () => { - this.setState({ refreshing: true }); - const { currentLocation } = this.props; - this.props.getCandidates( - currentLocation.id, - (data: any) => { - if (data?.error) { - this.setState({ refreshing: false, initialLoading: false }); - showPopup({ - title: 'Putaway Candidates', - message: data.errorMessage ?? 'Failed to load putaway candidates', - positiveButton: { - text: 'Retry', - callback: () => this.getScreenData() - }, - negativeButtonText: 'Cancel' - }); - } - }, - true + + setRefreshing(true); + dispatch( + getCandidates( + currentLocation.id, + (data: any) => { + setRefreshing(false); + setInitialLoading(false); + if (data?.error) { + showPopup({ + title: 'Putaway Candidates', + message: data.errorMessage ?? 'Failed to load putaway candidates', + positiveButton: { + text: 'Retry', + callback: () => getScreenData() + }, + negativeButtonText: 'Cancel' + }); + } + }, + true + ) ); - }; - - renderItem = (item: any) => { - const { productSummaryConfig } = this.props; - const showLotNumber = productSummaryConfig?.lotNumber !== false; - const showExpirationDate = productSummaryConfig?.expirationDate !== false; - - return ( - - item.id - ? Alert.alert('Item is already in a pending putaway') - : this.props.navigation.navigate('PutawayItem', { item }) - } - > + }, [dispatch, currentLocation?.id]); + + useFocusEffect( + useCallback(() => { + getScreenData(); + }, [getScreenData]) + ); + + const putawayCandidates = useMemo( + () => + (candidates ?? []) + .filter((candidate: PutawayCandidate) => candidate.putawayStatus === 'READY') + .map((candidate: PutawayCandidate) => { + const override = putAwayOverrides?.[putawayCandidateKey(candidate)]; + return override === undefined ? candidate : { ...candidate, quantity: override }; + }) + .filter((candidate: PutawayCandidate) => Number(candidate.quantity) > 0) + .sort((a: PutawayCandidate, b: PutawayCandidate) => + (a['currentLocation.name'] ?? '').toLowerCase().localeCompare((b['currentLocation.name'] ?? '').toLowerCase()) + ), + [candidates, putAwayOverrides] + ); + + const visibleData = useMemo(() => { + const term = searchTerm.trim().toLowerCase(); + if (!term) { + return putawayCandidates; + } + return putawayCandidates.filter((candidate) => matchesSearchTerm(candidate, term)); + }, [putawayCandidates, searchTerm]); + + const resetFiltering = useCallback(() => setSearchTerm(EMPTY_STRING), []); + + const onSearchTermSubmit = useCallback( + (query: string) => { + const term = query.trim().toLowerCase(); + if (!term) { + resetFiltering(); + return; + } + + const exactLotMatches = putawayCandidates.filter( + (candidate) => candidate['inventoryItem.lotNumber']?.toLowerCase() === term + ); + + if (exactLotMatches.length === 1) { + resetFiltering(); + navigateToPutawayItem(exactLotMatches[0]); + return; + } + + setSearchTerm(query); + }, + [putawayCandidates, navigateToPutawayItem, resetFiltering] + ); + + const renderItem = useCallback( + (item: PutawayCandidate) => ( + navigateToPutawayItem(item)}> @@ -102,7 +153,7 @@ class PutawayCandidates extends Component { {`${item['product.productCode']} - ${item['product.name']}`} {showLotNumber && ( - {`Lot Number: ${item?.['inventoryItem.lotNumber'] ?? 'Default'}`} + {`Lot Number: ${item['inventoryItem.lotNumber'] ?? 'Default'}`} )} @@ -117,103 +168,40 @@ class PutawayCandidates extends Component { - ); - }; - - navigateToPutawayItem = (item: any) => { - this.props.navigation.navigate('PutawayItem', { item }); - }; - - filterPutawayCandidates = (query: string) => { - this.setState({ searchTerm: query }); - if (query) { - const exactPutawayCandidate = _.filter( - this.state.putawayCandidates, - (putawayCandidate: any) => putawayCandidate['inventoryItem.lotNumber']?.toLowerCase() === query.toLowerCase() - ); - - if (exactPutawayCandidate.length === 1) { - this.resetFiltering(); - this.navigateToPutawayItem(exactPutawayCandidate[0]); - } else { - const filteredPutawayCandidates = _.filter( - this.state.putawayCandidates, - (putawayCandidate: any) => - putawayCandidate['inventoryItem.lotNumber']?.toLowerCase().includes(query.toLowerCase()) || - putawayCandidate['currentLocation.name']?.toLowerCase().includes(query.toLowerCase()) || - putawayCandidate['currentLocation.id']?.toLowerCase().includes(query.toLowerCase()) - ); - this.setState({ - ...this.state, - filteredPutawayCandidates - }); - } - - return; - } - - this.resetFiltering(); - }; - - resetFiltering = () => { - this.setState({ - ...this.state, - searchTerm: '', - filteredPutawayCandidates: [] - }); - }; - - render() { - const { filteredPutawayCandidates, putawayCandidates, initialLoading, refreshing, searchTerm } = this.state; - const visibleData = filteredPutawayCandidates.length > 0 ? filteredPutawayCandidates : putawayCandidates; - return ( - - -