| @@ -462,6 +462,7 @@ export interface LaneBtn { | |||
| loadingSequence?: number | null; | |||
| unassigned: number; | |||
| total: number; | |||
| handlerName: string; | |||
| } | |||
| export interface QrPickBatchSubmitRequest { | |||
| @@ -804,6 +805,7 @@ export const fetchFGPickOrdersByUserId = async (userId: number) => { | |||
| }; | |||
| /** DO workbench: FG headers from `delivery_order_pick_order`, not `do_pick_order_line`. */ | |||
| /* | |||
| export const fetchFGPickOrdersByUserIdWorkbench = async (userId: number) => { | |||
| return serverFetchJson<FGPickOrderResponse[]>( | |||
| @@ -1190,6 +1192,7 @@ export const fetchAllPickOrderLotsHierarchical = cache(async (userId: number): P | |||
| }); | |||
| /** DO workbench: hierarchical lots where header is `delivery_order_pick_order`. */ | |||
| /* | |||
| export const fetchAllPickOrderLotsHierarchicalWorkbench = cache(async (userId: number): Promise<any> => { | |||
| try { | |||
| const data = await serverFetchJson<any>( | |||
| @@ -1208,6 +1211,7 @@ export const fetchAllPickOrderLotsHierarchicalWorkbench = cache(async (userId: n | |||
| }; | |||
| } | |||
| }); | |||
| */ | |||
| export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId: number): Promise<{ | |||
| fgInfo: any; | |||
| pickOrders: any[]; | |||
| @@ -601,39 +601,55 @@ const getDateLabel = (offset: number) => { | |||
| flexWrap="wrap" | |||
| sx={{ flex: 1, gap: 1 }} | |||
| > | |||
| {slots.map((slot) => ( | |||
| <Button | |||
| key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.lane.truckLanceCode}-${slot.truckDepartureTime}`} | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={slot.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => | |||
| handleLaneButtonClick( | |||
| "4/F", | |||
| slot.truckDepartureTime, | |||
| slot.lane.truckLanceCode, | |||
| slot.lane.loadingSequence ?? null, | |||
| selectedDate, | |||
| slot.lane.unassigned, | |||
| slot.lane.total | |||
| ) | |||
| } | |||
| sx={{ | |||
| fontSize: "1rem", | |||
| py: 0.75, | |||
| px: 1.25, | |||
| borderWidth: 1, | |||
| borderColor: "#ccc", | |||
| fontWeight: 500, | |||
| "&:hover": { | |||
| borderColor: "#999", | |||
| backgroundColor: "#f5f5f5", | |||
| }, | |||
| }} | |||
| > | |||
| {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})`} | |||
| </Button> | |||
| ))} | |||
| {slots.map((slot) => { | |||
| const hasHandler = | |||
| !!slot.lane.handlerName?.trim() && slot.lane.total > slot.lane.unassigned; | |||
| return ( | |||
| <Button | |||
| key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.lane.truckLanceCode}-${slot.truckDepartureTime}`} | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={slot.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => | |||
| handleLaneButtonClick( | |||
| "4/F", | |||
| slot.truckDepartureTime, | |||
| slot.lane.truckLanceCode, | |||
| slot.lane.loadingSequence ?? null, | |||
| selectedDate, | |||
| slot.lane.unassigned, | |||
| slot.lane.total | |||
| ) | |||
| } | |||
| sx={{ | |||
| fontSize: "1rem", | |||
| py: 0.75, | |||
| px: 1.25, | |||
| borderWidth: 1, | |||
| borderColor: hasHandler ? "#f59e0b" : "#ccc", | |||
| backgroundColor: hasHandler ? "#fff7e6" : "#fff", | |||
| color: hasHandler ? "#1f2937" : "primary.main", | |||
| fontWeight: 500, | |||
| "&:hover": { | |||
| borderColor: hasHandler ? "#d97706" : "#999", | |||
| backgroundColor: hasHandler ? "#ffefcc" : "#f5f5f5", | |||
| }, | |||
| }} | |||
| > | |||
| <Box sx={{ display: "flex", alignItems: "center", gap: 0.75 }}> | |||
| <Typography component="span" sx={{ fontSize: "1rem", color: "inherit", fontWeight: 500 }}> | |||
| {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})`} | |||
| </Typography> | |||
| {hasHandler && ( | |||
| <Typography component="span" sx={{ fontSize: "1rem", fontWeight: 700, color: "#d97706" }}> | |||
| {slot.lane.handlerName!.trim()} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| </Button> | |||
| ); | |||
| })} | |||
| </Stack> | |||
| </Stack> | |||
| </Grid> | |||
| @@ -241,16 +241,24 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||
| // Handle quantity change in search results | |||
| const handleSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { | |||
| const getClampedQty = (qty: number | null, stock?: number) => { | |||
| if (qty === null) return null; | |||
| const maxQty = Math.max(0, stock ?? 0); | |||
| return Math.max(1, Math.min(qty, maxQty)); | |||
| }; | |||
| setFilteredItems(prev => | |||
| prev.map(item => | |||
| item.id === itemId ? { ...item, qty: newQty } : item | |||
| item.id === itemId ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) } : item | |||
| ) | |||
| ); | |||
| // Auto-update created items if this item exists there | |||
| setCreatedItems(prev => | |||
| prev.map(item => | |||
| item.itemId === itemId ? { ...item, qty: newQty || 1 } : item | |||
| item.itemId === itemId | |||
| ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) || 1 } | |||
| : item | |||
| ) | |||
| ); | |||
| }, []); | |||
| @@ -297,7 +305,12 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||
| const handleQtyChange = useCallback((itemId: number, newQty: number) => { | |||
| setCreatedItems(prev => | |||
| prev.map(item => | |||
| item.itemId === itemId ? { ...item, qty: newQty } : item | |||
| item.itemId === itemId | |||
| ? { | |||
| ...item, | |||
| qty: Math.max(1, Math.min(newQty, Math.max(0, item.currentStockBalance ?? 0))), | |||
| } | |||
| : item | |||
| ) | |||
| ); | |||
| }, []); | |||
| @@ -567,6 +580,15 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| alert(t("Please select at least one item to submit")); | |||
| return; | |||
| } | |||
| const invalidQtyItems = selectedCreatedItems.filter((item) => { | |||
| const stock = item.currentStockBalance ?? 0; | |||
| return item.qty <= 0 || item.qty > stock; | |||
| }); | |||
| if (invalidQtyItems.length > 0) { | |||
| alert(t("Order quantity cannot exceed available stock.")); | |||
| return; | |||
| } | |||
| // 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||
| // if (!data.type) { | |||
| @@ -946,6 +968,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| max: item.currentStockBalance || 0, | |||
| step: 1, | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| @@ -1033,6 +1056,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| max: item.currentStockBalance || 0, | |||
| step: 1, | |||
| style: { textAlign: 'center' } // Center the text | |||
| }} | |||
| @@ -1118,9 +1142,15 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| // 添加数量变更处理函数 | |||
| const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { | |||
| const getClampedQty = (qty: number | null, stock?: number) => { | |||
| if (qty === null) return null; | |||
| const maxQty = Math.max(0, stock ?? 0); | |||
| return Math.max(1, Math.min(qty, maxQty)); | |||
| }; | |||
| setSecondSearchResults(prev => | |||
| prev.map(item => | |||
| item.id === itemId ? { ...item, qty: newQty } : item | |||
| item.id === itemId ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) } : item | |||
| ) | |||
| ); | |||
| @@ -1254,6 +1284,8 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| } | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| max: item.currentStockBalance || 0, | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| sx={{ | |||
| @@ -1797,6 +1829,8 @@ const CustomSearchResultsTable = () => { | |||
| handleQtyBlur(item.id); | |||
| }} | |||
| inputProps={{ | |||
| min: 1, | |||
| max: item.currentStockBalance || 0, | |||
| style: { textAlign: 'center' } | |||
| }} | |||
| sx={{ | |||