| @@ -532,6 +532,8 @@ export interface AllJoPickOrderResponse { | |||
| jobOrderStatus: string; | |||
| finishedPickOLineCount: number; | |||
| floorPickCounts: FloorPickCount[]; | |||
| noLotPickCount?: FloorPickCount | null; | |||
| suggestedFailCount?: number; | |||
| } | |||
| export interface UpdateJoPickOrderHandledByRequest { | |||
| pickOrderId: number; | |||
| @@ -689,10 +691,11 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder | |||
| }, | |||
| ); | |||
| }); | |||
| export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null) => { | |||
| const query = isDrink !== undefined && isDrink !== null | |||
| ? `?isDrink=${isDrink}` | |||
| : ""; | |||
| export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null, floor?: string | null) => { | |||
| const params = new URLSearchParams(); | |||
| if (isDrink !== undefined && isDrink !== null) params.set("isDrink", String(isDrink)); | |||
| if (floor) params.set("floor", floor); | |||
| const query = params.toString() ? `?${params.toString()}` : ""; | |||
| return serverFetchJson<AllJoPickOrderResponse[]>( | |||
| `${BASE_API_URL}/jo/AllJoPickOrder${query}`, | |||
| { method: "GET" } | |||
| @@ -1,5 +1,5 @@ | |||
| "use client" | |||
| import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; | |||
| import { SearchJoResultRequest, fetchJos, releaseJo, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Criterion } from "../SearchBox"; | |||
| @@ -12,7 +12,7 @@ import { useRouter } from "next/navigation"; | |||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { StockInLineInput } from "@/app/api/stockIn"; | |||
| import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; | |||
| import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box } from "@mui/material"; | |||
| import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box, CircularProgress } from "@mui/material"; | |||
| import { BomCombo } from "@/app/api/bom"; | |||
| import JoCreateFormModal from "./JoCreateFormModal"; | |||
| import AddIcon from '@mui/icons-material/Add'; | |||
| @@ -55,6 +55,9 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | |||
| const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]); | |||
| const [releasingJoIds, setReleasingJoIds] = useState<Set<number>>(new Set()); | |||
| const [isBatchReleasing, setIsBatchReleasing] = useState(false); | |||
| // 合并后的统一编辑 Dialog 状态 | |||
| const [openEditDialog, setOpenEditDialog] = useState(false); | |||
| @@ -229,9 +232,108 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| setEditProductionPriority(50); | |||
| setEditProductProcessId(null); | |||
| }, []); | |||
| const newPageFetch = useCallback( | |||
| async ( | |||
| pagingController: { pageNum: number; pageSize: number }, | |||
| filterArgs: SearchJoResultRequest, | |||
| ) => { | |||
| const params: SearchJoResultRequest = { | |||
| ...filterArgs, | |||
| pageNum: pagingController.pageNum - 1, | |||
| pageSize: pagingController.pageSize, | |||
| }; | |||
| const response = await fetchJos(params); | |||
| console.log("newPageFetch params:", params) | |||
| console.log("newPageFetch response:", response) | |||
| if (response && response.records) { | |||
| console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| setTotalCount(response.total); | |||
| setFilteredJos(response.records); | |||
| console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| } else { | |||
| console.warn("newPageFetch - no response or no records"); | |||
| setFilteredJos([]); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| const isPlanningJo = useCallback((jo: JobOrder) => { | |||
| return String(jo.status ?? "").toLowerCase() === "planning"; | |||
| }, []); | |||
| const handleReleaseJo = useCallback(async (joId: number) => { | |||
| if (!joId) return; | |||
| setReleasingJoIds((prev) => { | |||
| const next = new Set(prev); | |||
| next.add(joId); | |||
| return next; | |||
| }); | |||
| try { | |||
| const response = await releaseJo({ id: joId }); | |||
| if (response) { | |||
| msg(t("update success")); | |||
| setCheckboxIds((prev) => prev.filter((id) => Number(id) !== joId)); | |||
| await newPageFetch(pagingController, inputs); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error releasing JO:", error); | |||
| msg(t("update failed")); | |||
| } finally { | |||
| setReleasingJoIds((prev) => { | |||
| const next = new Set(prev); | |||
| next.delete(joId); | |||
| return next; | |||
| }); | |||
| } | |||
| }, [inputs, pagingController, t, newPageFetch]); | |||
| const selectedPlanningJoIds = useMemo(() => { | |||
| const selectedIds = new Set(checkboxIds.map((id) => Number(id))); | |||
| return filteredJos | |||
| .filter((jo) => selectedIds.has(jo.id)) | |||
| .filter((jo) => isPlanningJo(jo)) | |||
| .map((jo) => jo.id); | |||
| }, [checkboxIds, filteredJos, isPlanningJo]); | |||
| const handleBatchRelease = useCallback(async () => { | |||
| if (selectedPlanningJoIds.length === 0) return; | |||
| setIsBatchReleasing(true); | |||
| try { | |||
| const results = await Promise.allSettled( | |||
| selectedPlanningJoIds.map((id) => releaseJo({ id })), | |||
| ); | |||
| const successCount = results.filter((r) => r.status === "fulfilled").length; | |||
| const failedCount = results.length - successCount; | |||
| if (successCount > 0 && failedCount === 0) { | |||
| msg(t("update success")); | |||
| } else if (successCount > 0) { | |||
| msg(`${t("update success")} (${successCount}), ${t("update failed")} (${failedCount})`); | |||
| } else { | |||
| msg(t("update failed")); | |||
| } | |||
| setCheckboxIds((prev) => prev.filter((id) => !selectedPlanningJoIds.includes(Number(id)))); | |||
| await newPageFetch(pagingController, inputs); | |||
| } catch (error) { | |||
| console.error("Error batch releasing JOs:", error); | |||
| msg(t("update failed")); | |||
| } finally { | |||
| setIsBatchReleasing(false); | |||
| } | |||
| }, [inputs, pagingController, selectedPlanningJoIds, t, newPageFetch]); | |||
| const columns = useMemo<Column<JobOrder>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (row) => { | |||
| const id = row.id; | |||
| return !isPlanningJo(row) || releasingJoIds.has(id) || isBatchReleasing; | |||
| } | |||
| }, | |||
| { | |||
| name: "planStart", | |||
| @@ -340,49 +442,43 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| name: "id", | |||
| label: t("Actions"), | |||
| renderCell: (row) => { | |||
| const isPlanning = isPlanningJo(row); | |||
| const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing; | |||
| return ( | |||
| <Button | |||
| id="emailSupplier" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ width: "150px" }} | |||
| onClick={() => onDetailClick(row)} | |||
| > | |||
| {t("View")} | |||
| </Button> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Button | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ minWidth: 120 }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onDetailClick(row); | |||
| }} | |||
| > | |||
| {t("View")} | |||
| </Button> | |||
| <Button | |||
| type="button" | |||
| variant="contained" | |||
| color="success" | |||
| disabled={!isPlanning || isReleasing} | |||
| sx={{ minWidth: 120 }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| handleReleaseJo(row.id); | |||
| }} | |||
| startIcon={isReleasing && isPlanning ? <CircularProgress size={16} color="inherit" /> : undefined} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| </Stack> | |||
| ) | |||
| } | |||
| }, | |||
| ], [t, inventoryData, detailedJos, handleOpenEditDialog] | |||
| ], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing] | |||
| ) | |||
| const newPageFetch = useCallback( | |||
| async ( | |||
| pagingController: { pageNum: number; pageSize: number }, | |||
| filterArgs: SearchJoResultRequest, | |||
| ) => { | |||
| const params: SearchJoResultRequest = { | |||
| ...filterArgs, | |||
| pageNum: pagingController.pageNum - 1, | |||
| pageSize: pagingController.pageSize, | |||
| }; | |||
| const response = await fetchJos(params); | |||
| console.log("newPageFetch params:", params) | |||
| console.log("newPageFetch response:", response) | |||
| if (response && response.records) { | |||
| console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); | |||
| setTotalCount(response.total); | |||
| setFilteredJos(response.records); | |||
| console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); | |||
| } else { | |||
| console.warn("newPageFetch - no response or no records"); | |||
| setFilteredJos([]); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { | |||
| try { | |||
| const response = await updateJoReqQty({ | |||
| @@ -560,6 +656,27 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| spacing={2} | |||
| sx={{ mt: 2 }} | |||
| > | |||
| <Stack direction="row" alignItems="center" spacing={1} sx={{ mr: "auto" }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Selected")}: {selectedPlanningJoIds.length} | |||
| </Typography> | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| disabled={selectedPlanningJoIds.length === 0 || isBatchReleasing} | |||
| onClick={handleBatchRelease} | |||
| startIcon={isBatchReleasing ? <CircularProgress size={16} color="inherit" /> : undefined} | |||
| > | |||
| {t("Release")} ({selectedPlanningJoIds.length}) | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| disabled={checkboxIds.length === 0 || isBatchReleasing} | |||
| onClick={() => setCheckboxIds([])} | |||
| > | |||
| {t("Reset")} | |||
| </Button> | |||
| </Stack> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<AddIcon />} | |||
| @@ -580,6 +697,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| pagingController={pagingController} | |||
| totalCount={totalCount} | |||
| isAutoPaging={false} | |||
| checkboxIds={checkboxIds} | |||
| setCheckboxIds={setCheckboxIds} | |||
| /> | |||
| <JoCreateFormModal | |||
| open={isCreateJoModalOpen} | |||
| @@ -10,7 +10,6 @@ import { | |||
| Typography, | |||
| Chip, | |||
| CircularProgress, | |||
| TablePagination, | |||
| Grid, | |||
| } from "@mui/material"; | |||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||
| @@ -20,36 +19,35 @@ import JobPickExecution from "./newJobPickExecution"; | |||
| interface Props { | |||
| onSwitchToRecordTab?: () => void; | |||
| } | |||
| const PER_PAGE = 6; | |||
| const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| const { t } = useTranslation(["common", "jo"]); | |||
| const [loading, setLoading] = useState(false); | |||
| const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | |||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | |||
| type PickOrderFilter = "all" | "drink" | "other"; | |||
| const [filter, setFilter] = useState<PickOrderFilter>("all"); | |||
| type FloorFilter = "ALL" | "2F" | "3F" | "4F"; | |||
| const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL"); | |||
| const fetchPickOrders = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const isDrinkParam = | |||
| filter === "all" ? undefined : filter === "drink" ? true : false; | |||
| const data = await fetchAllJoPickOrders(isDrinkParam); | |||
| const floorParam = floorFilter === "ALL" ? undefined : floorFilter; | |||
| const data = await fetchAllJoPickOrders(isDrinkParam, floorParam); | |||
| setPickOrders(Array.isArray(data) ? data : []); | |||
| setPage(0); | |||
| } catch (e) { | |||
| console.error(e); | |||
| setPickOrders([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [filter]); | |||
| }, [filter, floorFilter]); | |||
| useEffect(() => { | |||
| fetchPickOrders( ); | |||
| }, [fetchPickOrders, filter]); | |||
| fetchPickOrders(); | |||
| }, [fetchPickOrders, filter, floorFilter]); | |||
| const handleBackToList = useCallback(() => { | |||
| setSelectedPickOrderId(undefined); | |||
| setSelectedJobOrderId(undefined); | |||
| @@ -80,9 +78,6 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| ); | |||
| } | |||
| const startIdx = page * PER_PAGE; | |||
| const paged = pickOrders.slice(startIdx, startIdx + PER_PAGE); | |||
| return ( | |||
| <Box> | |||
| {loading ? ( | |||
| @@ -115,12 +110,43 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| {t("Other")} | |||
| </Button> | |||
| </Box> | |||
| <Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap', mb: 2 }}> | |||
| <Button | |||
| variant={floorFilter === "ALL" ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("ALL")} | |||
| > | |||
| {t("Select All")} | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "2F" ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("2F")} | |||
| > | |||
| 2F | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "3F" ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("3F")} | |||
| > | |||
| 3F | |||
| </Button> | |||
| <Button | |||
| variant={floorFilter === "4F" ? "contained" : "outlined"} | |||
| size="small" | |||
| onClick={() => setFloorFilter("4F")} | |||
| > | |||
| 4F | |||
| </Button> | |||
| </Box> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total pick orders")}: {pickOrders.length} | |||
| </Typography> | |||
| <Grid container spacing={2}> | |||
| {paged.map((pickOrder) => { | |||
| {pickOrders.map((pickOrder) => { | |||
| const status = String(pickOrder.jobOrderStatus || ""); | |||
| const statusLower = status.toLowerCase(); | |||
| const statusColor = | |||
| @@ -175,6 +201,22 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| {floor}: {finishedCount}/{totalCount} | |||
| </Typography> | |||
| ))} | |||
| {!!pickOrder.noLotPickCount && ( | |||
| <Typography | |||
| key="NO_LOT" | |||
| variant="body2" | |||
| color="text.secondary" | |||
| component="span" | |||
| sx={{ mr: 1 }} | |||
| > | |||
| {t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount} | |||
| </Typography> | |||
| )} | |||
| {typeof pickOrder.suggestedFailCount === "number" && pickOrder.suggestedFailCount > 0 && ( | |||
| <Typography variant="body2" color="error" sx={{ mt: 0.5 }}> | |||
| {t("Suggested Fail")}: {pickOrder.suggestedFailCount} | |||
| </Typography> | |||
| )} | |||
| {statusLower !== "pending" && finishedCount > 0 && ( | |||
| <Box sx={{ mt: 1 }}> | |||
| <Typography variant="body2" fontWeight={600}> | |||
| @@ -202,16 +244,6 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| ); | |||
| })} | |||
| </Grid> | |||
| {pickOrders.length > 0 && ( | |||
| <TablePagination | |||
| component="div" | |||
| count={pickOrders.length} | |||
| page={page} | |||
| rowsPerPage={PER_PAGE} | |||
| onPageChange={(e, p) => setPage(p)} | |||
| rowsPerPageOptions={[PER_PAGE]} | |||
| /> | |||
| )} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| @@ -2003,6 +2003,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| // ✅ 使用 batchSubmitList API | |||
| const result = await batchSubmitList(request); | |||
| console.log(`📥 Batch submit result:`, result); | |||
| // ✅ After batch submit, explicitly trigger completion check per consoCode. | |||
| // Otherwise pick_order/job_order may stay RELEASED even when all lines are completed. | |||
| try { | |||
| const consoCodes = Array.from( | |||
| new Set( | |||
| lines | |||
| .map((l) => (l.pickOrderConsoCode || "").trim()) | |||
| .filter((c) => c.length > 0), | |||
| ), | |||
| ); | |||
| if (consoCodes.length > 0) { | |||
| await Promise.all( | |||
| consoCodes.map(async (code) => { | |||
| try { | |||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(code); | |||
| console.log(`✅ Pick order completion check (${code}):`, completionResponse); | |||
| } catch (e) { | |||
| console.error(`❌ Error checking completion for ${code}:`, e); | |||
| } | |||
| }), | |||
| ); | |||
| } | |||
| } catch (e) { | |||
| console.error("❌ Error triggering completion checks after batch submit:", e); | |||
| } | |||
| // 刷新数据 | |||
| const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||
| @@ -2118,6 +2144,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||
| const issueData = { | |||
| ...data, | |||
| type: "Jo", // Delivery Order Record 类型 | |||
| pickerName: session?.user?.name || undefined, | |||
| handledBy: currentUserId || undefined, | |||
| }; | |||
| const result = await recordPickExecutionIssue(issueData); | |||
| @@ -126,6 +126,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||
| } | |||
| return "IQC"; // Default | |||
| }, [itemDetail]); | |||
| const isJobOrder = useMemo(() => { | |||
| return isExist(itemDetail?.jobOrderId); | |||
| }, [itemDetail?.jobOrderId]); | |||
| const detailMode = useMemo(() => { | |||
| const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId); | |||
| @@ -146,7 +150,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||
| if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") { | |||
| setError("acceptQty", { message: t("value must be a number") }); | |||
| } else | |||
| if (accQty > itemDetail.acceptedQty) { | |||
| if (!isJobOrder && accQty > itemDetail.acceptedQty) { | |||
| setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | |||
| itemDetail.acceptedQty}` }); | |||
| } else | |||
| @@ -156,10 +160,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||
| console.log("%c Validated accQty:", "color:yellow", accQty); | |||
| } | |||
| },[setError, qcDecision, accQty, itemDetail]) | |||
| },[setError, qcDecision, accQty, itemDetail, isJobOrder]) | |||
| useEffect(() => { // W I P // ----- | |||
| if (qcDecision == 1) { | |||
| if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ | |||
| if (!isJobOrder && validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ | |||
| itemDetail.acceptedQty}`)) return; | |||
| if (validateFieldFail("acceptQty", accQty <= 0, t("minimal value is 1"))) return; | |||
| @@ -188,7 +192,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||
| } | |||
| // console.log("Validated without errors"); | |||
| }, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult]); | |||
| }, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult, isJobOrder]); | |||
| useEffect(() => { | |||
| clearErrors(); | |||
| @@ -612,7 +616,7 @@ useEffect(() => { | |||
| } | |||
| e.target.value = r; | |||
| }} | |||
| inputProps={{ min: 0.01, max:itemDetail.acceptedQty, step: 0.01 }} | |||
| inputProps={isJobOrder ? { min: 0.01, step: 0.01 } : { min: 0.01, max: itemDetail.acceptedQty, step: 0.01 }} | |||
| // onChange={(e) => { | |||
| // const inputValue = e.target.value; | |||
| // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | |||
| @@ -632,7 +636,7 @@ useEffect(() => { | |||
| sx={{ width: '150px' }} | |||
| value={ | |||
| (!Boolean(errors.acceptQty) && qcDecision !== undefined && qcDecision != 3) ? | |||
| (qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty) | |||
| (qcDecision == 1 ? Math.max(0, itemDetail.acceptedQty - accQty) : itemDetail.acceptedQty) | |||
| : "" | |||
| } | |||
| error={Boolean(errors.acceptQty)} | |||
| @@ -404,6 +404,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| const isJobOrderSource = Boolean(stockInLineInfo?.jobOrderId) || printSource === "productionProcess"; | |||
| const qcData = { | |||
| dnNo : data.dnNo? data.dnNo : "DN00000", | |||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| @@ -413,6 +414,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| qcAccept: qcAccept? qcAccept : false, | |||
| acceptQty: acceptQty? acceptQty : 0, | |||
| // For Job Order QC, allow updating received qty beyond demand/accepted. | |||
| // Backend uses request.acceptedQty in QC flow, so we must send it explicitly. | |||
| acceptedQty: (qcAccept && isJobOrderSource) ? (acceptQty ? acceptQty : 0) : stockInLineInfo?.acceptedQty, | |||
| // qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ | |||
| qcResult: qcResults.map(item => ({ | |||
| // id: item.id, | |||
| @@ -481,8 +485,8 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| itemId: stockInLineInfo?.itemId, // Include Item ID | |||
| purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists | |||
| purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL ID if exists | |||
| acceptedQty:acceptQty, // Include acceptedQty | |||
| acceptQty: stockInLineInfo?.acceptedQty, // Putaway quantity | |||
| acceptedQty: acceptQty, // Keep in sync with QC acceptQty | |||
| acceptQty: acceptQty, // Putaway quantity | |||
| warehouseId: defaultWarehouseId, | |||
| status: "received", // Use string like PutAwayModal | |||
| productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, | |||
| @@ -490,7 +494,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, | |||
| inventoryLotLines: [{ | |||
| warehouseId: defaultWarehouseId, | |||
| qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal | |||
| qty: acceptQty, // Putaway qty | |||
| }], | |||
| } as StockInLineEntry & ModalFormInput; | |||
| @@ -10,6 +10,8 @@ | |||
| "Issue BOM List": "問題 BOM 列表", | |||
| "File Name": "檔案名稱", | |||
| "Please Select BOM": "請選擇 BOM", | |||
| "No Lot": "沒有批號", | |||
| "Select All": "全選", | |||
| "Loading BOM Detail...": "正在載入 BOM 明細…", | |||
| "Output Quantity": "使用數量", | |||
| "Process & Equipment": "製程與設備", | |||