From 2dae141ede7bf13e5122aeeb8e0cc0c3abc1004c Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Mon, 17 Nov 2025 09:15:36 +0100 Subject: [PATCH 1/8] OBLS-308 init pick up allocation --- src/Main.tsx | 27 +++- src/screens/Dashboard/dashboardData.ts | 7 + .../PickUpAllocation/PickUpEntryScreen.tsx | 63 ++++++++ .../PickUpAllocation/PickUpOrderScreen.tsx | 121 ++++++++++++++ src/screens/PickUpAllocation/mock-data.ts | 153 ++++++++++++++++++ src/screens/PickUpAllocation/styles.ts | 116 +++++++++++++ src/screens/PickUpAllocation/types.ts | 13 ++ 7 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 src/screens/PickUpAllocation/PickUpEntryScreen.tsx create mode 100644 src/screens/PickUpAllocation/PickUpOrderScreen.tsx create mode 100644 src/screens/PickUpAllocation/mock-data.ts create mode 100644 src/screens/PickUpAllocation/styles.ts create mode 100644 src/screens/PickUpAllocation/types.ts diff --git a/src/Main.tsx b/src/Main.tsx index 14c4e417..ab70b58a 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -7,6 +7,7 @@ import { SafeAreaView } from 'react-native'; import { Provider } from 'react-native-paper'; import SplashScreen from 'react-native-splash-screen'; import { connect } from 'react-redux'; + import FullScreenLoadingIndicator from './components/FullScreenLoadingIndicator'; import showPopup from './components/Popup'; import { appConfig } from './constants'; @@ -35,6 +36,8 @@ import OutboundStockDetails from './screens/OutboundStockDetails'; import OutboundStockList from './screens/OutboundStockList'; import PackingLocationPage from './screens/PackingLocationPage'; import PickOrderItem from './screens/PickList'; +import { PickUpEntryScreen } from './screens/PickUpAllocation/PickUpEntryScreen'; +import { PickUpOrderScreen } from './screens/PickUpAllocation/PickUpOrderScreen'; import Placeholder from './screens/Placeholder'; import ProductDetails from './screens/ProductDetails'; import Products from './screens/Products'; @@ -47,21 +50,21 @@ import PutawayList from './screens/PutawayList'; import Scan from './screens/Scan'; import Settings from './screens/Settings'; import ShipItemDetails from './screens/ShipItemDetails'; +import SortationContainerScreen from './screens/Sortation/SortationContainerScreen'; 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'; import PutawayEntryScreen from './screens/SortationPutaway/PutawayEntryScreen'; import PutawayLocationScanScreen from './screens/SortationPutaway/PutawayLocationScanScreen'; import PutawayProductScanScreen from './screens/SortationPutaway/PutawayProductScanScreen'; import PutawayQuantityScreen from './screens/SortationPutaway/PutawayQuantityScreen'; import HeaderRight from './screens/TopBar/RightHeader'; +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'; const Stack = createStackNavigator(); export interface OwnProps { @@ -292,6 +295,16 @@ class Main extends Component { component={PutawayQuantityScreen} options={{ title: 'Putaway Details' }} /> + + diff --git a/src/screens/Dashboard/dashboardData.ts b/src/screens/Dashboard/dashboardData.ts index c998632c..6da1ae41 100644 --- a/src/screens/Dashboard/dashboardData.ts +++ b/src/screens/Dashboard/dashboardData.ts @@ -43,6 +43,13 @@ const dashboardEntries: DashboardEntry[] = [ icon: IconPicking, navigationScreenName: 'Orders' }, + { + key: 'pickUpAllocation', + screenName: 'Pick-Up Allocation', + entryDescription: 'Manage pick-up allocations and tasks', + icon: IconPicking, + navigationScreenName: 'PickUpEntryScreen' + }, { key: 'packing', screenName: 'Packing', diff --git a/src/screens/PickUpAllocation/PickUpEntryScreen.tsx b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx new file mode 100644 index 00000000..44acc7c8 --- /dev/null +++ b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { FlatList, TouchableOpacity, View } from 'react-native'; +import { Caption, Card, Chip, Divider, Paragraph, Title } from 'react-native-paper'; + +import { navigate } from '../../NavigationService'; +import { MOCKED_ORDERS } from './mock-data'; +import styles from './styles'; +import { AllocationOrder } from './types'; + +export function PickUpEntryScreen() { + return ( + + Outstanding Orders ({MOCKED_ORDERS.length}) + + Select an order from the list to start the pick-up allocation process. + + + + + } + keyExtractor={(item: AllocationOrder) => item.orderNumber} + numColumns={1} + ItemSeparatorComponent={() => } + /> + + ); +} + +function PickUpCard({ order }: { order: AllocationOrder }) { + const onPress = () => navigate('PickUpOrderScreen', { order }); + + return ( + + + + + + {order.orderNumber} + + + {`Lines: ${order.orderLines.length}`} + + + + + + {order.name} + + {`Order Date: ${new Date(order.orderDate).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + })}`} + + + + + ); +} diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx new file mode 100644 index 00000000..036854e3 --- /dev/null +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -0,0 +1,121 @@ +import { RouteProp, useRoute } from '@react-navigation/native'; +import React from 'react'; +import { ScrollView, View } from 'react-native'; +import { Button, List, Paragraph, TextInput, Title } from 'react-native-paper'; + +import EmptyView from '../../components/EmptyView'; +import styles from './styles'; +import { AllocationOrder, AllocationOrderLine } from './types'; + +type QuantityRouteProp = RouteProp<{ PickUpOrderScreen: { order: AllocationOrder } }, 'PickUpOrderScreen'>; + +export function PickUpOrderScreen() { + const { params } = useRoute(); + const { order } = params; + + if (!order || !order.orderLines) { + return ( + + ); + } + + return ( + + Order Line Allocation ({order.orderLines.length}) + + + + {order.orderLines.map((line, index) => ( + + ))} + + + + ); +} + +function AllocationOrderItem({ orderLine }: { orderLine: AllocationOrderLine }) { + const [expanded, setExpanded] = React.useState(false); + const toggleExpanded = () => setExpanded(!expanded); + + const { product, quantityRequired } = orderLine; + + return ( + } + expanded={expanded} + style={styles.accordion} + titleStyle={styles.accordionTitle} + descriptionStyle={styles.accordionDescription} + onPress={toggleExpanded} + > + + + + + ); +} + +function OrderLineController() { + const [partialQuantity, setPartialQuantity] = React.useState(null); + + function handleConfirm() { + // Placeholder for confirm action + } + + function handleWarehousePick() { + // Placeholder for full warehouse pick action + } + + function handleDisplayPick() { + // Placeholder for full display pick action + } + + return ( + + + + + + + + {/* Label + Input */} + + Enter Partial Quantity From Display: + + + setPartialQuantity(text ? parseInt(text, 10) : null)} + /> + + + + {/* Confirm button */} + + + + ); +} diff --git a/src/screens/PickUpAllocation/mock-data.ts b/src/screens/PickUpAllocation/mock-data.ts new file mode 100644 index 00000000..2294c34f --- /dev/null +++ b/src/screens/PickUpAllocation/mock-data.ts @@ -0,0 +1,153 @@ +import { AllocationOrder } from './types'; + +const MOCKED_PRODUCTS = [ + { + id: 'P-001', + productCode: 'SKU-1001', + name: 'Wireless Mouse', + category: { + id: 'C-01', + name: 'Electronics' + }, + availability: { + quantityOnHand: { + value: 90, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityAvailableToPromise: { + value: 75, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityAllocated: { + value: 15, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityOnOrder: { + value: 30, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + } + }, + attributes: [], + productType: { + name: 'Standard' + }, + images: [], + description: 'High-precision wireless mouse with ergonomic design.', + pricePerUnit: 25.99, + quantityAllocated: 15, + quantityAvailableToPromise: 75, + quantityOnHand: 90, + quantityOnOrder: 30, + unitOfMeasure: 'pcs', + availableItems: [] + }, + { + id: 'P-002', + productCode: 'SKU-2002', + name: 'Bluetooth Keyboard', + category: { + id: 'C-01', + name: 'Electronics' + }, + availability: { + quantityOnHand: { + value: 70, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityAvailableToPromise: { + value: 60, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityAllocated: { + value: 10, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + }, + quantityOnOrder: { + value: 20, + unitOfMeasure: { + code: 'pcs', + name: 'pieces' + } + } + }, + attributes: [], + productType: { + name: 'Standard' + }, + images: [], + description: 'Compact Bluetooth keyboard suitable for tablets and laptops.', + pricePerUnit: 45.0, + quantityAllocated: 10, + quantityAvailableToPromise: 60, + quantityOnHand: 70, + quantityOnOrder: 20, + unitOfMeasure: 'pcs', + availableItems: [] + } +]; + +export const MOCKED_ORDERS: AllocationOrder[] = [ + { + orderNumber: 'ORD-001', + name: 'Frank Lee', + orderLines: [ + { product: MOCKED_PRODUCTS[0], quantityRequired: 10 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 5 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 3 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 5 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 4 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 2 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 1 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 8 } + ], + orderDate: '2026-06-15T10:30:00Z' + }, + { + orderNumber: 'ORD-002', + name: 'Grace Kim', + orderLines: [{ product: MOCKED_PRODUCTS[1], quantityRequired: 8 }], + orderDate: '2026-06-14T14:45:00Z' + }, + { + orderNumber: 'ORD-003', + name: 'Hannah Scott', + orderLines: [ + { product: MOCKED_PRODUCTS[0], quantityRequired: 4 }, + { product: MOCKED_PRODUCTS[1], quantityRequired: 4 } + ], + orderDate: '2026-06-13T09:15:00Z' + }, + { + orderNumber: 'ORD-004', + name: 'Ian Wright', + orderLines: [{ product: MOCKED_PRODUCTS[0], quantityRequired: 12 }], + orderDate: '2026-06-12T16:00:00Z' + }, + { + orderNumber: 'ORD-005', + name: 'Jane Foster', + orderLines: [{ product: MOCKED_PRODUCTS[1], quantityRequired: 7 }], + orderDate: '2026-06-11T11:20:00Z' + } +]; diff --git a/src/screens/PickUpAllocation/styles.ts b/src/screens/PickUpAllocation/styles.ts new file mode 100644 index 00000000..39949c65 --- /dev/null +++ b/src/screens/PickUpAllocation/styles.ts @@ -0,0 +1,116 @@ +import { StyleSheet } from 'react-native'; +import Theme from '../../utils/Theme'; + +export default StyleSheet.create({ + screenContainer: { + flex: 1, + backgroundColor: '#f9f9f9', + padding: Theme.spacing.large + }, + titleText: { + fontSize: 24, + fontWeight: '600', + marginBottom: Theme.spacing.small - 2 + }, + subtitleText: { + color: '#555' + }, + sectionDivider: { + marginTop: Theme.spacing.small + }, + itemSeparator: { + height: Theme.spacing.small - 2 + }, + + cardTouchable: { + borderRadius: Theme.roundness + }, + card: { + borderRadius: Theme.roundness, + backgroundColor: 'white', + borderWidth: 1, + borderColor: '#ddd' + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: Theme.spacing.small - 2 + }, + cardDivider: { + marginVertical: Theme.spacing.small / 2 + }, + chip: { + height: 24, + justifyContent: 'flex-start', + borderRadius: Theme.roundness, + alignItems: 'center', + paddingHorizontal: Theme.spacing.small + }, + chipText: { + fontSize: 12 + }, + + accordion: { + backgroundColor: '#fff', + borderRadius: Theme.roundness, + marginBottom: Theme.spacing.small, + elevation: 1, + borderWidth: 1, + borderColor: '#ddd' + }, + accordionTitle: { + fontWeight: '600' + }, + accordionDescription: { + color: '#666' + }, + accordionContent: { + paddingHorizontal: Theme.spacing.medium, + paddingBottom: Theme.spacing.medium + }, + productDescription: { + marginBottom: Theme.spacing.small + }, + + buttonRow: { + justifyContent: 'space-between' + }, + button: { + flex: 1, + marginBottom: Theme.spacing.small / 2, + flexGrow: 1 + }, + buttonText: { + fontSize: 12, + lineHeight: 16 + }, + paddingZero: { + paddingLeft: 0, + paddingRight: 0 + }, + + partialInputContainer: { + paddingVertical: Theme.spacing.small / 2 + }, + inputRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: Theme.spacing.small + }, + inputLabel: { + fontSize: 14, + flexShrink: 0, + marginRight: Theme.spacing.small, + width: 180 + }, + inputWrapper: { + flex: 1 + }, + input: { + justifyContent: 'center' + }, + confirmButton: { + alignSelf: 'stretch' + } +}); diff --git a/src/screens/PickUpAllocation/types.ts b/src/screens/PickUpAllocation/types.ts new file mode 100644 index 00000000..95612224 --- /dev/null +++ b/src/screens/PickUpAllocation/types.ts @@ -0,0 +1,13 @@ +import Product from '../../data/product/Product'; + +export type AllocationOrderLine = { + product: Product; + quantityRequired: number; +}; + +export type AllocationOrder = { + orderNumber: string; + name: string; + orderLines: Array; + orderDate: string; +}; From b68f1a1f3dfa8aa8b4884c2de7737ecf0c0530aa Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Thu, 4 Dec 2025 11:08:59 +0100 Subject: [PATCH 2/8] OBLS-308 distinguish picked lines --- .../PickUpAllocation/PickUpOrderScreen.tsx | 115 +++++++++++++++--- src/screens/PickUpAllocation/styles.ts | 4 +- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 036854e3..15d0f725 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -1,9 +1,10 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import React from 'react'; -import { ScrollView, View } from 'react-native'; +import { Alert, ScrollView, View } from 'react-native'; import { Button, List, Paragraph, TextInput, Title } from 'react-native-paper'; import EmptyView from '../../components/EmptyView'; +import Theme from '../../utils/Theme'; import styles from './styles'; import { AllocationOrder, AllocationOrderLine } from './types'; @@ -13,6 +14,39 @@ export function PickUpOrderScreen() { const { params } = useRoute(); const { order } = params; + const totalLines = order?.orderLines?.length ?? 0; + const [pickedLines, setPickedLines] = React.useState(0); + const [allPickedAlertShown, setAllPickedAlertShown] = React.useState(false); + + const handleLinePicked = React.useCallback(() => { + setPickedLines((prev) => prev + 1); + }, []); + + React.useEffect(() => { + if (totalLines > 0 && pickedLines === totalLines && !allPickedAlertShown) { + setAllPickedAlertShown(true); + showAllPickedDialog(); + } + }, [allPickedAlertShown, pickedLines, totalLines]); + + const showAllPickedDialog = () => { + Alert.alert( + 'All Lines Picked', + 'All lines for this order have been allocated.Would you like to self-pick this order?', + [ + { + text: 'No', + style: 'cancel', + onPress: () => {} + }, + { + text: 'Yes', + onPress: () => {} + } + ] + ); + }; + if (!order || !order.orderLines) { return ( {order.orderLines.map((line, index) => ( - + ))} @@ -37,43 +75,90 @@ export function PickUpOrderScreen() { ); } -function AllocationOrderItem({ orderLine }: { orderLine: AllocationOrderLine }) { - const [expanded, setExpanded] = React.useState(false); - const toggleExpanded = () => setExpanded(!expanded); +function AllocationOrderItem({ orderLine, onPicked }: { orderLine: AllocationOrderLine; onPicked: () => void }) { + const [expanded, setExpanded] = React.useState(true); + const [isPicked, setIsPicked] = React.useState(false); + + const toggleExpanded = () => { + if (!isPicked) { + setExpanded(!expanded); + } + }; const { product, quantityRequired } = orderLine; + function handleMarkPicked() { + setIsPicked(true); + setExpanded(false); + onPicked?.(); + } + return ( } + description={isPicked ? `Quantity Picked: ${quantityRequired}` : `Quantity Required: ${quantityRequired}`} + left={(props) => ( + + )} expanded={expanded} - style={styles.accordion} + // eslint-disable-next-line react-native/no-inline-styles + style={[styles.accordion, isPicked && { opacity: 0.5 }]} titleStyle={styles.accordionTitle} descriptionStyle={styles.accordionDescription} onPress={toggleExpanded} > - - - + {!isPicked && ( + + + + )} ); } -function OrderLineController() { +function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; orderLine: AllocationOrderLine }) { const [partialQuantity, setPartialQuantity] = React.useState(null); function handleConfirm() { - // Placeholder for confirm action + if ( + !partialQuantity || + partialQuantity > orderLine.quantityRequired || + partialQuantity === null || + partialQuantity <= 0 + ) { + Alert.alert('Invalid Quantity', `Please enter a valid partial quantity (1 - ${orderLine.quantityRequired}).`); + return; + } + + Alert.alert( + 'Confirm Partial Quantity', + `Are you sure you want to confirm a partial quantity of ${partialQuantity ?? 0}?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Confirm', + onPress: onPicked + } + ] + ); } function handleWarehousePick() { - // Placeholder for full warehouse pick action + Alert.alert('Full Warehouse Pick', 'Confirm full warehouse pick?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Confirm', onPress: onPicked } + ]); } function handleDisplayPick() { - // Placeholder for full display pick action + Alert.alert('Full Display Pick', 'Confirm full display pick?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Confirm', onPress: onPicked } + ]); } return ( diff --git a/src/screens/PickUpAllocation/styles.ts b/src/screens/PickUpAllocation/styles.ts index 39949c65..fbad67c6 100644 --- a/src/screens/PickUpAllocation/styles.ts +++ b/src/screens/PickUpAllocation/styles.ts @@ -8,7 +8,7 @@ export default StyleSheet.create({ padding: Theme.spacing.large }, titleText: { - fontSize: 24, + fontSize: 20, fontWeight: '600', marginBottom: Theme.spacing.small - 2 }, @@ -16,7 +16,7 @@ export default StyleSheet.create({ color: '#555' }, sectionDivider: { - marginTop: Theme.spacing.small + marginTop: Theme.spacing.small - 2 }, itemSeparator: { height: Theme.spacing.small - 2 From abe1aaefe79ddafc530a2a629fecb4af86ec7114 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Thu, 4 Dec 2025 11:13:49 +0100 Subject: [PATCH 3/8] OBLS-308 copilot review --- src/screens/PickUpAllocation/PickUpOrderScreen.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 15d0f725..62eb27cd 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -8,10 +8,10 @@ import Theme from '../../utils/Theme'; import styles from './styles'; import { AllocationOrder, AllocationOrderLine } from './types'; -type QuantityRouteProp = RouteProp<{ PickUpOrderScreen: { order: AllocationOrder } }, 'PickUpOrderScreen'>; +type PickUpOrderRouteProp = RouteProp<{ PickUpOrderScreen: { order: AllocationOrder } }, 'PickUpOrderScreen'>; export function PickUpOrderScreen() { - const { params } = useRoute(); + const { params } = useRoute(); const { order } = params; const totalLines = order?.orderLines?.length ?? 0; @@ -185,7 +185,10 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or placeholder="Enter Partial Qty" keyboardType="numeric" style={styles.input} - onChangeText={(text) => setPartialQuantity(text ? parseInt(text, 10) : null)} + onChangeText={(text) => { + const parsed = parseInt(text, 10); + setPartialQuantity(text && !isNaN(parsed) ? parsed : null); + }} /> From 901f613f487d73ac3914f24cc68a9779e6ac9cd3 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Fri, 12 Dec 2025 12:57:02 +0100 Subject: [PATCH 4/8] OBLS-308 add stock pick modal --- .../PickUpAllocation/PickUpOrderScreen.tsx | 120 +++++++++++++++++- src/screens/PickUpAllocation/styles.ts | 30 +++++ 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 62eb27cd..893b1726 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -1,7 +1,7 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import React from 'react'; -import { Alert, ScrollView, View } from 'react-native'; -import { Button, List, Paragraph, TextInput, Title } from 'react-native-paper'; +import { Alert, Modal, ScrollView, View } from 'react-native'; +import { Button, DataTable, Headline, List, Paragraph, TextInput, Title } from 'react-native-paper'; import EmptyView from '../../components/EmptyView'; import Theme from '../../utils/Theme'; @@ -122,6 +122,7 @@ function AllocationOrderItem({ orderLine, onPicked }: { orderLine: AllocationOrd function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; orderLine: AllocationOrderLine }) { const [partialQuantity, setPartialQuantity] = React.useState(null); + const [isStockPickOpen, setIsStockPickOpen] = React.useState(false); function handleConfirm() { if ( @@ -161,6 +162,10 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or ]); } + function handleStockPick() { + setIsStockPickOpen(true); + } + return ( @@ -170,6 +175,9 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or + @@ -204,6 +212,114 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or Confirm Quantity + + setIsStockPickOpen(false)} + onConfirm={onPicked} + /> ); } + +type StockRow = { + id: string; + binLocation: string; + availableQty: number; + onHandQty?: number; + pickedQty: string; +}; + +const MOCK_STOCK_ROWS: StockRow[] = [ + { id: 'A1', binLocation: 'WH-A1-01', availableQty: 12, pickedQty: '0' }, + { id: 'A2', binLocation: 'WH-A1-02', availableQty: 8, pickedQty: '0' }, + { id: 'B1', binLocation: 'DP-B1-01', availableQty: 5, pickedQty: '0' }, + { id: 'B2', binLocation: 'DP-B1-02', availableQty: 3, pickedQty: '0' }, + { id: 'C1', binLocation: 'BACK-C1', availableQty: 20, pickedQty: '0' }, + { id: 'C2', binLocation: 'BACK-C2', availableQty: 15, pickedQty: '0' }, + { id: 'D1', binLocation: 'FRONT-D1', availableQty: 6, pickedQty: '0' } +]; + +type StockPickModalProps = { + visible: boolean; + onDismiss: () => void; + onConfirm: () => void; + orderLine: AllocationOrderLine; +}; + +function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderLine }: StockPickModalProps) { + const [rows, setRows] = React.useState([]); + + /** + * Fetch available stock for the current order line item. + * For now, we are using mocked data until backend integration is ready. + */ + function fetchAvailableStock() { + // TODO: Replace with real API call + // eslint-disable-next-line no-restricted-syntax + console.log(orderLine); + setRows(MOCK_STOCK_ROWS); + } + + const fetchAvailableStockMemo = React.useCallback(fetchAvailableStock, [orderLine]); + + React.useEffect(() => { + if (visible) { + fetchAvailableStockMemo(); + } + }, [fetchAvailableStockMemo, visible]); + + function updateQty(id: string, value: string) { + setRows((prev) => prev.map((row) => (row.id === id ? { ...row, pickedQty: value } : row))); + } + + return ( + + + + Stock Pick + + + Product: {orderLine.product.productCode} | {orderLine.product.name} + + Quantity Picked: 0 / {orderLine.quantityRequired} + + + + Bin Location + Available + Picked + + + + {rows.map((row) => ( + + {row.binLocation} + {row.availableQty} + + updateQty(row.id, v)} + /> + + + ))} + + + + + + + + + + + ); +} diff --git a/src/screens/PickUpAllocation/styles.ts b/src/screens/PickUpAllocation/styles.ts index fbad67c6..90a31ee8 100644 --- a/src/screens/PickUpAllocation/styles.ts +++ b/src/screens/PickUpAllocation/styles.ts @@ -112,5 +112,35 @@ export default StyleSheet.create({ }, confirmButton: { alignSelf: 'stretch' + }, + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.5)' + }, + modalContent: { + width: '90%', + backgroundColor: 'white', + padding: Theme.spacing.large, + borderRadius: Theme.roundness * 2 + }, + scrollableContent: { + maxHeight: 360 + }, + cellInput: { + width: 70, + height: 25 + }, + actionButtons: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + marginTop: Theme.spacing.large + }, + leftMargin: { + marginLeft: Theme.spacing.small } }); From 69ee70ce3b433fb237ce1d9611b7a0131d5c01f1 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Fri, 19 Dec 2025 15:03:18 +0100 Subject: [PATCH 5/8] Connecting PUA screens with REST API --- src/apis/index.ts | 1 + src/apis/pua.ts | 24 ++ .../PickUpAllocation/PickUpEntryScreen.tsx | 45 +++- .../PickUpAllocation/PickUpOrderScreen.tsx | 230 ++++++++++++------ src/screens/PickUpAllocation/mock-data.ts | 153 ------------ src/screens/PickUpAllocation/types.ts | 20 +- 6 files changed, 239 insertions(+), 234 deletions(-) create mode 100644 src/apis/pua.ts delete mode 100644 src/screens/PickUpAllocation/mock-data.ts diff --git a/src/apis/index.ts b/src/apis/index.ts index bb57f100..4cc5fb38 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -10,3 +10,4 @@ export * from './lpn'; export * from './transfers'; export * from './others'; export * from './picking'; +export * from './pua'; diff --git a/src/apis/pua.ts b/src/apis/pua.ts new file mode 100644 index 00000000..090581ed --- /dev/null +++ b/src/apis/pua.ts @@ -0,0 +1,24 @@ +import apiClient from '../utils/ApiClient'; + +export function getOutboundOrders() { + let url = + '/stockMovements?exclude=lineItems&direction=OUTBOUND&requisitionStatusCode=VERIFYING' + + '&sort=dateCreated&order=asc&deliveryTypeCode=PICK_UP'; + if (global.location) { + url += '&origin=' + global.location.id; + } + + return apiClient.get(url); +} + +export function getOutboundOrderDetails(id: string) { + return apiClient.get(`/outbound-orders/${id}`); +} + +export function allocate(orderId: string, lineItemId: string, payload: any) { + return apiClient.post(`/outbound-orders/${orderId}/items/${lineItemId}/allocations`, payload); +} + +export function updateOrderStatus(orderId: string, status: string) { + return apiClient.post(`/stockMovements/${orderId}/status`, { status }); +} diff --git a/src/screens/PickUpAllocation/PickUpEntryScreen.tsx b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx index 44acc7c8..76052794 100644 --- a/src/screens/PickUpAllocation/PickUpEntryScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx @@ -1,16 +1,39 @@ -import React from 'react'; -import { FlatList, TouchableOpacity, View } from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Alert, FlatList, TouchableOpacity, View } from 'react-native'; import { Caption, Card, Chip, Divider, Paragraph, Title } from 'react-native-paper'; import { navigate } from '../../NavigationService'; -import { MOCKED_ORDERS } from './mock-data'; import styles from './styles'; import { AllocationOrder } from './types'; +import { getOutboundOrders } from '../../apis/pua'; +import { useFocusEffect } from '@react-navigation/native'; export function PickUpEntryScreen() { + const [orders, setOrders] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const fetchOrders = async () => { + try { + setIsLoading(true); + const response = await getOutboundOrders(); + setOrders(response.data || []); + } catch (error) { + Alert.alert('Error', 'Failed to fetch orders data'); + } finally { + setIsLoading(false); + } + }; + + // used useFocusEffect to refresh when screen is focused after nagivation + useFocusEffect( + useCallback(() => { + fetchOrders(); + }, []) + ); + return ( - Outstanding Orders ({MOCKED_ORDERS.length}) + Outstanding Orders ({orders.length}) Select an order from the list to start the pick-up allocation process. @@ -18,18 +41,20 @@ export function PickUpEntryScreen() { } - keyExtractor={(item: AllocationOrder) => item.orderNumber} + keyExtractor={(item: AllocationOrder) => item.id} numColumns={1} ItemSeparatorComponent={() => } + refreshing={isLoading} + onRefresh={fetchOrders} /> ); } function PickUpCard({ order }: { order: AllocationOrder }) { - const onPress = () => navigate('PickUpOrderScreen', { order }); + const onPress = () => navigate('PickUpOrderScreen', { orderId: order.id }); return ( @@ -37,10 +62,10 @@ function PickUpCard({ order }: { order: AllocationOrder }) { - {order.orderNumber} + {order.identifier} - {`Lines: ${order.orderLines.length}`} + {`Lines: ${order.lineItemCount}`} @@ -48,7 +73,7 @@ function PickUpCard({ order }: { order: AllocationOrder }) { {order.name} - {`Order Date: ${new Date(order.orderDate).toLocaleDateString('en-US', { + {`Order Date: ${new Date(order.dateTimeCreated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit', diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 893b1726..15a5662a 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -1,34 +1,68 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, Modal, ScrollView, View } from 'react-native'; import { Button, DataTable, Headline, List, Paragraph, TextInput, Title } from 'react-native-paper'; import EmptyView from '../../components/EmptyView'; import Theme from '../../utils/Theme'; import styles from './styles'; -import { AllocationOrder, AllocationOrderLine } from './types'; +import { AllocationOrderLine, AllocationStrategy, AvailableItem } from './types'; +import { allocate, getOutboundOrderDetails, updateOrderStatus } from '../../apis'; +import { navigate } from '../../NavigationService'; -type PickUpOrderRouteProp = RouteProp<{ PickUpOrderScreen: { order: AllocationOrder } }, 'PickUpOrderScreen'>; +type PickUpOrderRouteProp = RouteProp<{ PickUpOrderScreen: { orderId: string } }, 'PickUpOrderScreen'>; export function PickUpOrderScreen() { const { params } = useRoute(); - const { order } = params; + const { orderId } = params; - const totalLines = order?.orderLines?.length ?? 0; - const [pickedLines, setPickedLines] = React.useState(0); - const [allPickedAlertShown, setAllPickedAlertShown] = React.useState(false); + const [pickedLines, setPickedLines] = useState(0); + const [allPickedAlertShown, setAllPickedAlertShown] = useState(false); + + const [fullOrder, setFullOrder] = useState(null); const handleLinePicked = React.useCallback(() => { setPickedLines((prev) => prev + 1); }, []); - React.useEffect(() => { + useEffect(() => { + const fetchDetails = async () => { + try { + const response = await getOutboundOrderDetails(orderId); + setFullOrder(response.data || []); + } catch (error) { + Alert.alert('Error', 'Failed to load order details.'); + } + }; + + fetchDetails(); + }, [orderId]); + + const totalLines = fullOrder?.lineItems?.length ?? 0; + + useEffect(() => { if (totalLines > 0 && pickedLines === totalLines && !allPickedAlertShown) { setAllPickedAlertShown(true); showAllPickedDialog(); } }, [allPickedAlertShown, pickedLines, totalLines]); + const handleFinishAllocation = async (navigateToPicking: boolean) => { + try { + await updateOrderStatus(orderId, 'PICKING'); + + // 2. Przekierowanie w zależności od wyboru + if (navigateToPicking) { + navigate('PickingPickType'); + } else { + navigate('PickUpEntryScreen'); + } + } catch (error) { + console.error(error); + Alert.alert('Error', 'Failed to update order status.'); + } + }; + const showAllPickedDialog = () => { Alert.alert( 'All Lines Picked', @@ -37,17 +71,17 @@ export function PickUpOrderScreen() { { text: 'No', style: 'cancel', - onPress: () => {} + onPress: () => handleFinishAllocation(false) }, { text: 'Yes', - onPress: () => {} + onPress: () => handleFinishAllocation(true) } ] ); }; - if (!order || !order.orderLines) { + if (!fullOrder || !fullOrder.lineItems) { return ( - Order Line Allocation ({order.orderLines.length}) + Order Line Allocation ({fullOrder.lineItems.length}) - {order.orderLines.map((line, index) => ( + {fullOrder.lineItems.map((line, index) => ( ))} @@ -75,9 +110,17 @@ export function PickUpOrderScreen() { ); } -function AllocationOrderItem({ orderLine, onPicked }: { orderLine: AllocationOrderLine; onPicked: () => void }) { - const [expanded, setExpanded] = React.useState(true); - const [isPicked, setIsPicked] = React.useState(false); +function AllocationOrderItem({ + orderLine, + onPicked, + orderId +}: { + orderLine: AllocationOrderLine; + onPicked: () => void; + orderId: string; +}) { + const [expanded, setExpanded] = useState(true); + const [isPicked, setIsPicked] = useState(false); const toggleExpanded = () => { if (!isPicked) { @@ -113,16 +156,45 @@ function AllocationOrderItem({ orderLine, onPicked }: { orderLine: AllocationOrd > {!isPicked && ( - + )} ); } -function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; orderLine: AllocationOrderLine }) { - const [partialQuantity, setPartialQuantity] = React.useState(null); - const [isStockPickOpen, setIsStockPickOpen] = React.useState(false); +function OrderLineController({ + onPicked, + orderLine, + orderId +}: { + onPicked: () => void; + orderId: string; + orderLine: AllocationOrderLine; +}) { + const [partialQuantity, setPartialQuantity] = useState(null); + const [isStockPickOpen, setIsStockPickOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleAutoPick = async (strategy: AllocationStrategy) => { + try { + setIsSubmitting(true); + + const payload = { + mode: 'AUTO', + strategies: [strategy] + }; + + await allocate(orderId, orderLine.id, payload); + + Alert.alert('Success', 'Item allocated successfully', [{ text: 'OK', onPress: onPicked }]); + } catch (error) { + console.error(error); + Alert.alert('Error', 'Allocation failed'); + } finally { + setIsSubmitting(false); + } + }; function handleConfirm() { if ( @@ -151,14 +223,14 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or function handleWarehousePick() { Alert.alert('Full Warehouse Pick', 'Confirm full warehouse pick?', [ { text: 'Cancel', style: 'cancel' }, - { text: 'Confirm', onPress: onPicked } + { text: 'Confirm', onPress: () => handleAutoPick('WAREHOUSE_PICK') } ]); } function handleDisplayPick() { Alert.alert('Full Display Pick', 'Confirm full display pick?', [ { text: 'Cancel', style: 'cancel' }, - { text: 'Confirm', onPress: onPicked } + { text: 'Confirm', onPress: () => handleAutoPick('DISPLAY_PICK') } ]); } @@ -169,10 +241,23 @@ function OrderLineController({ onPicked, orderLine }: { onPicked: () => void; or return ( - - - diff --git a/src/screens/PickUpAllocation/mock-data.ts b/src/screens/PickUpAllocation/mock-data.ts deleted file mode 100644 index 2294c34f..00000000 --- a/src/screens/PickUpAllocation/mock-data.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { AllocationOrder } from './types'; - -const MOCKED_PRODUCTS = [ - { - id: 'P-001', - productCode: 'SKU-1001', - name: 'Wireless Mouse', - category: { - id: 'C-01', - name: 'Electronics' - }, - availability: { - quantityOnHand: { - value: 90, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityAvailableToPromise: { - value: 75, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityAllocated: { - value: 15, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityOnOrder: { - value: 30, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - } - }, - attributes: [], - productType: { - name: 'Standard' - }, - images: [], - description: 'High-precision wireless mouse with ergonomic design.', - pricePerUnit: 25.99, - quantityAllocated: 15, - quantityAvailableToPromise: 75, - quantityOnHand: 90, - quantityOnOrder: 30, - unitOfMeasure: 'pcs', - availableItems: [] - }, - { - id: 'P-002', - productCode: 'SKU-2002', - name: 'Bluetooth Keyboard', - category: { - id: 'C-01', - name: 'Electronics' - }, - availability: { - quantityOnHand: { - value: 70, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityAvailableToPromise: { - value: 60, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityAllocated: { - value: 10, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - }, - quantityOnOrder: { - value: 20, - unitOfMeasure: { - code: 'pcs', - name: 'pieces' - } - } - }, - attributes: [], - productType: { - name: 'Standard' - }, - images: [], - description: 'Compact Bluetooth keyboard suitable for tablets and laptops.', - pricePerUnit: 45.0, - quantityAllocated: 10, - quantityAvailableToPromise: 60, - quantityOnHand: 70, - quantityOnOrder: 20, - unitOfMeasure: 'pcs', - availableItems: [] - } -]; - -export const MOCKED_ORDERS: AllocationOrder[] = [ - { - orderNumber: 'ORD-001', - name: 'Frank Lee', - orderLines: [ - { product: MOCKED_PRODUCTS[0], quantityRequired: 10 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 5 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 3 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 5 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 4 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 2 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 1 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 8 } - ], - orderDate: '2026-06-15T10:30:00Z' - }, - { - orderNumber: 'ORD-002', - name: 'Grace Kim', - orderLines: [{ product: MOCKED_PRODUCTS[1], quantityRequired: 8 }], - orderDate: '2026-06-14T14:45:00Z' - }, - { - orderNumber: 'ORD-003', - name: 'Hannah Scott', - orderLines: [ - { product: MOCKED_PRODUCTS[0], quantityRequired: 4 }, - { product: MOCKED_PRODUCTS[1], quantityRequired: 4 } - ], - orderDate: '2026-06-13T09:15:00Z' - }, - { - orderNumber: 'ORD-004', - name: 'Ian Wright', - orderLines: [{ product: MOCKED_PRODUCTS[0], quantityRequired: 12 }], - orderDate: '2026-06-12T16:00:00Z' - }, - { - orderNumber: 'ORD-005', - name: 'Jane Foster', - orderLines: [{ product: MOCKED_PRODUCTS[1], quantityRequired: 7 }], - orderDate: '2026-06-11T11:20:00Z' - } -]; diff --git a/src/screens/PickUpAllocation/types.ts b/src/screens/PickUpAllocation/types.ts index 95612224..f1b2f59f 100644 --- a/src/screens/PickUpAllocation/types.ts +++ b/src/screens/PickUpAllocation/types.ts @@ -1,13 +1,27 @@ +import Location from '../../data/location/Location'; import Product from '../../data/product/Product'; +export type AvailableItem = { + _localId: string; + 'inventoryItem.id': string; + binLocation: Location; + quantityAvailable: number; + quantityPicked: string; +}; + export type AllocationOrderLine = { + id: string; product: Product; quantityRequired: number; + availableItems: Array; }; export type AllocationOrder = { - orderNumber: string; + id: string; name: string; - orderLines: Array; - orderDate: string; + identifier: string; + lineItemCount: number; + dateTimeCreated: string; }; + +export type AllocationStrategy = 'WAREHOUSE_PICK' | 'DISPLAY_PICK'; \ No newline at end of file From 2467cb1da23751700255038da28cd3ac125a06d9 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Mon, 22 Dec 2025 14:00:48 +0100 Subject: [PATCH 6/8] OBLS-308 Handled manual allocation, grouped bins by display and locations, code refactor --- src/data/location/Location.ts | 1 + .../PickUpAllocation/PickUpEntryScreen.tsx | 2 +- .../PickUpAllocation/PickUpOrderScreen.tsx | 259 ++++++++++++------ src/screens/PickUpAllocation/types.ts | 4 +- 4 files changed, 174 insertions(+), 92 deletions(-) diff --git a/src/data/location/Location.ts b/src/data/location/Location.ts index 0836973d..be8607df 100644 --- a/src/data/location/Location.ts +++ b/src/data/location/Location.ts @@ -18,6 +18,7 @@ interface Location { hasPartialReceivingSupport: boolean; locationType: LocationType; locationNumber: string; + isDisplay: boolean; } export default Location; diff --git a/src/screens/PickUpAllocation/PickUpEntryScreen.tsx b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx index 76052794..add625be 100644 --- a/src/screens/PickUpAllocation/PickUpEntryScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpEntryScreen.tsx @@ -24,7 +24,7 @@ export function PickUpEntryScreen() { } }; - // used useFocusEffect to refresh when screen is focused after nagivation + // useFocusEffect usedto refresh when screen is focused after nagivation useFocusEffect( useCallback(() => { fetchOrders(); diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 15a5662a..43694742 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -16,13 +16,13 @@ export function PickUpOrderScreen() { const { params } = useRoute(); const { orderId } = params; - const [pickedLines, setPickedLines] = useState(0); - const [allPickedAlertShown, setAllPickedAlertShown] = useState(false); + const [allocatedLines, setAllocatedLines] = useState(0); + const [allAllocatedAlertShown, setAllAllocatedAlertShown] = useState(false); const [fullOrder, setFullOrder] = useState(null); - const handleLinePicked = React.useCallback(() => { - setPickedLines((prev) => prev + 1); + const handleLineAllocated = React.useCallback(() => { + setAllocatedLines((prev) => prev + 1); }, []); useEffect(() => { @@ -41,24 +41,22 @@ export function PickUpOrderScreen() { const totalLines = fullOrder?.lineItems?.length ?? 0; useEffect(() => { - if (totalLines > 0 && pickedLines === totalLines && !allPickedAlertShown) { - setAllPickedAlertShown(true); + if (totalLines > 0 && allocatedLines === totalLines && !allAllocatedAlertShown) { + setAllAllocatedAlertShown(true); showAllPickedDialog(); } - }, [allPickedAlertShown, pickedLines, totalLines]); + }, [allAllocatedAlertShown, allocatedLines, totalLines]); const handleFinishAllocation = async (navigateToPicking: boolean) => { try { await updateOrderStatus(orderId, 'PICKING'); - // 2. Przekierowanie w zależności od wyboru if (navigateToPicking) { navigate('PickingPickType'); } else { navigate('PickUpEntryScreen'); } } catch (error) { - console.error(error); Alert.alert('Error', 'Failed to update order status.'); } }; @@ -66,7 +64,7 @@ export function PickUpOrderScreen() { const showAllPickedDialog = () => { Alert.alert( 'All Lines Picked', - 'All lines for this order have been allocated.Would you like to self-pick this order?', + 'All lines for this order have been allocated. Would you like to self-pick this order?', [ { text: 'No', @@ -101,7 +99,7 @@ export function PickUpOrderScreen() { key={`${line.product.productCode}-${index}`} orderLine={line} orderId={orderId} - onPicked={handleLinePicked} + onAllocated={handleLineAllocated} /> ))} @@ -112,51 +110,51 @@ export function PickUpOrderScreen() { function AllocationOrderItem({ orderLine, - onPicked, + onAllocated, orderId }: { orderLine: AllocationOrderLine; - onPicked: () => void; + onAllocated: () => void; orderId: string; }) { const [expanded, setExpanded] = useState(true); - const [isPicked, setIsPicked] = useState(false); + const [isAllocated, setIsAllocated] = useState(false); const toggleExpanded = () => { - if (!isPicked) { + if (!isAllocated) { setExpanded(!expanded); } }; const { product, quantityRequired } = orderLine; - function handleMarkPicked() { - setIsPicked(true); + function handleMarkAllocated() { + setIsAllocated(true); setExpanded(false); - onPicked?.(); + onAllocated?.(); } return ( ( )} expanded={expanded} // eslint-disable-next-line react-native/no-inline-styles - style={[styles.accordion, isPicked && { opacity: 0.5 }]} + style={[styles.accordion, isAllocated && { opacity: 0.5 }]} titleStyle={styles.accordionTitle} descriptionStyle={styles.accordionDescription} onPress={toggleExpanded} > - {!isPicked && ( + {!isAllocated && ( - + )} @@ -164,11 +162,11 @@ function AllocationOrderItem({ } function OrderLineController({ - onPicked, + onAllocated, orderLine, orderId }: { - onPicked: () => void; + onAllocated: () => void; orderId: string; orderLine: AllocationOrderLine; }) { @@ -187,15 +185,52 @@ function OrderLineController({ await allocate(orderId, orderLine.id, payload); - Alert.alert('Success', 'Item allocated successfully', [{ text: 'OK', onPress: onPicked }]); + Alert.alert('Success', 'Item allocated successfully', [{ text: 'OK', onPress: onAllocated }]); } catch (error) { - console.error(error); Alert.alert('Error', 'Allocation failed'); } finally { setIsSubmitting(false); } }; + const handleManualAllocation = async (rows: AvailableItem[]) => { + try { + setIsStockPickOpen(false); + setIsSubmitting(true); + + const itemsToAllocate = rows.filter((row) => { + const qty = parseInt(row.quantityAllocated, 10); + return !isNaN(qty) && qty > 0; + }); + + if (itemsToAllocate.length === 0) { + Alert.alert('No items selected', 'Please enter a quantity to allocate.'); + setIsSubmitting(false); + setIsStockPickOpen(true); + return; + } + + const allocationsPayload = itemsToAllocate.map((row) => ({ + inventoryItemId: row['inventoryItem.id'], + binLocationId: row.binLocation.id, + quantity: parseInt(row.quantityAllocated, 10) + })); + + const payload = { + mode: 'MANUAL', + allocations: allocationsPayload + }; + + await allocate(orderId, orderLine.id, payload); + + Alert.alert('Success', 'Manual allocation saved', [{ text: 'OK', onPress: onAllocated }]); + } catch (error) { + Alert.alert('Error', 'Manual allocation failed'); + } finally { + setIsSubmitting(false); + } + }; + function handleConfirm() { if ( !partialQuantity || @@ -214,7 +249,7 @@ function OrderLineController({ { text: 'Cancel', style: 'cancel' }, { text: 'Confirm', - onPress: onPicked + onPress: onAllocated } ] ); @@ -223,14 +258,14 @@ function OrderLineController({ function handleWarehousePick() { Alert.alert('Full Warehouse Pick', 'Confirm full warehouse pick?', [ { text: 'Cancel', style: 'cancel' }, - { text: 'Confirm', onPress: () => handleAutoPick('WAREHOUSE_PICK') } + { text: 'Confirm', onPress: () => handleAutoPick('WAREHOUSE_FIRST') } ]); } function handleDisplayPick() { Alert.alert('Full Display Pick', 'Confirm full display pick?', [ { text: 'Cancel', style: 'cancel' }, - { text: 'Confirm', onPress: () => handleAutoPick('DISPLAY_PICK') } + { text: 'Confirm', onPress: () => handleAutoPick('DISPLAY_FIRST') } ]); } @@ -265,44 +300,47 @@ function OrderLineController({ - - {/* Label + Input */} - - Enter Partial Quantity From Display: - - - { - const parsed = parseInt(text, 10); - setPartialQuantity(text && !isNaN(parsed) ? parsed : null); - }} - /> + {/* Temporarily hidden because it probably won't be needed. To be removed/shown later. */} + {false && ( + + {/* Label + Input */} + + Enter Partial Quantity From Display: + + + { + const parsed = parseInt(text, 10); + setPartialQuantity(text && !isNaN(parsed) ? parsed : null); + }} + /> + - - {/* Confirm button */} - - + {/* Confirm button */} + + + )} setIsStockPickOpen(false)} - onConfirm={onPicked} + onConfirm={handleManualAllocation} /> ); @@ -311,7 +349,7 @@ function OrderLineController({ type StockPickModalProps = { visible: boolean; onDismiss: () => void; - onConfirm: () => void; + onConfirm: (rows: AvailableItem[]) => void; orderLine: AllocationOrderLine; }; @@ -320,25 +358,27 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL useEffect(() => { if (visible && orderLine?.availableItems) { - const preparedData = orderLine.availableItems.map((item, index) => ({ + const filteredItems = orderLine.availableItems.filter((item) => item.quantityAvailable > 0); + + const preparedData = filteredItems.map((item, index) => ({ ...item, _localId: `loc-${item.binLocation?.id || 'null'}-idx-${index}`, - quantityPicked: item.quantityPicked ? String(item.quantityPicked) : '0' + quantityPicked: item.quantityAllocated ? String(item.quantityAllocated) : '0' })); setRows(preparedData); } }, [visible, orderLine]); - const totalPicked = rows.reduce((sum, row) => { - const qty = parseInt(row.quantityPicked, 10); + const totalAllocated = rows.reduce((sum, row) => { + const qty = parseInt(row.quantityAllocated, 10); return sum + (isNaN(qty) ? 0 : qty); }, 0); - const isQuantityRequiredExceeded = totalPicked > orderLine.quantityRequired; + const isQuantityRequiredExceeded = totalAllocated > orderLine.quantityRequired; function updateQty(localId: string, text: string) { if (text === '') { - setRows((prev) => prev.map((row) => (row._localId === localId ? { ...row, quantityPicked: '' } : row))); + setRows((prev) => prev.map((row) => (row._localId === localId ? { ...row, quantityAllocated: '' } : row))); return; } @@ -353,13 +393,37 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL if (row._localId === localId) { const cappedValue = newValue > row.quantityAvailable ? row.quantityAvailable : newValue; - return { ...row, quantityPicked: String(cappedValue) }; + return { ...row, quantityAllocated: String(cappedValue) }; } return row; }) ); } + const displayLocations = rows.filter((row) => row.binLocation?.isDisplay); + const warehouseLocations = rows.filter((row) => !row.binLocation?.isDisplay); + + const renderRowsGroup = (items: AvailableItem[]) => { + return items.map((row) => ( + + {row.binLocation?.locationNumber ?? 'Default'} + + {row.quantityAvailable} + + + updateQty(row._localId, v)} + /> + + + )); + }; + return ( @@ -376,39 +440,56 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL color: isQuantityRequiredExceeded ? Theme.colors.error : Theme.colors.text }} > - Quantity Picked: {totalPicked} / {orderLine.quantityRequired} + Quantity Allocated: {totalAllocated} / {orderLine.quantityRequired} Bin Location Available - Picked + Allocated - {rows.map((row) => ( - - {row.binLocation?.locationNumber ?? 'Default'} - {row.quantityAvailable} - - updateQty(row._localId, v)} - /> - - - ))} + {displayLocations.length > 0 && ( + <> + + Display + + {renderRowsGroup(displayLocations)} + + )} + + {warehouseLocations.length > 0 && ( + <> + 0 ? 8 : 0 + }} + > + Warehouse + + {renderRowsGroup(warehouseLocations)} + + )} + + {rows.length === 0 && ( + No available stock found. + )} - diff --git a/src/screens/PickUpAllocation/types.ts b/src/screens/PickUpAllocation/types.ts index f1b2f59f..cf1fc333 100644 --- a/src/screens/PickUpAllocation/types.ts +++ b/src/screens/PickUpAllocation/types.ts @@ -6,7 +6,7 @@ export type AvailableItem = { 'inventoryItem.id': string; binLocation: Location; quantityAvailable: number; - quantityPicked: string; + quantityAllocated: string; }; export type AllocationOrderLine = { @@ -24,4 +24,4 @@ export type AllocationOrder = { dateTimeCreated: string; }; -export type AllocationStrategy = 'WAREHOUSE_PICK' | 'DISPLAY_PICK'; \ No newline at end of file +export type AllocationStrategy = 'WAREHOUSE_FIRST' | 'DISPLAY_FIRST'; From 053b9a2f6fdfd8fc6db5e501454e6115f52feaf3 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Wed, 7 Jan 2026 18:28:03 +0100 Subject: [PATCH 7/8] OBLS-308 Adjusted pick stock modal to handle allocation quantity and statuses --- .../PickUpAllocation/PickUpOrderScreen.tsx | 131 +++++++++++------- src/screens/PickUpAllocation/types.ts | 4 + src/utils/Theme.ts | 2 +- 3 files changed, 83 insertions(+), 54 deletions(-) diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index 43694742..c4bdd7cc 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -1,5 +1,5 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Alert, Modal, ScrollView, View } from 'react-native'; import { Button, DataTable, Headline, List, Paragraph, TextInput, Title } from 'react-native-paper'; @@ -16,52 +16,40 @@ export function PickUpOrderScreen() { const { params } = useRoute(); const { orderId } = params; - const [allocatedLines, setAllocatedLines] = useState(0); const [allAllocatedAlertShown, setAllAllocatedAlertShown] = useState(false); - const [fullOrder, setFullOrder] = useState(null); - const handleLineAllocated = React.useCallback(() => { - setAllocatedLines((prev) => prev + 1); - }, []); - - useEffect(() => { - const fetchDetails = async () => { - try { - const response = await getOutboundOrderDetails(orderId); - setFullOrder(response.data || []); - } catch (error) { - Alert.alert('Error', 'Failed to load order details.'); - } - }; - - fetchDetails(); + const fetchDetails = useCallback(async () => { + try { + const response = await getOutboundOrderDetails(orderId); + setFullOrder(response.data || []); + } catch (error) { + Alert.alert('Error', 'Failed to load order details.'); + } }, [orderId]); - const totalLines = fullOrder?.lineItems?.length ?? 0; - useEffect(() => { - if (totalLines > 0 && allocatedLines === totalLines && !allAllocatedAlertShown) { - setAllAllocatedAlertShown(true); - showAllPickedDialog(); - } - }, [allAllocatedAlertShown, allocatedLines, totalLines]); + fetchDetails(); + }, [fetchDetails]); - const handleFinishAllocation = async (navigateToPicking: boolean) => { - try { - await updateOrderStatus(orderId, 'PICKING'); + const handleFinishAllocation = useCallback( + async (navigateToPicking: boolean) => { + try { + await updateOrderStatus(orderId, 'PICKING'); - if (navigateToPicking) { - navigate('PickingPickType'); - } else { - navigate('PickUpEntryScreen'); + if (navigateToPicking) { + navigate('PickingPickType'); + } else { + navigate('PickUpEntryScreen'); + } + } catch (error) { + Alert.alert('Error', 'Failed to update order status.'); } - } catch (error) { - Alert.alert('Error', 'Failed to update order status.'); - } - }; + }, + [orderId] + ); - const showAllPickedDialog = () => { + const showAllPickedDialog = useCallback(() => { Alert.alert( 'All Lines Picked', 'All lines for this order have been allocated. Would you like to self-pick this order?', @@ -77,7 +65,32 @@ export function PickUpOrderScreen() { } ] ); - }; + }, [handleFinishAllocation]); + + const handleLineAllocated = useCallback(() => { + fetchDetails(); + }, [fetchDetails]); + + useEffect(() => { + if (!fullOrder || !fullOrder.lineItems || fullOrder.lineItems.length === 0) { + return; + } + + const areAllLinesFullyAllocated = fullOrder.lineItems.every((line: any) => { + const allocated = line.quantityAllocated || 0; + const required = line.quantityRequired || 0; + return allocated >= required; + }); + + if (areAllLinesFullyAllocated && !allAllocatedAlertShown) { + setAllAllocatedAlertShown(true); + showAllPickedDialog(); + } + + if (!areAllLinesFullyAllocated && allAllocatedAlertShown) { + setAllAllocatedAlertShown(false); + } + }, [fullOrder, allAllocatedAlertShown, showAllPickedDialog]); if (!fullOrder || !fullOrder.lineItems) { return ( @@ -117,19 +130,24 @@ function AllocationOrderItem({ onAllocated: () => void; orderId: string; }) { - const [expanded, setExpanded] = useState(true); - const [isAllocated, setIsAllocated] = useState(false); + const { product, quantityRequired, quantityAllocated } = orderLine; - const toggleExpanded = () => { - if (!isAllocated) { - setExpanded(!expanded); + const isFullyAllocated = (quantityAllocated || 0) >= (quantityRequired || 0); + const isPartiallyAllocated = (quantityAllocated || 0) > 0 && (quantityAllocated || 0) < (quantityRequired || 0); + + const [expanded, setExpanded] = useState(!isFullyAllocated); + + useEffect(() => { + if (isFullyAllocated) { + setExpanded(false); } - }; + }, [isFullyAllocated]); - const { product, quantityRequired } = orderLine; + const toggleExpanded = () => { + setExpanded(!expanded); + }; function handleMarkAllocated() { - setIsAllocated(true); setExpanded(false); onAllocated?.(); } @@ -137,22 +155,26 @@ function AllocationOrderItem({ return ( ( )} expanded={expanded} // eslint-disable-next-line react-native/no-inline-styles - style={[styles.accordion, isAllocated && { opacity: 0.5 }]} + style={[styles.accordion, isFullyAllocated && { opacity: 0.5 }]} titleStyle={styles.accordionTitle} descriptionStyle={styles.accordionDescription} onPress={toggleExpanded} > - {!isAllocated && ( + {!isFullyAllocated && ( @@ -363,13 +385,15 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL const preparedData = filteredItems.map((item, index) => ({ ...item, _localId: `loc-${item.binLocation?.id || 'null'}-idx-${index}`, - quantityPicked: item.quantityAllocated ? String(item.quantityAllocated) : '0' + quantityAllocated: item.quantityAllocated ? String(item.quantityAllocated) : '0' })); setRows(preparedData); } }, [visible, orderLine]); - const totalAllocated = rows.reduce((sum, row) => { + let totalAllocated = orderLine.quantityAllocated; + + totalAllocated += rows.reduce((sum, row) => { const qty = parseInt(row.quantityAllocated, 10); return sum + (isNaN(qty) ? 0 : qty); }, 0); @@ -477,6 +501,7 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL )} {rows.length === 0 && ( + // eslint-disable-next-line react-native/no-inline-styles No available stock found. )} diff --git a/src/screens/PickUpAllocation/types.ts b/src/screens/PickUpAllocation/types.ts index cf1fc333..d8112537 100644 --- a/src/screens/PickUpAllocation/types.ts +++ b/src/screens/PickUpAllocation/types.ts @@ -13,7 +13,9 @@ export type AllocationOrderLine = { id: string; product: Product; quantityRequired: number; + quantityAllocated: number; availableItems: Array; + allocationStatus: AllocationStatus; }; export type AllocationOrder = { @@ -25,3 +27,5 @@ export type AllocationOrder = { }; export type AllocationStrategy = 'WAREHOUSE_FIRST' | 'DISPLAY_FIRST'; + +export type AllocationStatus = 'ALLOCATED' | 'PARTIALLY_ALLOCATED' | 'UNALLOCATED'; diff --git a/src/utils/Theme.ts b/src/utils/Theme.ts index 60d29c43..38785831 100644 --- a/src/utils/Theme.ts +++ b/src/utils/Theme.ts @@ -12,7 +12,7 @@ export default { colors: { ...DefaultTheme.colors, primary: '#20345c', - warning: '#FCFFC1', + warning: '#f7812d', warningText: '#8a6d3b', danger: '#FF5630', success: '#22bb33', From db0257f356fedeff350162a6cc4e8f7b3f3a5853 Mon Sep 17 00:00:00 2001 From: druchniewicz Date: Fri, 9 Jan 2026 01:08:54 +0100 Subject: [PATCH 8/8] OBLS-308 Adjusted to the latest backend changes --- .../PickUpAllocation/PickUpOrderScreen.tsx | 49 +++++++++++++++---- src/screens/PickUpAllocation/types.ts | 9 ++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx index c4bdd7cc..6ee2a1db 100644 --- a/src/screens/PickUpAllocation/PickUpOrderScreen.tsx +++ b/src/screens/PickUpAllocation/PickUpOrderScreen.tsx @@ -233,6 +233,7 @@ function OrderLineController({ } const allocationsPayload = itemsToAllocate.map((row) => ({ + id: row.id, inventoryItemId: row['inventoryItem.id'], binLocationId: row.binLocation.id, quantity: parseInt(row.quantityAllocated, 10) @@ -380,20 +381,49 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL useEffect(() => { if (visible && orderLine?.availableItems) { - const filteredItems = orderLine.availableItems.filter((item) => item.quantityAvailable > 0); + const allocationsMap = new Map(); + + if (orderLine.allocations) { + orderLine.allocations.forEach((allocation: any) => { + const invId = allocation.inventoryItemId || ''; + const binId = allocation.binLocationId || ''; + const key = `${invId}-${binId}`; + allocationsMap.set(key, allocation); + }); + } + + const filteredItems = orderLine.availableItems.filter((item) => { + const invId = item['inventoryItem.id'] || ''; + const binId = item.binLocation?.id || ''; + const key = `${invId}-${binId}`; + + const hasAllocation = allocationsMap.has(key) && allocationsMap.get(key) > 0; + const hasAvailability = item.quantityAvailable > 0; + return hasAvailability || hasAllocation; + }); + + const preparedData = filteredItems.map((item, index) => { + const invId = item['inventoryItem.id'] || ''; + const binId = item.binLocation?.id || ''; + const key = `${invId}-${binId}`; + + const existingAllocation = allocationsMap.get(key); + const savedQuantity = existingAllocation ? existingAllocation.quantity : 0; + const allocationId = existingAllocation ? existingAllocation.id : undefined; + + return { + ...item, + _localId: `loc-${item.binLocation?.id || 'null'}-idx-${index}`, + id: allocationId, + quantityAllocated: savedQuantity ? String(savedQuantity) : '' + }; + }); - const preparedData = filteredItems.map((item, index) => ({ - ...item, - _localId: `loc-${item.binLocation?.id || 'null'}-idx-${index}`, - quantityAllocated: item.quantityAllocated ? String(item.quantityAllocated) : '0' - })); setRows(preparedData); } }, [visible, orderLine]); - let totalAllocated = orderLine.quantityAllocated; - - totalAllocated += rows.reduce((sum, row) => { + const totalAllocated = rows.reduce((sum, row) => { const qty = parseInt(row.quantityAllocated, 10); return sum + (isNaN(qty) ? 0 : qty); }, 0); @@ -407,7 +437,6 @@ function StockPickModal({ visible, onDismiss: onClose, onConfirm: onSave, orderL } const newValue = parseInt(text, 10); - if (isNaN(newValue)) { return; } diff --git a/src/screens/PickUpAllocation/types.ts b/src/screens/PickUpAllocation/types.ts index d8112537..d4b0d2a9 100644 --- a/src/screens/PickUpAllocation/types.ts +++ b/src/screens/PickUpAllocation/types.ts @@ -3,6 +3,7 @@ import Product from '../../data/product/Product'; export type AvailableItem = { _localId: string; + id?: string; 'inventoryItem.id': string; binLocation: Location; quantityAvailable: number; @@ -16,6 +17,7 @@ export type AllocationOrderLine = { quantityAllocated: number; availableItems: Array; allocationStatus: AllocationStatus; + allocations: Array; }; export type AllocationOrder = { @@ -29,3 +31,10 @@ export type AllocationOrder = { export type AllocationStrategy = 'WAREHOUSE_FIRST' | 'DISPLAY_FIRST'; export type AllocationStatus = 'ALLOCATED' | 'PARTIALLY_ALLOCATED' | 'UNALLOCATED'; + +export type AllocationDto = { + id?: string; + inventoryItemId: string; + binLocationId: string; + quantity: number; +};