| @@ -532,6 +532,8 @@ export interface AllJoPickOrderResponse { | |||||
| jobOrderStatus: string; | jobOrderStatus: string; | ||||
| finishedPickOLineCount: number; | finishedPickOLineCount: number; | ||||
| floorPickCounts: FloorPickCount[]; | floorPickCounts: FloorPickCount[]; | ||||
| noLotPickCount?: FloorPickCount | null; | |||||
| suggestedFailCount?: number; | |||||
| } | } | ||||
| export interface UpdateJoPickOrderHandledByRequest { | export interface UpdateJoPickOrderHandledByRequest { | ||||
| pickOrderId: number; | 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[]>( | return serverFetchJson<AllJoPickOrderResponse[]>( | ||||
| `${BASE_API_URL}/jo/AllJoPickOrder${query}`, | `${BASE_API_URL}/jo/AllJoPickOrder${query}`, | ||||
| { method: "GET" } | { method: "GET" } | ||||
| @@ -1,5 +1,5 @@ | |||||
| "use client" | "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 React, { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Criterion } from "../SearchBox"; | import { Criterion } from "../SearchBox"; | ||||
| @@ -12,7 +12,7 @@ import { useRouter } from "next/navigation"; | |||||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { StockInLineInput } from "@/app/api/stockIn"; | import { StockInLineInput } from "@/app/api/stockIn"; | ||||
| import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; | 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 { BomCombo } from "@/app/api/bom"; | ||||
| import JoCreateFormModal from "./JoCreateFormModal"; | import JoCreateFormModal from "./JoCreateFormModal"; | ||||
| import AddIcon from '@mui/icons-material/Add'; | 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 [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | ||||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | 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 状态 | // 合并后的统一编辑 Dialog 状态 | ||||
| const [openEditDialog, setOpenEditDialog] = useState(false); | const [openEditDialog, setOpenEditDialog] = useState(false); | ||||
| @@ -229,9 +232,108 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| setEditProductionPriority(50); | setEditProductionPriority(50); | ||||
| setEditProductProcessId(null); | 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>[]>( | 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", | name: "planStart", | ||||
| @@ -340,49 +442,43 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| name: "id", | name: "id", | ||||
| label: t("Actions"), | label: t("Actions"), | ||||
| renderCell: (row) => { | renderCell: (row) => { | ||||
| const isPlanning = isPlanningJo(row); | |||||
| const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing; | |||||
| return ( | 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) => { | const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { | ||||
| try { | try { | ||||
| const response = await updateJoReqQty({ | const response = await updateJoReqQty({ | ||||
| @@ -560,6 +656,27 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| spacing={2} | spacing={2} | ||||
| sx={{ mt: 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 | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<AddIcon />} | startIcon={<AddIcon />} | ||||
| @@ -580,6 +697,8 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| pagingController={pagingController} | pagingController={pagingController} | ||||
| totalCount={totalCount} | totalCount={totalCount} | ||||
| isAutoPaging={false} | isAutoPaging={false} | ||||
| checkboxIds={checkboxIds} | |||||
| setCheckboxIds={setCheckboxIds} | |||||
| /> | /> | ||||
| <JoCreateFormModal | <JoCreateFormModal | ||||
| open={isCreateJoModalOpen} | open={isCreateJoModalOpen} | ||||
| @@ -10,7 +10,6 @@ import { | |||||
| Typography, | Typography, | ||||
| Chip, | Chip, | ||||
| CircularProgress, | CircularProgress, | ||||
| TablePagination, | |||||
| Grid, | Grid, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | ||||
| @@ -20,36 +19,35 @@ import JobPickExecution from "./newJobPickExecution"; | |||||
| interface Props { | interface Props { | ||||
| onSwitchToRecordTab?: () => void; | onSwitchToRecordTab?: () => void; | ||||
| } | } | ||||
| const PER_PAGE = 6; | |||||
| const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | ||||
| const { t } = useTranslation(["common", "jo"]); | const { t } = useTranslation(["common", "jo"]); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | ||||
| const [page, setPage] = useState(0); | |||||
| const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | ||||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | ||||
| type PickOrderFilter = "all" | "drink" | "other"; | type PickOrderFilter = "all" | "drink" | "other"; | ||||
| const [filter, setFilter] = useState<PickOrderFilter>("all"); | const [filter, setFilter] = useState<PickOrderFilter>("all"); | ||||
| type FloorFilter = "ALL" | "2F" | "3F" | "4F"; | |||||
| const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL"); | |||||
| const fetchPickOrders = useCallback(async () => { | const fetchPickOrders = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const isDrinkParam = | const isDrinkParam = | ||||
| filter === "all" ? undefined : filter === "drink" ? true : false; | 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 : []); | setPickOrders(Array.isArray(data) ? data : []); | ||||
| setPage(0); | |||||
| } catch (e) { | } catch (e) { | ||||
| console.error(e); | console.error(e); | ||||
| setPickOrders([]); | setPickOrders([]); | ||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, [filter]); | |||||
| }, [filter, floorFilter]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchPickOrders( ); | |||||
| }, [fetchPickOrders, filter]); | |||||
| fetchPickOrders(); | |||||
| }, [fetchPickOrders, filter, floorFilter]); | |||||
| const handleBackToList = useCallback(() => { | const handleBackToList = useCallback(() => { | ||||
| setSelectedPickOrderId(undefined); | setSelectedPickOrderId(undefined); | ||||
| setSelectedJobOrderId(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 ( | return ( | ||||
| <Box> | <Box> | ||||
| {loading ? ( | {loading ? ( | ||||
| @@ -115,12 +110,43 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| {t("Other")} | {t("Other")} | ||||
| </Button> | </Button> | ||||
| </Box> | </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 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| {t("Total pick orders")}: {pickOrders.length} | {t("Total pick orders")}: {pickOrders.length} | ||||
| </Typography> | </Typography> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| {paged.map((pickOrder) => { | |||||
| {pickOrders.map((pickOrder) => { | |||||
| const status = String(pickOrder.jobOrderStatus || ""); | const status = String(pickOrder.jobOrderStatus || ""); | ||||
| const statusLower = status.toLowerCase(); | const statusLower = status.toLowerCase(); | ||||
| const statusColor = | const statusColor = | ||||
| @@ -175,6 +201,22 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| {floor}: {finishedCount}/{totalCount} | {floor}: {finishedCount}/{totalCount} | ||||
| </Typography> | </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 && ( | {statusLower !== "pending" && finishedCount > 0 && ( | ||||
| <Box sx={{ mt: 1 }}> | <Box sx={{ mt: 1 }}> | ||||
| <Typography variant="body2" fontWeight={600}> | <Typography variant="body2" fontWeight={600}> | ||||
| @@ -202,16 +244,6 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| ); | ); | ||||
| })} | })} | ||||
| </Grid> | </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> | ||||
| )} | )} | ||||
| </Box> | </Box> | ||||
| @@ -2003,6 +2003,32 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| // ✅ 使用 batchSubmitList API | // ✅ 使用 batchSubmitList API | ||||
| const result = await batchSubmitList(request); | const result = await batchSubmitList(request); | ||||
| console.log(`📥 Batch submit result:`, result); | 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; | const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | ||||
| @@ -2118,6 +2144,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const issueData = { | const issueData = { | ||||
| ...data, | ...data, | ||||
| type: "Jo", // Delivery Order Record 类型 | type: "Jo", // Delivery Order Record 类型 | ||||
| pickerName: session?.user?.name || undefined, | |||||
| handledBy: currentUserId || undefined, | |||||
| }; | }; | ||||
| const result = await recordPickExecutionIssue(issueData); | const result = await recordPickExecutionIssue(issueData); | ||||
| @@ -126,6 +126,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||||
| } | } | ||||
| return "IQC"; // Default | return "IQC"; // Default | ||||
| }, [itemDetail]); | }, [itemDetail]); | ||||
| const isJobOrder = useMemo(() => { | |||||
| return isExist(itemDetail?.jobOrderId); | |||||
| }, [itemDetail?.jobOrderId]); | |||||
| const detailMode = useMemo(() => { | const detailMode = useMemo(() => { | ||||
| const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId); | 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") { | if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") { | ||||
| setError("acceptQty", { message: t("value must be a number") }); | setError("acceptQty", { message: t("value must be a number") }); | ||||
| } else | } else | ||||
| if (accQty > itemDetail.acceptedQty) { | |||||
| if (!isJobOrder && accQty > itemDetail.acceptedQty) { | |||||
| setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | ||||
| itemDetail.acceptedQty}` }); | itemDetail.acceptedQty}` }); | ||||
| } else | } else | ||||
| @@ -156,10 +160,10 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false, compactLay | |||||
| console.log("%c Validated accQty:", "color:yellow", accQty); | console.log("%c Validated accQty:", "color:yellow", accQty); | ||||
| } | } | ||||
| },[setError, qcDecision, accQty, itemDetail]) | |||||
| },[setError, qcDecision, accQty, itemDetail, isJobOrder]) | |||||
| useEffect(() => { // W I P // ----- | useEffect(() => { // W I P // ----- | ||||
| if (qcDecision == 1) { | 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; | itemDetail.acceptedQty}`)) return; | ||||
| if (validateFieldFail("acceptQty", accQty <= 0, t("minimal value is 1"))) 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"); | // console.log("Validated without errors"); | ||||
| }, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult]); | |||||
| }, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult, isJobOrder]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| clearErrors(); | clearErrors(); | ||||
| @@ -612,7 +616,7 @@ useEffect(() => { | |||||
| } | } | ||||
| e.target.value = r; | 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) => { | // onChange={(e) => { | ||||
| // const inputValue = e.target.value; | // const inputValue = e.target.value; | ||||
| // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | ||||
| @@ -632,7 +636,7 @@ useEffect(() => { | |||||
| sx={{ width: '150px' }} | sx={{ width: '150px' }} | ||||
| value={ | value={ | ||||
| (!Boolean(errors.acceptQty) && qcDecision !== undefined && qcDecision != 3) ? | (!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)} | error={Boolean(errors.acceptQty)} | ||||
| @@ -404,6 +404,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| return; | return; | ||||
| } | } | ||||
| const isJobOrderSource = Boolean(stockInLineInfo?.jobOrderId) || printSource === "productionProcess"; | |||||
| const qcData = { | const qcData = { | ||||
| dnNo : data.dnNo? data.dnNo : "DN00000", | dnNo : data.dnNo? data.dnNo : "DN00000", | ||||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | ||||
| @@ -413,6 +414,9 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| qcAccept: qcAccept? qcAccept : false, | qcAccept: qcAccept? qcAccept : false, | ||||
| acceptQty: acceptQty? acceptQty : 0, | 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: itemDetail.status != "escalated" ? qcResults.map(item => ({ | ||||
| qcResult: qcResults.map(item => ({ | qcResult: qcResults.map(item => ({ | ||||
| // id: item.id, | // id: item.id, | ||||
| @@ -481,8 +485,8 @@ const QcStockInModal: React.FC<Props> = ({ | |||||
| itemId: stockInLineInfo?.itemId, // Include Item ID | itemId: stockInLineInfo?.itemId, // Include Item ID | ||||
| purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists | purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists | ||||
| purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL 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, | warehouseId: defaultWarehouseId, | ||||
| status: "received", // Use string like PutAwayModal | status: "received", // Use string like PutAwayModal | ||||
| productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, | 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, | receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, | ||||
| inventoryLotLines: [{ | inventoryLotLines: [{ | ||||
| warehouseId: defaultWarehouseId, | warehouseId: defaultWarehouseId, | ||||
| qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal | |||||
| qty: acceptQty, // Putaway qty | |||||
| }], | }], | ||||
| } as StockInLineEntry & ModalFormInput; | } as StockInLineEntry & ModalFormInput; | ||||
| @@ -10,6 +10,8 @@ | |||||
| "Issue BOM List": "問題 BOM 列表", | "Issue BOM List": "問題 BOM 列表", | ||||
| "File Name": "檔案名稱", | "File Name": "檔案名稱", | ||||
| "Please Select BOM": "請選擇 BOM", | "Please Select BOM": "請選擇 BOM", | ||||
| "No Lot": "沒有批號", | |||||
| "Select All": "全選", | |||||
| "Loading BOM Detail...": "正在載入 BOM 明細…", | "Loading BOM Detail...": "正在載入 BOM 明細…", | ||||
| "Output Quantity": "使用數量", | "Output Quantity": "使用數量", | ||||
| "Process & Equipment": "製程與設備", | "Process & Equipment": "製程與設備", | ||||