diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 0aa7986..ee6a566 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -508,6 +508,11 @@ export interface ProductProcessLineInfoResponse { startTime: string, endTime: string } +export interface FloorPickCount { + floor: string; + finishedCount: number; + totalCount: number; +} export interface AllJoPickOrderResponse { id: number; pickOrderId: number | null; @@ -523,6 +528,7 @@ export interface AllJoPickOrderResponse { uomName: string; jobOrderStatus: string; finishedPickOLineCount: number; + floorPickCounts: FloorPickCount[]; } export interface UpdateJoPickOrderHandledByRequest { pickOrderId: number; diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 22c519a..548f2c1 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -1384,4 +1384,55 @@ export const fetchReleasedDoPickOrders = async (): Promise => { + const params = new URLSearchParams(); + if (shopName?.trim()) params.append("shopName", shopName.trim()); + if (storeId?.trim()) params.append("storeId", storeId.trim()); + if (truck?.trim()) params.append("truck", truck.trim()); + const query = params.toString(); + const url = `${BASE_API_URL}/doPickOrder/released${query ? `?${query}` : ""}`; + const response = await serverFetchJson(url, { + method: "GET", + }); + return response ?? []; +}; +export const fetchReleasedDoPickOrderCountByStore = async ( + storeId: string +): Promise => { + const list = await fetchReleasedDoPickOrdersForSelection(undefined, storeId); + return list.length; +}; +// 新增:依 doPickOrderId 分配 +export const assignByDoPickOrderId = async ( + userId: number, + doPickOrderId: number +): Promise => { + const response = await serverFetchJson( + `${BASE_API_URL}/doPickOrder/assign-by-id`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId, doPickOrderId }), + } + ); + revalidateTag("pickorder"); + return response; }; \ No newline at end of file diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index 83403a5..7b9c1a2 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -95,6 +95,7 @@ export interface AllPickedStockTakeListReponse { totalItemNumber: number; startTime: string | null; endTime: string | null; + planStartDate: string | null; reStockTakeTrueFalse: boolean; } diff --git a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx index 3f14e5f..b396b42 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx @@ -5,9 +5,10 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; -import { fetchStoreLaneSummary, assignByLane, type StoreLaneSummary } from "@/app/api/pickOrder/actions"; +import { fetchStoreLaneSummary,fetchReleasedDoPickOrdersForSelection,fetchReleasedDoPickOrderCountByStore, assignByLane, type StoreLaneSummary } from "@/app/api/pickOrder/actions"; import Swal from "sweetalert2"; import dayjs from "dayjs"; +import ReleasedDoPickOrderSelectModal from "./ReleasedDoPickOrderSelectModal"; interface Props { onPickOrderAssigned?: () => void; @@ -18,7 +19,11 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSw const { t } = useTranslation("pickOrder"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; - + const [selectedStore, setSelectedStore] = useState("2/F"); + const [selectedTruck, setSelectedTruck] = useState(""); + const [modalOpen, setModalOpen] = useState(false); + const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]); + const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]); const [summary2F, setSummary2F] = useState(null); const [summary4F, setSummary4F] = useState(null); const [isLoadingSummary, setIsLoadingSummary] = useState(false); @@ -56,7 +61,33 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSw useEffect(() => { loadSummaries(); }, [loadSummaries]); - + useEffect(() => { + const loadCounts = async () => { + try { + const [list2F, list4F] = await Promise.all([ + fetchReleasedDoPickOrdersForSelection(undefined, "2/F"), + fetchReleasedDoPickOrdersForSelection(undefined, "4/F"), + ]); + const groupByTruck = (list: { truckLanceCode?: string | null }[]) => { + const map: Record = {}; + list.forEach((item) => { + const t = item.truckLanceCode || "-"; + map[t] = (map[t] || 0) + 1; + }); + return Object.entries(map) + .map(([truck, count]) => ({ truck, count })) + .sort((a, b) => a.truck.localeCompare(b.truck)); + }; + setTruckCounts2F(groupByTruck(list2F)); + setTruckCounts4F(groupByTruck(list4F)); + } catch (e) { + console.error("Error loading counts:", e); + setTruckCounts2F([]); + setTruckCounts4F([]); + } + }; + loadCounts(); + }, [loadSummaries]); const handleAssignByLane = useCallback(async ( storeId: string, truckDepartureTime: string, @@ -468,6 +499,194 @@ const getDateLabel = (offset: number) => { + {/* 2/F 未完成已放單 - 與上方相同 UI */} + + + + {t("Not yet finished released do pick orders")} + + + {t("Released orders not yet completed - click lane to select and assign")} + + + + + + + 2/F + + + {truckCounts2F.length === 0 ? ( + + {t("No entries available")} + + ) : ( + + {truckCounts2F.map(({ truck, count }) => ( + + + + + + + ))} + + )} + + + + + {/* 4/F 未完成已放單 - 與上方相同 UI */} + + + + 4/F + + + {truckCounts4F.length === 0 ? ( + + {t("No entries available")} + + ) : ( + + {truckCounts4F.map(({ truck, count }) => ( + + + + + + + ))} + + )} + + + + setModalOpen(false)} + onAssigned={() => { + loadSummaries(); + const loadCounts = async () => { + try { + const [list2F, list4F] = await Promise.all([ + fetchReleasedDoPickOrdersForSelection(undefined, "2/F"), + fetchReleasedDoPickOrdersForSelection(undefined, "4/F"), + ]); + const groupByTruck = (list: { truckLanceCode?: string | null }[]) => { + const map: Record = {}; + list.forEach((item) => { + const t = item.truckLanceCode || "-"; + map[t] = (map[t] || 0) + 1; + }); + return Object.entries(map) + .map(([truck, count]) => ({ truck, count })) + .sort((a, b) => a.truck.localeCompare(b.truck)); + }; + setTruckCounts2F(groupByTruck(list2F)); + setTruckCounts4F(groupByTruck(list4F)); + } catch (e) { + setTruckCounts2F([]); + setTruckCounts4F([]); + } + }; + loadCounts(); + onPickOrderAssigned?.(); + onSwitchToDetailTab?.(); + }} +/> diff --git a/src/components/FinishedGoodSearch/GoodPickExecution.tsx b/src/components/FinishedGoodSearch/GoodPickExecution.tsx index 7d8147b..5a4d61a 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecution.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecution.tsx @@ -1097,7 +1097,7 @@ const fetchFgPickOrdersData = useCallback(async () => { const paginatedData = useMemo(() => { // ✅ Fix: Add safety check to ensure combinedLotData is an array if (!Array.isArray(combinedLotData)) { - console.warn("⚠️ combinedLotData is not an array:", combinedLotData); + console.warn(" combinedLotData is not an array:", combinedLotData); return []; } diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 9097a15..3c773a1 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -3124,7 +3124,7 @@ paginatedData.map((lot, index) => { }} > {lot.lotNo || - t('No Stock Available')} + t('This lot is not available, please scan another lot.')} diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 6655a92..ad0888c 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -72,7 +72,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT } return response.json(); }; - +/* useEffect(() => { const fetchDetailedJos = async () => { const detailedMap = new Map(); @@ -98,6 +98,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchDetailedJos(); } }, [filteredJos]); + */ /* useEffect(() => { const fetchInventoryData = async () => { diff --git a/src/components/Jodetail/JoPickOrderList.tsx b/src/components/Jodetail/JoPickOrderList.tsx index 938960b..a9d12c2 100644 --- a/src/components/Jodetail/JoPickOrderList.tsx +++ b/src/components/Jodetail/JoPickOrderList.tsx @@ -140,6 +140,11 @@ const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ {t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName}) + {pickOrder.floorPickCounts?.map(({ floor, finishedCount, totalCount }) => ( + + {floor}: {finishedCount}/{totalCount} + + ))} {statusLower !== "pending" && finishedCount > 0 && ( diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 3b9a9d8..3742fc0 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -186,7 +186,8 @@ const QrCodeModal: React.FC<{ const { t } = useTranslation("jo"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [manualInput, setManualInput] = useState(''); - + // 楼层筛选状态 + const [selectedFloor, setSelectedFloor] = useState(null); const [manualInputSubmitted, setManualInputSubmitted] = useState(false); const [manualInputError, setManualInputError] = useState(false); const [isProcessingQr, setIsProcessingQr] = useState(false); @@ -474,7 +475,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // Add QR modal states const [qrModalOpen, setQrModalOpen] = useState(false); const [selectedLotForQr, setSelectedLotForQr] = useState(null); - + const [selectedFloor, setSelectedFloor] = useState(null); // Add GoodPickExecutionForm states const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); @@ -545,6 +546,17 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { return allLots; }, []); + const extractFloor = (lot: any): string => { + const raw = lot.routerRoute || lot.routerArea || lot.location || ''; + const match = raw.match(/^(\d+F?)/i) || raw.split('-')[0]; + return (match?.[1] || match || raw || '').toUpperCase().replace(/(\d)F?/i, '$1F'); + }; + + // 楼层排序权重:4F > 3F > 2F(数字越大越靠前) + const floorSortOrder = (floor: string): number => { + const n = parseInt(floor.replace(/\D/g, ''), 10); + return isNaN(n) ? 0 : n; + }; const combinedLotData = useMemo(() => { return getAllLotsFromHierarchical(jobOrderData); }, [jobOrderData, getAllLotsFromHierarchical]); @@ -1910,23 +1922,31 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const scannedItemsCount = useMemo(() => { return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; }, [combinedLotData]); - - // Progress bar data (align with Finished Good execution detail) - const progress = useMemo(() => { - if (combinedLotData.length === 0) { - return { completed: 0, total: 0 }; - } - - const nonPendingCount = combinedLotData.filter((lot) => { - const status = lot.stockOutLineStatus?.toLowerCase(); - return status !== 'pending'; - }).length; - - return { - completed: nonPendingCount, - total: combinedLotData.length, - }; + + // 先定义 filteredByFloor 和 availableFloors + const availableFloors = useMemo(() => { + const floors = new Set(); + combinedLotData.forEach(lot => { + const f = extractFloor(lot); + if (f) floors.add(f); + }); + return Array.from(floors).sort((a, b) => floorSortOrder(b) - floorSortOrder(a)); }, [combinedLotData]); + + const filteredByFloor = useMemo(() => { + if (!selectedFloor) return combinedLotData; + return combinedLotData.filter(lot => extractFloor(lot) === selectedFloor); + }, [combinedLotData, selectedFloor]); + + // Progress bar data - 现在可以正确引用 filteredByFloor + const progress = useMemo(() => { + const data = selectedFloor ? filteredByFloor : combinedLotData; + if (data.length === 0) return { completed: 0, total: 0 }; + const nonPendingCount = data.filter(lot => + lot.stockOutLineStatus?.toLowerCase() !== 'pending' + ).length; + return { completed: nonPendingCount, total: data.length }; + }, [selectedFloor, filteredByFloor, combinedLotData]); // Handle reject lot const handleRejectLot = useCallback(async (lot: any) => { if (!lot.stockOutLineId) { @@ -2057,15 +2077,18 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // Pagination data with sorting by routerIndex const paginatedData = useMemo(() => { - // Sort by routerIndex first, then by other criteria - const sortedData = [...combinedLotData].sort((a, b) => { - const aIndex = a.routerIndex || 0; - const bIndex = b.routerIndex || 0; - - // Primary sort: by routerIndex - if (aIndex !== bIndex) { - return aIndex - bIndex; - } + + const sourceData = selectedFloor ? filteredByFloor : combinedLotData; +const sortedData = [...sourceData].sort((a, b) => { + const floorA = extractFloor(a); + const floorB = extractFloor(b); + const orderA = floorSortOrder(floorA); + const orderB = floorSortOrder(floorB); + if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F + // 同楼层再按 routerIndex、pickOrderCode、lotNo + const aIndex = a.routerIndex ?? 0; + const bIndex = b.routerIndex ?? 0; + if (aIndex !== bIndex) return aIndex - bIndex; // Secondary sort: by pickOrderCode if routerIndex is the same if (a.pickOrderCode !== b.pickOrderCode) { @@ -2079,7 +2102,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; return sortedData.slice(startIndex, endIndex); - }, [combinedLotData, paginationController]); + }, [selectedFloor, filteredByFloor, combinedLotData, paginationController]); // Add these functions for manual scanning const handleStartScan = useCallback(() => { @@ -2188,7 +2211,25 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { successMessage={t("QR code verified.")} /> - + + + {availableFloors.map(floor => ( + + ))} + {/* Job Order Header */} {jobOrderData && ( @@ -2479,7 +2520,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { = ({ onCardClick, onReStockT if (session.totalInventoryLotNumber === 0) return 0; return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); }; - + const planStartDate = (() => { + const first = stockTakeSessions.find(s => s.planStartDate); + if (!first?.planStartDate) return null; + return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT); + })(); if (loading) { return ( @@ -177,9 +181,15 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT return ( + + {t("Total Sections")}: {stockTakeSessions.length} + + {t("Start Stock Take Date")}: {planStartDate || "-"} + + + + {!submitDisabled && isSecondSubmit ? ( <> @@ -595,18 +607,7 @@ const PickerReStockTake: React.FC = ({ )} - - - - - + ); }) diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index 7ece7a7..a62323d 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -517,10 +517,10 @@ const PickerStockTake: React.FC = ({ {t("Item-lotNo-ExpiryDate")} {t("UOM")} {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Action")} {t("Remark")} - {t("Record Status")} - {t("Action")} + @@ -728,7 +728,21 @@ const PickerStockTake: React.FC = ({ )} - + + + + + + + + {/* Remark */} {!submitDisabled && isSecondSubmit ? ( @@ -755,7 +769,7 @@ const PickerStockTake: React.FC = ({ - + {detail.stockTakeRecordStatus === "completed" ? ( = ({ )} - - - - - - - - + ); })