|
- "use client";
-
- import {
- Box,
- Button,
- Stack,
- TextField,
- Typography,
- Alert,
- CircularProgress,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- TablePagination,
- Modal,
- Card,
- CardContent,
- CardActions,
- Chip,
- Accordion,
- AccordionSummary,
- AccordionDetails,
- } from "@mui/material";
- import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
- import { useCallback, useEffect, useState, useRef, useMemo } from "react";
- import { useTranslation } from "react-i18next";
- import { useRouter } from "next/navigation";
- import {
- fetchALLPickOrderLineLotDetails,
- updateStockOutLineStatus,
- createStockOutLine,
- recordPickExecutionIssue,
- fetchFGPickOrders,
- FGPickOrderResponse,
- autoAssignAndReleasePickOrder,
- AutoAssignReleaseResponse,
- checkPickOrderCompletion,
- PickOrderCompletionResponse,
- checkAndCompletePickOrderByConsoCode,
- fetchCompletedDoPickOrders,
- CompletedDoPickOrderResponse,
- CompletedDoPickOrderSearchParams,
- fetchLotDetailsByDoPickOrderRecordId
- } from "@/app/api/pickOrder/actions";
- import { fetchNameList, NameList } from "@/app/api/user/actions";
- import {
- FormProvider,
- useForm,
- } from "react-hook-form";
- import SearchBox, { Criterion } from "../SearchBox";
- import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
- import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
- import QrCodeIcon from '@mui/icons-material/QrCode';
- import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import { fetchStockInLineInfo } from "@/app/api/po/actions";
- import GoodPickExecutionForm from "./GoodPickExecutionForm";
- import FGPickOrderCard from "./FGPickOrderCard";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
- import { printDN, printDNLabels } from "@/app/api/do/actions";
- import Swal from "sweetalert2";
-
-
- interface Props {
- filterArgs: Record<string, any>;
- }
-
-
- // ✅ 新增:Pick Order 数据接口
- interface PickOrderData {
- pickOrderId: number;
- pickOrderCode: string;
- pickOrderConsoCode: string;
- pickOrderStatus: string;
- completedDate: string;
- lots: any[];
- }
-
- const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
- const { t } = useTranslation("pickOrder");
- const router = useRouter();
- const { data: session } = useSession() as { data: SessionWithTokens | null };
-
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
-
- // ✅ 新增:已完成 DO Pick Orders 状态
- const [completedDoPickOrders, setCompletedDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
- const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false);
-
- // ✅ 新增:详情视图状态
- const [selectedDoPickOrder, setSelectedDoPickOrder] = useState<CompletedDoPickOrderResponse | null>(null);
- const [showDetailView, setShowDetailView] = useState(false);
- const [detailLotData, setDetailLotData] = useState<any[]>([]);
-
- // ✅ 新增:搜索状态
- const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
- const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
-
- // ✅ 新增:分页状态
- const [paginationController, setPaginationController] = useState({
- pageNum: 0,
- pageSize: 10,
- });
-
- const formProps = useForm();
- const errors = formProps.formState.errors;
-
- const handleDN = useCallback(async (doPickOrderId: number) => {
- const askNumofCarton = await Swal.fire({
- title: t("Enter the number of cartons: "),
- icon: "info",
- input: "number",
- inputPlaceholder: t("Number of cartons"),
- inputAttributes:{
- min: "1",
- step: "1"
- },
- inputValidator: (value) => {
- if(!value){
- return t("You need to enter a number")
- }
- if(parseInt(value) < 1){
- return t("Number must be at least 1");
- }
- return null
- },
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438",
- showLoaderOnConfirm: true,
- allowOutsideClick: () => !Swal.isLoading()
- });
-
- if (askNumofCarton.isConfirmed) {
- const numOfCartons = askNumofCarton.value;
- try{
- const printRequest = {
- printerId: 1,
- printQty: 1,
- isDraft: false,
- numOfCarton: numOfCartons,
- doPickOrderId: doPickOrderId
- };
-
- console.log("Printing Delivery Note with request: ", printRequest);
- const response = await printDN(printRequest);
- console.log("Print Delivery Note response: ", response);
-
- if(response.success){
- Swal.fire({
- position: "bottom-end",
- icon: "success",
- text: t("Printed Successfully."),
- showConfirmButton: false,
- timer: 1500
- });
- } else {
- console.error("Print failed: ", response.message);
- }
- } catch(error){
- console.error("error: ", error)
- }
- }
- }, [t]);
-
- const handleDNandLabel = useCallback(async (doPickOrderId: number) => {
- const askNumofCarton = await Swal.fire({
- title: t("Enter the number of cartons: "),
- icon: "info",
- input: "number",
- inputPlaceholder: t("Number of cartons"),
- inputAttributes:{
- min: "1",
- step: "1"
- },
- inputValidator: (value) => {
- if(!value){
- return t("You need to enter a number")
- }
- if(parseInt(value) < 1){
- return t("Number must be at least 1");
- }
- return null
- },
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438",
- showLoaderOnConfirm: true,
- allowOutsideClick: () => !Swal.isLoading()
- });
-
- if (askNumofCarton.isConfirmed) {
- const numOfCartons = askNumofCarton.value;
- try{
- const printDNRequest = {
- printerId: 1,
- printQty: 1,
- isDraft: false,
- numOfCarton: numOfCartons,
- doPickOrderId: doPickOrderId,
- };
-
- const printDNLabelsRequest = {
- printerId: 1,
- printQty: 1,
- numOfCarton: numOfCartons,
- doPickOrderId: doPickOrderId
- };
-
- console.log("Printing Labels with request: ", printDNLabelsRequest);
- console.log("Printing DN with request: ", printDNRequest);
-
- const LabelsResponse = await printDNLabels(printDNLabelsRequest);
- const DNResponse = await printDN(printDNRequest);
-
- console.log("Print Labels response: ", LabelsResponse);
- console.log("Print DN response: ", DNResponse);
-
- if(LabelsResponse.success && DNResponse.success){
- Swal.fire({
- position: "bottom-end",
- icon: "success",
- text: t("Printed Successfully."),
- showConfirmButton: false,
- timer: 1500
- });
- } else {
- if(!LabelsResponse.success){
- console.error("Print failed: ", LabelsResponse.message);
- }
- else{
- console.error("Print failed: ", DNResponse.message);
- }
- }
- } catch(error){
- console.error("error: ", error)
- }
- }
- }, [t]);
-
- const handleLabel = useCallback(async (doPickOrderId: number) => {
- const askNumofCarton = await Swal.fire({
- title: t("Enter the number of cartons: "),
- icon: "info",
- input: "number",
- inputPlaceholder: t("Number of cartons"),
- inputAttributes:{
- min: "1",
- step: "1"
- },
- inputValidator: (value) => {
- if(!value){
- return t("You need to enter a number")
- }
- if(parseInt(value) < 1){
- return t("Number must be at least 1");
- }
- return null
- },
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438",
- showLoaderOnConfirm: true,
- allowOutsideClick: () => !Swal.isLoading()
- });
-
- if (askNumofCarton.isConfirmed) {
- const numOfCartons = askNumofCarton.value;
- try{
- const printRequest = {
- printerId: 1,
- printQty: 1,
- numOfCarton: numOfCartons,
- doPickOrderId: doPickOrderId
- };
-
- console.log("Printing Labels with request: ", printRequest);
- const response = await printDNLabels(printRequest);
- console.log("Print Labels response: ", response);
-
- if(response.success){
- Swal.fire({
- position: "bottom-end",
- icon: "success",
- text: t("Printed Successfully."),
- showConfirmButton: false,
- timer: 1500
- });
- } else {
- console.error("Print failed: ", response.message);
- }
- } catch(error){
- console.error("error: ", error)
- }
- }
- }, [t]);
-
- // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders
- const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
- if (!currentUserId) return;
-
- setCompletedDoPickOrdersLoading(true);
- try {
- console.log("🔍 Fetching completed DO pick orders with params:", searchParams);
-
- const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams);
-
- setCompletedDoPickOrders(completedDoPickOrders);
- setFilteredDoPickOrders(completedDoPickOrders);
- console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders);
- } catch (error) {
- console.error("❌ Error fetching completed DO pick orders:", error);
- setCompletedDoPickOrders([]);
- setFilteredDoPickOrders([]);
- } finally {
- setCompletedDoPickOrdersLoading(false);
- }
- }, [currentUserId]);
-
- // ✅ 初始化时获取数据
- useEffect(() => {
- if (currentUserId) {
- fetchCompletedDoPickOrdersData();
- }
- }, [currentUserId, fetchCompletedDoPickOrdersData]);
-
- // ✅ 修改:搜索功能使用新的 API
- const handleSearch = useCallback((query: Record<string, any>) => {
- setSearchQuery({ ...query });
- console.log("Search query:", query);
-
- const searchParams: CompletedDoPickOrderSearchParams = {
- pickOrderCode: query.pickOrderCode || undefined,
- shopName: query.shopName || undefined,
- deliveryNo: query.deliveryNo || undefined,
- //ticketNo: query.ticketNo || undefined,
- };
-
- // 使用新的 API 进行搜索
- fetchCompletedDoPickOrdersData(searchParams);
- }, [fetchCompletedDoPickOrdersData]);
-
- // ✅ 修复:重命名函数避免重复声明
- const handleSearchReset = useCallback(() => {
- setSearchQuery({});
- fetchCompletedDoPickOrdersData(); // 重新获取所有数据
- }, [fetchCompletedDoPickOrdersData]);
-
- // ✅ 分页功能
- const handlePageChange = useCallback((event: unknown, newPage: number) => {
- setPaginationController(prev => ({
- ...prev,
- pageNum: newPage,
- }));
- }, []);
-
- const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
- const newPageSize = parseInt(event.target.value, 10);
- setPaginationController({
- pageNum: 0,
- pageSize: newPageSize,
- });
- }, []);
-
- // ✅ 分页数据
- const paginatedData = useMemo(() => {
- const startIndex = paginationController.pageNum * paginationController.pageSize;
- const endIndex = startIndex + paginationController.pageSize;
- return filteredDoPickOrders.slice(startIndex, endIndex);
- }, [filteredDoPickOrders, paginationController]);
-
- // ✅ 搜索条件
- const searchCriteria: Criterion<any>[] = [
- {
- label: t("Pick Order Code"),
- paramName: "pickOrderCode",
- type: "text",
- },
- {
- label: t("Shop Name"),
- paramName: "shopName",
- type: "text",
- },
- {
- label: t("Delivery No"),
- paramName: "deliveryNo",
- type: "text",
- }
- ];
-
- const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrderResponse) => {
- setSelectedDoPickOrder(doPickOrder);
- setShowDetailView(true);
-
- try {
- // ✅ 使用 doPickOrderRecordId 而不是 pickOrderId
- const hierarchicalData = await fetchLotDetailsByDoPickOrderRecordId(doPickOrder.doPickOrderRecordId);
- console.log("✅ Loaded hierarchical lot data:", hierarchicalData);
-
- // ✅ 转换为平铺格式
- const flatLotData: any[] = [];
-
- if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) {
- hierarchicalData.pickOrders.forEach((po: any) => {
- po.pickOrderLines?.forEach((line: any) => {
- if (line.lots && line.lots.length > 0) {
- line.lots.forEach((lot: any) => {
- flatLotData.push({
- pickOrderCode: po.pickOrderCode,
- itemCode: line.item.code,
- itemName: line.item.name,
- lotNo: lot.lotNo,
- location: lot.location,
- deliveryOrderCode: po.deliveryOrderCode,
- requiredQty: lot.requiredQty,
- actualPickQty: lot.actualPickQty,
- processingStatus: lot.processingStatus,
- stockOutLineStatus: lot.stockOutLineStatus
- });
- });
- }
- });
- });
- }
-
- setDetailLotData(flatLotData);
-
- // ✅ 计算完成状态
- const allCompleted = flatLotData.length > 0 && flatLotData.every(lot =>
- lot.processingStatus === 'completed'
- );
-
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: allCompleted,
- tabIndex: 2
- }
- }));
-
- } catch (error) { // ✅ 添加 catch 块
- console.error("❌ Error loading detail lot data:", error);
- setDetailLotData([]);
-
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: false,
- tabIndex: 2
- }
- }));
- }
- }, []);
-
-
- // ✅ 返回列表视图
- const handleBackToList = useCallback(() => {
- setShowDetailView(false);
- setSelectedDoPickOrder(null);
- setDetailLotData([]);
-
- // ✅ 返回列表时禁用打印按钮
- window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
- detail: {
- allLotsCompleted: false,
- tabIndex: 2
- }
- }));
- }, []);
-
-
- // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格
- // ✅ 如果显示详情视图,渲染层级结构
- if (showDetailView && selectedDoPickOrder) {
- return (
- <FormProvider {...formProps}>
- <Box>
- {/* 返回按钮和标题 */}
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
- <Button variant="outlined" onClick={handleBackToList}>
- {t("Back to List")}
- </Button>
- <Typography variant="h6">
- {t("Pick Order Details")}: {selectedDoPickOrder.ticketNo}
- </Typography>
- </Box>
-
- {/* FG 订单基本信息 */}
- <Paper sx={{ mb: 2, p: 2 }}>
- <Stack spacing={1}>
- <Typography variant="subtitle1">
- <strong>{t("Shop Name")}:</strong> {selectedDoPickOrder.shopName}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Store ID")}:</strong> {selectedDoPickOrder.storeId}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Ticket No.")}:</strong> {selectedDoPickOrder.ticketNo}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Truck Lance Code")}:</strong> {selectedDoPickOrder.truckLanceCode}
- </Typography>
- <Typography variant="subtitle1">
- <strong>{t("Completed Date")}:</strong> {dayjs(selectedDoPickOrder.completedDate).format(OUTPUT_DATE_FORMAT)}
- </Typography>
- </Stack>
- </Paper>
-
- {/* ✅ 添加:多个 Pick Orders 信息(如果有) */}
- {selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && (
- <Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}>
- <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
- {t("This ticket contains")} {selectedDoPickOrder.pickOrderIds.length} {t("pick orders")}:
- </Typography>
- <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
- {selectedDoPickOrder.pickOrderCodes?.split(', ').map((code, idx) => (
- <Chip
- key={idx}
- label={code}
- size="small"
- variant="outlined"
- />
- ))}
- </Box>
- </Paper>
- )}
-
- {/* ✅ 数据检查 */}
- {detailLotData.length === 0 ? (
- <Box sx={{ p: 3, textAlign: 'center' }}>
- <Typography variant="body2" color="text.secondary">
- {t("No lot details found for this order")}
- </Typography>
- </Box>
- ) : (
- /* ✅ 按 Pick Order 分组显示 */
- <Stack spacing={2}>
- {/* ✅ 按 pickOrderCode 分组 */}
- {Object.entries(
- detailLotData.reduce((acc: any, lot: any) => {
- const key = lot.pickOrderCode || 'Unknown';
- if (!acc[key]) {
- acc[key] = {
- lots: [],
- deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // ✅ 保存对应的 deliveryOrderCode
- };
- }
- acc[key].lots.push(lot);
- return acc;
- }, {})
- ).map(([pickOrderCode, data]: [string, any]) => (
- <Accordion key={pickOrderCode} defaultExpanded={true}>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1" fontWeight="bold">
- {t("Pick Order")}: {pickOrderCode} ({data.lots.length} {t("items")})
- {" | "}
- {t("Delivery Order")}: {data.deliveryOrderCode} {/* ✅ 使用保存的 deliveryOrderCode */}
- </Typography>
- </AccordionSummary>
- <AccordionDetails>
- <TableContainer component={Paper}>
- <Table size="small">
- <TableHead>
- <TableRow>
- <TableCell>{t("Index")}</TableCell>
- <TableCell>{t("Item Code")}</TableCell>
- <TableCell>{t("Item Name")}</TableCell>
- <TableCell>{t("Lot No")}</TableCell>
- <TableCell>{t("Location")}</TableCell>
- <TableCell align="right">{t("Required Qty")}</TableCell>
- <TableCell align="right">{t("Actual Pick Qty")}</TableCell>
- <TableCell align="center">{t("Status")}</TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {data.lots.map((lot: any, index: number) => (
- <TableRow key={index}>
- <TableCell>{index + 1}</TableCell>
- <TableCell>{lot.itemCode || 'N/A'}</TableCell>
- <TableCell>{lot.itemName || 'N/A'}</TableCell>
- <TableCell>{lot.lotNo || 'N/A'}</TableCell>
- <TableCell>{lot.location || 'N/A'}</TableCell>
- <TableCell align="right">{lot.requiredQty || 0}</TableCell>
- <TableCell align="right">{lot.actualPickQty || 0}</TableCell>
- <TableCell align="center">
- <Chip
- label={t(lot.processingStatus || 'unknown')}
- color={lot.processingStatus === 'completed' ? 'success' : 'default'}
- size="small"
- />
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </TableContainer>
- </AccordionDetails>
- </Accordion>
- ))}
- </Stack>
- )}
- </Box>
- </FormProvider>
- );
- }
-
- // ✅ 默认列表视图
- return (
- <FormProvider {...formProps}>
- <Box>
- {/* 搜索框 */}
- <Box sx={{ mb: 2 }}>
- <SearchBox
- criteria={searchCriteria}
- onSearch={handleSearch}
- onReset={handleSearchReset}
- />
- </Box>
-
- {/* 加载状态 */}
- {completedDoPickOrdersLoading ? (
- <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <Box>
- {/* 结果统计 */}
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
- {t("Completed DO pick orders: ")} {filteredDoPickOrders.length}
- </Typography>
-
- {/* 列表 */}
- {filteredDoPickOrders.length === 0 ? (
- <Box sx={{ p: 3, textAlign: 'center' }}>
- <Typography variant="body2" color="text.secondary">
- {t("No completed DO pick orders found")}
- </Typography>
- </Box>
- ) : (
- <Stack spacing={2}>
- {paginatedData.map((doPickOrder) => (
- <Card key={doPickOrder.id}>
- <CardContent>
- <Stack direction="row" justifyContent="space-between" alignItems="center">
- <Box>
- <Typography variant="h6">
- {doPickOrder.pickOrderCode}
- </Typography>
-
- <Typography variant="body2" color="text.secondary">
- {doPickOrder.shopName} - {doPickOrder.deliveryNo}
- </Typography>
- <Typography variant="body2" color="text.secondary">
- {t("Completed")}: {dayjs(doPickOrder.completedDate).format(OUTPUT_DATE_FORMAT)}
- </Typography>
- </Box>
- <Box>
- <Chip
- label={t(doPickOrder.pickOrderStatus)}
- color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'}
- size="small"
- sx={{ mb: 1 }}
- />
- <Typography variant="body2" color="text.secondary">
- {doPickOrder.fgPickOrders.length} {t("FG orders")}
- </Typography>
- </Box>
- </Stack>
- </CardContent>
- <CardActions>
- <Button
- variant="outlined"
- onClick={() => handleDetailClick(doPickOrder)}
- >
- {t("View Details")}
- </Button>
-
- <>
- <Button
- variant="contained"
- onClick={() => handleDN(
- doPickOrder.id
- )}
- >
- {t("Print Pick Order")}
- </Button>
- <Button
- variant="contained"
- onClick={() => handleDNandLabel(
- doPickOrder.id
- )}
- >
- {t("Print DN & Label")}
- </Button>
- <Button
- variant="contained"
- onClick={() => handleLabel(
- doPickOrder.id
- )}
- >
- {t("Print Label")}
- </Button>
- </>
- )
- </CardActions>
- </Card>
- ))}
- </Stack>
- )}
-
- {/* 分页 */}
- {filteredDoPickOrders.length > 0 && (
- <TablePagination
- component="div"
- count={filteredDoPickOrders.length}
- page={paginationController.pageNum}
- rowsPerPage={paginationController.pageSize}
- onPageChange={handlePageChange}
- onRowsPerPageChange={handlePageSizeChange}
- rowsPerPageOptions={[5, 10, 25, 50]}
- />
- )}
- </Box>
- )}
- </Box>
- </FormProvider>
- );
- };
-
- export default GoodPickExecutionRecord;
|