| @@ -76,7 +76,129 @@ export interface JobOrderDetail { | |||
| pickLines: any[]; | |||
| status: string; | |||
| } | |||
| export interface UnassignedJobOrderPickOrder { | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| pickOrderConsoCode: string; | |||
| pickOrderTargetDate: string; | |||
| pickOrderStatus: string; | |||
| jobOrderId: number; | |||
| jobOrderCode: string; | |||
| jobOrderName: string; | |||
| reqQty: number; | |||
| uom: string; | |||
| planStart: string; | |||
| planEnd: string; | |||
| } | |||
| export interface AssignJobOrderResponse { | |||
| id: number | null; | |||
| code: string | null; | |||
| name: string | null; | |||
| type: string | null; | |||
| message: string | null; | |||
| errorPosition: string | null; | |||
| } | |||
| export const recordSecondScanIssue = cache(async ( | |||
| pickOrderId: number, | |||
| itemId: number, | |||
| data: { | |||
| qty: number; | |||
| isMissing: boolean; | |||
| isBad: boolean; | |||
| reason: string; | |||
| createdBy: number; | |||
| } | |||
| ) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(data), | |||
| next: { tags: ["jo-second-scan"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`, | |||
| { | |||
| method: "POST", | |||
| next: { tags: ["jo-second-scan"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| export const submitSecondScanQuantity = cache(async ( | |||
| pickOrderId: number, | |||
| itemId: number, | |||
| data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string } | |||
| ) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(data), | |||
| next: { tags: ["jo-second-scan"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| // 获取未分配的 Job Order pick orders | |||
| export const fetchUnassignedJobOrderPickOrders = cache(async () => { | |||
| return serverFetchJson<UnassignedJobOrderPickOrder[]>( | |||
| `${BASE_API_URL}/jo/unassigned-job-order-pick-orders`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["jo-unassigned"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| // 分配 Job Order pick order 给用户 | |||
| export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => { | |||
| return serverFetchJson<AssignJobOrderResponse>( | |||
| `${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| } | |||
| ); | |||
| }; | |||
| // 获取 Job Order 分层数据 | |||
| export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["jo-hierarchical"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| // 获取已完成的 Job Order pick orders | |||
| export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["jo-completed"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| // 获取已完成的 Job Order pick order records | |||
| export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { | |||
| return serverFetchJson<any[]>( | |||
| `${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["jo-records"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| export const fetchJobOrderDetailByCode = cache(async (code: string) => { | |||
| return serverFetchJson<JobOrderDetail>( | |||
| `${BASE_API_URL}/jo/detailByCode/${code}`, | |||
| @@ -59,7 +59,7 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| import GoodPickExecutionForm from "./GoodPickExecutionForm"; | |||
| import GoodPickExecutionForm from "./JobPickExecutionForm"; | |||
| import FGPickOrderCard from "./FGPickOrderCard"; | |||
| interface Props { | |||
| @@ -99,7 +99,7 @@ interface PickOrderData { | |||
| lots: any[]; | |||
| } | |||
| const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| @@ -437,4 +437,4 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| ); | |||
| }; | |||
| export default GoodPickExecutionRecord; | |||
| export default FInishedJobOrderRecord; | |||
| @@ -81,7 +81,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const [errors, setErrors] = useState<FormErrors>({}); | |||
| const [loading, setLoading] = useState(false); | |||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | |||
| const [verifiedQty, setVerifiedQty] = useState<number>(0); | |||
| // 计算剩余可用数量 | |||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | |||
| const remainingQty = lot.inQty - lot.outQty; | |||
| @@ -123,17 +123,15 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| } | |||
| }; | |||
| // 计算剩余可用数量 | |||
| const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | |||
| const requiredQty = calculateRequiredQty(selectedLot); | |||
| // ✅ Initialize verified quantity to the received quantity (actualPickQty) | |||
| const initialVerifiedQty = selectedLot.actualPickQty || 0; | |||
| setVerifiedQty(initialVerifiedQty); | |||
| console.log("=== PickExecutionForm Debug ==="); | |||
| console.log("selectedLot:", selectedLot); | |||
| console.log("inQty:", selectedLot.inQty); | |||
| console.log("outQty:", selectedLot.outQty); | |||
| console.log("holdQty:", selectedLot.holdQty); | |||
| console.log("availableQty:", selectedLot.availableQty); | |||
| console.log("calculated remainingAvailableQty:", remainingAvailableQty); | |||
| console.log("initialVerifiedQty:", initialVerifiedQty); | |||
| console.log("=== End Debug ==="); | |||
| setFormData({ | |||
| pickOrderId: pickOrderId, | |||
| pickOrderCode: selectedPickOrderLine.pickOrderCode, | |||
| @@ -147,18 +145,24 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| lotNo: selectedLot.lotNo, | |||
| storeLocation: selectedLot.location, | |||
| requiredQty: selectedLot.requiredQty, | |||
| actualPickQty: selectedLot.actualPickQty || 0, | |||
| actualPickQty: initialVerifiedQty, // ✅ Use the initial value | |||
| missQty: 0, | |||
| badItemQty: 0, // 初始化为 0,用户需要手动输入 | |||
| badItemQty: 0, | |||
| issueRemark: '', | |||
| pickerName: '', | |||
| handledBy: undefined, | |||
| }); | |||
| } | |||
| }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); | |||
| }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]); | |||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | |||
| setFormData(prev => ({ ...prev, [field]: value })); | |||
| // ✅ Update verified quantity state when actualPickQty changes | |||
| if (field === 'actualPickQty') { | |||
| setVerifiedQty(value); | |||
| } | |||
| // 清除错误 | |||
| if (errors[field as keyof FormErrors]) { | |||
| setErrors(prev => ({ ...prev, [field]: undefined })); | |||
| @@ -169,21 +173,21 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { | |||
| if (verifiedQty === undefined || verifiedQty < 0) { | |||
| newErrors.actualPickQty = t('Qty is required'); | |||
| } | |||
| // ✅ FIXED: Check if actual pick qty exceeds remaining available qty | |||
| if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { | |||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); | |||
| // ✅ Check if verified qty exceeds received qty | |||
| if (verifiedQty > (selectedLot?.actualPickQty || 0)) { | |||
| newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity'); | |||
| } | |||
| // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) | |||
| if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { | |||
| // ✅ Check if verified qty exceeds required qty | |||
| if (verifiedQty > (selectedLot?.requiredQty || 0)) { | |||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); | |||
| } | |||
| // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) | |||
| // ✅ Require either missQty > 0 OR badItemQty > 0 | |||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||
| @@ -203,7 +207,13 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| setLoading(true); | |||
| try { | |||
| await onSubmit(formData as PickExecutionIssueData); | |||
| // ✅ Use the verified quantity in the submission | |||
| const submissionData = { | |||
| ...formData, | |||
| actualPickQty: verifiedQty | |||
| } as PickExecutionIssueData; | |||
| await onSubmit(submissionData); | |||
| onClose(); | |||
| } catch (error) { | |||
| console.error('Error submitting pick execution issue:', error); | |||
| @@ -215,6 +225,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const handleClose = () => { | |||
| setFormData({}); | |||
| setErrors({}); | |||
| setVerifiedQty(0); | |||
| onClose(); | |||
| }; | |||
| @@ -257,8 +268,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Remaining Available Qty')} | |||
| value={remainingAvailableQty} | |||
| label={t('Received Qty')} | |||
| value={formData.actualPickQty || 0} | |||
| disabled | |||
| variant="outlined" | |||
| // helperText={t('Available in warehouse')} | |||
| @@ -268,12 +279,16 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Actual Pick Qty')} | |||
| label={t('Verified Qty')} | |||
| type="number" | |||
| value={formData.actualPickQty || 0} | |||
| onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} | |||
| value={verifiedQty} // ✅ Use the separate state | |||
| onChange={(e) => { | |||
| const newValue = parseFloat(e.target.value) || 0; | |||
| setVerifiedQty(newValue); | |||
| handleInputChange('actualPickQty', newValue); | |||
| }} | |||
| error={!!errors.actualPickQty} | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -17,13 +17,21 @@ import { | |||
| } from "@/app/utils/formatUtil"; | |||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material"; | |||
| import Jodetail from "./Jodetail" | |||
| import PickExecution from "./GoodPickExecution"; | |||
| import PickExecution from "./JobPickExecution"; | |||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import PickExecutionDetail from "./GoodPickExecutiondetail"; | |||
| import GoodPickExecutionRecord from "./GoodPickExecutionRecord"; | |||
| import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan"; | |||
| import FInishedJobOrderRecord from "./FInishedJobOrderRecord"; | |||
| import JobPickExecution from "./JobPickExecution"; | |||
| import { | |||
| fetchUnassignedJobOrderPickOrders, | |||
| assignJobOrderPickOrder, | |||
| fetchJobOrderLotsHierarchical, | |||
| fetchCompletedJobOrderPickOrders, | |||
| fetchCompletedJobOrderPickOrderRecords | |||
| } from "@/app/api/jo/actions"; | |||
| interface Props { | |||
| pickOrders: PickOrderResult[]; | |||
| } | |||
| @@ -48,6 +56,8 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | |||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | |||
| ); | |||
| @@ -125,7 +135,47 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| } | |||
| }; | |||
| // ✅ Manual assignment handler - uses the action function | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| try { | |||
| const orders = await fetchUnassignedJobOrderPickOrders(); | |||
| setUnassignedOrders(orders); | |||
| } catch (error) { | |||
| console.error("Error loading unassigned orders:", error); | |||
| } finally { | |||
| setIsLoadingUnassigned(false); | |||
| } | |||
| }, []); | |||
| // 分配订单给当前用户 | |||
| const handleAssignOrder = useCallback(async (pickOrderId: number) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| } | |||
| try { | |||
| const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||
| if (result.message === "Successfully assigned") { | |||
| console.log("✅ Successfully assigned pick order"); | |||
| // 刷新数据 | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| // 重新加载未分配订单列表 | |||
| loadUnassignedOrders(); | |||
| } else { | |||
| console.warn("⚠️ Assignment failed:", result.message); | |||
| alert(`Assignment failed: ${result.message}`); | |||
| } | |||
| } catch (error) { | |||
| console.error("❌ Error assigning order:", error); | |||
| alert("Error occurred during assignment"); | |||
| } | |||
| }, [currentUserId, loadUnassignedOrders]); | |||
| // 在组件加载时获取未分配订单 | |||
| useEffect(() => { | |||
| loadUnassignedOrders(); | |||
| }, [loadUnassignedOrders]); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| @@ -333,80 +383,33 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| <Stack rowGap={2}> | |||
| <Grid container alignItems="center"> | |||
| <Grid item xs={8}> | |||
| <Box mb={2}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Finished Good Order")} | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| {/* Last 2 buttons aligned right */} | |||
| <Grid item xs={6} > | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => handleAssignByStore("2/F")} | |||
| disabled={isAssigning} | |||
| > | |||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => handleAssignByStore("4/F")} | |||
| disabled={isAssigning} | |||
| > | |||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")} | |||
| </Button> | |||
| </Stack> | |||
| {/* Unassigned Job Orders */} | |||
| {unassignedOrders.length > 0 && ( | |||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | |||
| <Typography variant="h6" gutterBottom> | |||
| {t("Unassigned Job Orders")} ({unassignedOrders.length}) | |||
| </Typography> | |||
| <Stack direction="row" spacing={1} flexWrap="wrap"> | |||
| {unassignedOrders.map((order) => ( | |||
| <Button | |||
| key={order.pickOrderId} | |||
| variant="outlined" | |||
| size="small" | |||
| onClick={() => handleAssignOrder(order.pickOrderId)} | |||
| disabled={isLoadingUnassigned} | |||
| > | |||
| {order.pickOrderCode} - {order.jobOrderName} | |||
| </Button> | |||
| ))} | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| </Grid> | |||
| {/* ✅ Updated print buttons with completion status */} | |||
| <Grid item xs={6} display="flex" justifyContent="flex-end"> | |||
| <Stack direction="row" spacing={1}> | |||
| {/* | |||
| <Button | |||
| variant={hideCompletedUntilNext ? "contained" : "outlined"} | |||
| color={hideCompletedUntilNext ? "warning" : "inherit"} | |||
| onClick={() => { | |||
| const next = !hideCompletedUntilNext; | |||
| setHideCompletedUntilNext(next); | |||
| if (next) localStorage.setItem('hideCompletedUntilNext', 'true'); | |||
| else localStorage.removeItem('hideCompletedUntilNext'); | |||
| window.dispatchEvent(new Event('pickOrderAssigned')); // ask detail to re-fetch | |||
| }} | |||
| > | |||
| {hideCompletedUntilNext ? t("Hide Completed: ON") : t("Hide Completed: OFF")} | |||
| </Button> | |||
| */} | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Draft")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Pick Order and DN Label")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Pick Order")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print DN Label")} | |||
| </Button> | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -419,8 +422,8 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Finished Good Detail")} iconPosition="end" /> | |||
| <Tab label={t("Finished Good Record")} iconPosition="end" /> | |||
| <Tab label={t("Job order match")} iconPosition="end" /> | |||
| <Tab label={t("Finished Job Order Record")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Box> | |||
| @@ -429,9 +432,9 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| <Box sx={{ | |||
| p: 2 | |||
| }}> | |||
| {tabIndex === 0 && <PickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />} | |||
| {tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />} | |||
| {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 1 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} | |||
| {tabIndex === 2 && <FInishedJobOrderRecord filterArgs={filterArgs} />} | |||
| </Box> | |||
| </Box> | |||
| ); | |||