| @@ -508,6 +508,11 @@ export interface ProductProcessLineInfoResponse { | |||||
| startTime: string, | startTime: string, | ||||
| endTime: string | endTime: string | ||||
| } | } | ||||
| export interface FloorPickCount { | |||||
| floor: string; | |||||
| finishedCount: number; | |||||
| totalCount: number; | |||||
| } | |||||
| export interface AllJoPickOrderResponse { | export interface AllJoPickOrderResponse { | ||||
| id: number; | id: number; | ||||
| pickOrderId: number | null; | pickOrderId: number | null; | ||||
| @@ -523,6 +528,7 @@ export interface AllJoPickOrderResponse { | |||||
| uomName: string; | uomName: string; | ||||
| jobOrderStatus: string; | jobOrderStatus: string; | ||||
| finishedPickOLineCount: number; | finishedPickOLineCount: number; | ||||
| floorPickCounts: FloorPickCount[]; | |||||
| } | } | ||||
| export interface UpdateJoPickOrderHandledByRequest { | export interface UpdateJoPickOrderHandledByRequest { | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| @@ -1384,4 +1384,55 @@ export const fetchReleasedDoPickOrders = async (): Promise<ReleasedDoPickOrderRe | |||||
| }, | }, | ||||
| ); | ); | ||||
| return response; | return response; | ||||
| }; | |||||
| // 新增:Released Do Pick Order 列表項目(對應後端 ReleasedDoPickOrderListItem) | |||||
| export interface ReleasedDoPickOrderListItem { | |||||
| id: number; | |||||
| requiredDeliveryDate: string | null; | |||||
| shopCode: string | null; | |||||
| shopName: string | null; | |||||
| storeId: string | null; | |||||
| truckLanceCode: string | null; | |||||
| truckDepartureTime: string | null; | |||||
| deliveryOrderCodes: string[]; | |||||
| } | |||||
| // 修改:fetchReleasedDoPickOrders 支援 shopName 篩選,並回傳新結構 | |||||
| export const fetchReleasedDoPickOrdersForSelection = async ( | |||||
| shopName?: string, | |||||
| storeId?: string, | |||||
| truck?: string | |||||
| ): Promise<ReleasedDoPickOrderListItem[]> => { | |||||
| 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<ReleasedDoPickOrderListItem[]>(url, { | |||||
| method: "GET", | |||||
| }); | |||||
| return response ?? []; | |||||
| }; | |||||
| export const fetchReleasedDoPickOrderCountByStore = async ( | |||||
| storeId: string | |||||
| ): Promise<number> => { | |||||
| const list = await fetchReleasedDoPickOrdersForSelection(undefined, storeId); | |||||
| return list.length; | |||||
| }; | |||||
| // 新增:依 doPickOrderId 分配 | |||||
| export const assignByDoPickOrderId = async ( | |||||
| userId: number, | |||||
| doPickOrderId: number | |||||
| ): Promise<PostPickOrderResponse> => { | |||||
| const response = await serverFetchJson<PostPickOrderResponse>( | |||||
| `${BASE_API_URL}/doPickOrder/assign-by-id`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| body: JSON.stringify({ userId, doPickOrderId }), | |||||
| } | |||||
| ); | |||||
| revalidateTag("pickorder"); | |||||
| return response; | |||||
| }; | }; | ||||
| @@ -95,6 +95,7 @@ export interface AllPickedStockTakeListReponse { | |||||
| totalItemNumber: number; | totalItemNumber: number; | ||||
| startTime: string | null; | startTime: string | null; | ||||
| endTime: string | null; | endTime: string | null; | ||||
| planStartDate: string | null; | |||||
| reStockTakeTrueFalse: boolean; | reStockTakeTrueFalse: boolean; | ||||
| } | } | ||||
| @@ -5,9 +5,10 @@ import { useCallback, useEffect, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | 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 Swal from "sweetalert2"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import ReleasedDoPickOrderSelectModal from "./ReleasedDoPickOrderSelectModal"; | |||||
| interface Props { | interface Props { | ||||
| onPickOrderAssigned?: () => void; | onPickOrderAssigned?: () => void; | ||||
| @@ -18,7 +19,11 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const [selectedStore, setSelectedStore] = useState<string>("2/F"); | |||||
| const [selectedTruck, setSelectedTruck] = useState<string>(""); | |||||
| 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<StoreLaneSummary | null>(null); | const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null); | ||||
| const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null); | const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null); | ||||
| const [isLoadingSummary, setIsLoadingSummary] = useState(false); | const [isLoadingSummary, setIsLoadingSummary] = useState(false); | ||||
| @@ -56,7 +61,33 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| useEffect(() => { | useEffect(() => { | ||||
| loadSummaries(); | loadSummaries(); | ||||
| }, [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<string, number> = {}; | |||||
| 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 ( | const handleAssignByLane = useCallback(async ( | ||||
| storeId: string, | storeId: string, | ||||
| truckDepartureTime: string, | truckDepartureTime: string, | ||||
| @@ -468,6 +499,194 @@ const getDateLabel = (offset: number) => { | |||||
| </Box> | </Box> | ||||
| </Stack> | </Stack> | ||||
| </Grid> | </Grid> | ||||
| {/* 2/F 未完成已放單 - 與上方相同 UI */} | |||||
| <Grid item xs={12}> | |||||
| <Box | |||||
| sx={{ | |||||
| py: 2, | |||||
| mt: 1, | |||||
| mb: 0.5, | |||||
| borderTop: "1px solid #e0e0e0", | |||||
| }} | |||||
| > | |||||
| <Typography | |||||
| variant="subtitle1" | |||||
| sx={{ fontWeight: 600, mb: 0.5 }} | |||||
| > | |||||
| {t("Not yet finished released do pick orders")} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Released orders not yet completed - click lane to select and assign")} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> | |||||
| 2/F | |||||
| </Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| border: "1px solid #e0e0e0", | |||||
| borderRadius: 1, | |||||
| p: 1, | |||||
| backgroundColor: "#fafafa", | |||||
| flex: 1, | |||||
| }} | |||||
| > | |||||
| {truckCounts2F.length === 0 ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: "1rem", | |||||
| textAlign: "center", | |||||
| py: 1, | |||||
| }} | |||||
| > | |||||
| {t("No entries available")} | |||||
| </Typography> | |||||
| ) : ( | |||||
| <Grid container spacing={1}> | |||||
| {truckCounts2F.map(({ truck, count }) => ( | |||||
| <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="medium" | |||||
| onClick={() => { | |||||
| setSelectedStore("2/F"); | |||||
| setSelectedTruck(truck); | |||||
| setModalOpen(true); | |||||
| }} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| fontSize: "1.1rem", | |||||
| py: 1, | |||||
| px: 1.5, | |||||
| borderWidth: 1, | |||||
| borderColor: "#ccc", | |||||
| fontWeight: 500, | |||||
| "&:hover": { | |||||
| borderColor: "#999", | |||||
| backgroundColor: "#f5f5f5", | |||||
| }, | |||||
| }} | |||||
| > | |||||
| {`${truck} (${count})`} | |||||
| </Button> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| </Grid> | |||||
| {/* 4/F 未完成已放單 - 與上方相同 UI */} | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||||
| <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}> | |||||
| 4/F | |||||
| </Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| border: "1px solid #e0e0e0", | |||||
| borderRadius: 1, | |||||
| p: 1, | |||||
| backgroundColor: "#fafafa", | |||||
| flex: 1, | |||||
| }} | |||||
| > | |||||
| {truckCounts4F.length === 0 ? ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| sx={{ | |||||
| fontWeight: 600, | |||||
| fontSize: "1rem", | |||||
| textAlign: "center", | |||||
| py: 1, | |||||
| }} | |||||
| > | |||||
| {t("No entries available")} | |||||
| </Typography> | |||||
| ) : ( | |||||
| <Grid container spacing={1}> | |||||
| {truckCounts4F.map(({ truck, count }) => ( | |||||
| <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="medium" | |||||
| onClick={() => { | |||||
| setSelectedStore("4/F"); | |||||
| setSelectedTruck(truck); | |||||
| setModalOpen(true); | |||||
| }} | |||||
| sx={{ | |||||
| flex: 1, | |||||
| fontSize: "1.1rem", | |||||
| py: 1, | |||||
| px: 1.5, | |||||
| borderWidth: 1, | |||||
| borderColor: "#ccc", | |||||
| fontWeight: 500, | |||||
| "&:hover": { | |||||
| borderColor: "#999", | |||||
| backgroundColor: "#f5f5f5", | |||||
| }, | |||||
| }} | |||||
| > | |||||
| {`${truck} (${count})`} | |||||
| </Button> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| )} | |||||
| </Box> | |||||
| </Stack> | |||||
| </Grid> | |||||
| <ReleasedDoPickOrderSelectModal | |||||
| open={modalOpen} | |||||
| storeId={selectedStore} | |||||
| truck={selectedTruck} | |||||
| onClose={() => 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<string, number> = {}; | |||||
| 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?.(); | |||||
| }} | |||||
| /> | |||||
| </Grid> | </Grid> | ||||
| @@ -1097,7 +1097,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||||
| const paginatedData = useMemo(() => { | const paginatedData = useMemo(() => { | ||||
| // ✅ Fix: Add safety check to ensure combinedLotData is an array | // ✅ Fix: Add safety check to ensure combinedLotData is an array | ||||
| if (!Array.isArray(combinedLotData)) { | if (!Array.isArray(combinedLotData)) { | ||||
| console.warn("⚠️ combinedLotData is not an array:", combinedLotData); | |||||
| console.warn(" combinedLotData is not an array:", combinedLotData); | |||||
| return []; | return []; | ||||
| } | } | ||||
| @@ -3124,7 +3124,7 @@ paginatedData.map((lot, index) => { | |||||
| }} | }} | ||||
| > | > | ||||
| {lot.lotNo || | {lot.lotNo || | ||||
| t('No Stock Available')} | |||||
| t('This lot is not available, please scan another lot.')} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -72,7 +72,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| } | } | ||||
| return response.json(); | return response.json(); | ||||
| }; | }; | ||||
| /* | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchDetailedJos = async () => { | const fetchDetailedJos = async () => { | ||||
| const detailedMap = new Map<number, JobOrder>(); | const detailedMap = new Map<number, JobOrder>(); | ||||
| @@ -98,6 +98,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| fetchDetailedJos(); | fetchDetailedJos(); | ||||
| } | } | ||||
| }, [filteredJos]); | }, [filteredJos]); | ||||
| */ | |||||
| /* | /* | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchInventoryData = async () => { | const fetchInventoryData = async () => { | ||||
| @@ -140,6 +140,11 @@ const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName}) | {t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName}) | ||||
| </Typography> | </Typography> | ||||
| {pickOrder.floorPickCounts?.map(({ floor, finishedCount, totalCount }) => ( | |||||
| <Typography key={floor} variant="body2" color="text.secondary" component="span" sx={{ mr: 1 }}> | |||||
| {floor}: {finishedCount}/{totalCount} | |||||
| </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}> | ||||
| @@ -186,7 +186,8 @@ const QrCodeModal: React.FC<{ | |||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const [manualInput, setManualInput] = useState<string>(''); | const [manualInput, setManualInput] = useState<string>(''); | ||||
| // 楼层筛选状态 | |||||
| const [selectedFloor, setSelectedFloor] = useState<string | null>(null); | |||||
| const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); | const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); | ||||
| const [manualInputError, setManualInputError] = useState<boolean>(false); | const [manualInputError, setManualInputError] = useState<boolean>(false); | ||||
| const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); | const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); | ||||
| @@ -474,7 +475,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| // Add QR modal states | // Add QR modal states | ||||
| const [qrModalOpen, setQrModalOpen] = useState(false); | const [qrModalOpen, setQrModalOpen] = useState(false); | ||||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | ||||
| const [selectedFloor, setSelectedFloor] = useState<string | null>(null); | |||||
| // Add GoodPickExecutionForm states | // Add GoodPickExecutionForm states | ||||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | ||||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | ||||
| @@ -545,6 +546,17 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| return allLots; | 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(() => { | const combinedLotData = useMemo(() => { | ||||
| return getAllLotsFromHierarchical(jobOrderData); | return getAllLotsFromHierarchical(jobOrderData); | ||||
| }, [jobOrderData, getAllLotsFromHierarchical]); | }, [jobOrderData, getAllLotsFromHierarchical]); | ||||
| @@ -1910,23 +1922,31 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const scannedItemsCount = useMemo(() => { | const scannedItemsCount = useMemo(() => { | ||||
| return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; | return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; | ||||
| }, [combinedLotData]); | }, [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<string>(); | |||||
| combinedLotData.forEach(lot => { | |||||
| const f = extractFloor(lot); | |||||
| if (f) floors.add(f); | |||||
| }); | |||||
| return Array.from(floors).sort((a, b) => floorSortOrder(b) - floorSortOrder(a)); | |||||
| }, [combinedLotData]); | }, [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 | // Handle reject lot | ||||
| const handleRejectLot = useCallback(async (lot: any) => { | const handleRejectLot = useCallback(async (lot: any) => { | ||||
| if (!lot.stockOutLineId) { | if (!lot.stockOutLineId) { | ||||
| @@ -2057,15 +2077,18 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| // Pagination data with sorting by routerIndex | // Pagination data with sorting by routerIndex | ||||
| const paginatedData = useMemo(() => { | 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 | // Secondary sort: by pickOrderCode if routerIndex is the same | ||||
| if (a.pickOrderCode !== b.pickOrderCode) { | if (a.pickOrderCode !== b.pickOrderCode) { | ||||
| @@ -2079,7 +2102,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | const startIndex = paginationController.pageNum * paginationController.pageSize; | ||||
| const endIndex = startIndex + paginationController.pageSize; | const endIndex = startIndex + paginationController.pageSize; | ||||
| return sortedData.slice(startIndex, endIndex); | return sortedData.slice(startIndex, endIndex); | ||||
| }, [combinedLotData, paginationController]); | |||||
| }, [selectedFloor, filteredByFloor, combinedLotData, paginationController]); | |||||
| // Add these functions for manual scanning | // Add these functions for manual scanning | ||||
| const handleStartScan = useCallback(() => { | const handleStartScan = useCallback(() => { | ||||
| @@ -2188,7 +2211,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| successMessage={t("QR code verified.")} | successMessage={t("QR code verified.")} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}> | |||||
| <Button | |||||
| variant={selectedFloor === null ? 'contained' : 'outlined'} | |||||
| size="small" | |||||
| onClick={() => setSelectedFloor(null)} | |||||
| > | |||||
| {t("All")} | |||||
| </Button> | |||||
| {availableFloors.map(floor => ( | |||||
| <Button | |||||
| key={floor} | |||||
| variant={selectedFloor === floor ? 'contained' : 'outlined'} | |||||
| size="small" | |||||
| onClick={() => setSelectedFloor(floor)} | |||||
| > | |||||
| {floor} | |||||
| </Button> | |||||
| ))} | |||||
| </Box> | |||||
| {/* Job Order Header */} | {/* Job Order Header */} | ||||
| {jobOrderData && ( | {jobOrderData && ( | ||||
| <Paper sx={{ p: 2 }}> | <Paper sx={{ p: 2 }}> | ||||
| @@ -2479,7 +2520,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => { | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={combinedLotData.length} | |||||
| count={selectedFloor ? filteredByFloor.length : combinedLotData.length} | |||||
| page={paginationController.pageNum} | page={paginationController.pageNum} | ||||
| rowsPerPage={paginationController.pageSize} | rowsPerPage={paginationController.pageSize} | ||||
| onPageChange={handlePageChange} | onPageChange={handlePageChange} | ||||
| @@ -165,7 +165,11 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| if (session.totalInventoryLotNumber === 0) return 0; | if (session.totalInventoryLotNumber === 0) return 0; | ||||
| return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); | 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) { | if (loading) { | ||||
| return ( | return ( | ||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | ||||
| @@ -177,9 +181,15 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Total Sections")}: {stockTakeSessions.length} | {t("Total Sections")}: {stockTakeSessions.length} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Start Stock Take Date")}: {planStartDate || "-"} | |||||
| </Typography> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="primary" | color="primary" | ||||
| @@ -214,12 +224,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| <Typography variant="subtitle1" fontWeight={600}> | <Typography variant="subtitle1" fontWeight={600}> | ||||
| {t("Section")}: {session.stockTakeSession} | {t("Section")}: {session.stockTakeSession} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| </Typography> | |||||
| </Stack> | </Stack> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | ||||
| @@ -390,9 +390,9 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | ||||
| <TableCell>{t("UOM")}</TableCell> | <TableCell>{t("UOM")}</TableCell> | ||||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("Record Status")}</TableCell> | <TableCell>{t("Record Status")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -562,6 +562,18 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| onClick={() => handleSaveStockTake(detail)} | |||||
| disabled={saving || submitDisabled } | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </TableCell> | |||||
| <TableCell sx={{ width: 180 }}> | <TableCell sx={{ width: 180 }}> | ||||
| {!submitDisabled && isSecondSubmit ? ( | {!submitDisabled && isSecondSubmit ? ( | ||||
| <> | <> | ||||
| @@ -595,18 +607,7 @@ const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({ | |||||
| <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" /> | <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" /> | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| onClick={() => handleSaveStockTake(detail)} | |||||
| disabled={saving || submitDisabled } | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| ); | ); | ||||
| }) | }) | ||||
| @@ -517,10 +517,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | ||||
| <TableCell>{t("UOM")}</TableCell> | <TableCell>{t("UOM")}</TableCell> | ||||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("Record Status")}</TableCell> | <TableCell>{t("Record Status")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -728,7 +728,21 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| onClick={() => handleSaveStockTake(detail)} | |||||
| disabled={saving || submitDisabled} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </TableCell> | |||||
| {/* Remark */} | {/* Remark */} | ||||
| <TableCell sx={{ width: 180 }}> | <TableCell sx={{ width: 180 }}> | ||||
| {!submitDisabled && isSecondSubmit ? ( | {!submitDisabled && isSecondSubmit ? ( | ||||
| @@ -755,7 +769,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| {detail.stockTakeRecordStatus === "completed" ? ( | {detail.stockTakeRecordStatus === "completed" ? ( | ||||
| <Chip | <Chip | ||||
| @@ -784,21 +798,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| size="small" | |||||
| variant="contained" | |||||
| onClick={() => handleSaveStockTake(detail)} | |||||
| disabled={saving || submitDisabled} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| ); | ); | ||||
| }) | }) | ||||