| @@ -133,6 +133,7 @@ export interface PrintPickRecordRequest{ | |||||
| pickOrderId: number; | pickOrderId: number; | ||||
| printerId: number; | printerId: number; | ||||
| printQty: number; | printQty: number; | ||||
| floor?: "2F" | "3F" | "4F" | "ALL"; | |||||
| } | } | ||||
| export interface PrintPickRecordResponse{ | export interface PrintPickRecordResponse{ | ||||
| @@ -604,6 +605,9 @@ export interface StockOutLineDetailResponse { | |||||
| location: string | null; | location: string | null; | ||||
| availableQty: number | null; | availableQty: number | null; | ||||
| noLot: boolean; | noLot: boolean; | ||||
| /** Workbench API: matched suggest_pick_lot qty for this SOL lot line */ | |||||
| //suggestedPickQty?: number | null; | |||||
| //suggestedPickLotId?: number | null; | |||||
| } | } | ||||
| export interface LotDetailResponse { | export interface LotDetailResponse { | ||||
| @@ -712,6 +716,21 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder | |||||
| }, | }, | ||||
| ); | ); | ||||
| }); | }); | ||||
| /** JO Workbench: in−out available (matches scan-pick); stockouts include suggestedPickQty / suggestedPickLotId when SPL matches SOL lot line */ | |||||
| /* | |||||
| export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache( | |||||
| async (pickOrderId: number) => { | |||||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||||
| `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["jo-hierarchical-workbench"] }, | |||||
| }, | |||||
| ); | |||||
| }, | |||||
| ); | |||||
| */ | |||||
| // NOTE: Do NOT wrap in `cache()` because the list needs to reflect just-completed lines | // NOTE: Do NOT wrap in `cache()` because the list needs to reflect just-completed lines | ||||
| // immediately when navigating back from JobPickExecution. | // immediately when navigating back from JobPickExecution. | ||||
| export const fetchAllJoPickOrders = async (type?: string | null, floor?: string | null) => { | export const fetchAllJoPickOrders = async (type?: string | null, floor?: string | null) => { | ||||
| @@ -1076,6 +1095,32 @@ export const fetchCompletedJobOrderPickOrdersrecords = async (completedDate?: st | |||||
| cache: "no-store", | cache: "no-store", | ||||
| }); | }); | ||||
| }; | }; | ||||
| export const fetchJobOrderPickOrdersrecords = async ( | |||||
| date?: string | null, | |||||
| status?: string | null, | |||||
| ) => { | |||||
| const params = new URLSearchParams(); | |||||
| if (date && String(date).trim() !== "") { | |||||
| params.set("date", String(date).trim()); | |||||
| } | |||||
| if (status && String(status).trim() !== "" && String(status) !== "All") { | |||||
| params.set("status", String(status).trim()); | |||||
| } | |||||
| const q = params.toString() ? `?${params.toString()}` : ""; | |||||
| return serverFetchJson<any>(`${BASE_API_URL}/jo/job-order-pick-orders${q}`, { | |||||
| method: "GET", | |||||
| cache: "no-store", | |||||
| }); | |||||
| }; | |||||
| export const fetchJobOrderPickOrderLotDetailsForPick = cache(async (pickOrderId: number) => { | |||||
| return serverFetchJson<any[]>(`${BASE_API_URL}/jo/job-order-pick-order-lot-details/${pickOrderId}`, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" } | |||||
| }) | |||||
| }) | |||||
| export const fetchJoForPrintQrCode = cache(async (date: string) => { | export const fetchJoForPrintQrCode = cache(async (date: string) => { | ||||
| return serverFetchJson<JobOrderListForPrintQrCodeResponse[]>( | return serverFetchJson<JobOrderListForPrintQrCodeResponse[]>( | ||||
| `${BASE_API_URL}/jo/joForPrintQrCode/${date}`, | `${BASE_API_URL}/jo/joForPrintQrCode/${date}`, | ||||
| @@ -1274,6 +1319,9 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ | |||||
| if (request.printQty !== null && request.printQty !== undefined) { | if (request.printQty !== null && request.printQty !== undefined) { | ||||
| params.append('printQty', request.printQty.toString()); | params.append('printQty', request.printQty.toString()); | ||||
| } | } | ||||
| if (request.floor) { | |||||
| params.append('floor', request.floor); | |||||
| } | |||||
| //const response = await serverFetchWithNoContent(`${BASE_API_URL}/jo/print-PickRecord?${params.toString()}`,{ | //const response = await serverFetchWithNoContent(`${BASE_API_URL}/jo/print-PickRecord?${params.toString()}`,{ | ||||
| const response = await serverFetchWithNoContent(`${BASE_API_URL}/jo/print-PickRecord?${params.toString()}`,{ | const response = await serverFetchWithNoContent(`${BASE_API_URL}/jo/print-PickRecord?${params.toString()}`,{ | ||||
| @@ -0,0 +1,255 @@ | |||||
| "use client"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| Alert, | |||||
| Box, | |||||
| CircularProgress, | |||||
| Grid, | |||||
| MenuItem, | |||||
| Paper, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import type { ApexOptions } from "apexcharts"; | |||||
| import dayjs from "dayjs"; | |||||
| import { | |||||
| CompletedDoPickOrderResponse, | |||||
| fetchCompletedDoPickOrdersAll, | |||||
| } from "@/app/api/pickOrder/actions"; | |||||
| import SafeApexCharts from "@/components/charts/SafeApexCharts"; | |||||
| type FloorFilter = "all" | "2/F" | "4/F"; | |||||
| type DailySummaryRow = { | |||||
| date: string; | |||||
| floor2F: number; | |||||
| floor4F: number; | |||||
| truckX: number; | |||||
| total: number; | |||||
| }; | |||||
| const FinishedGoodCartonDashboardTab: React.FC = () => { | |||||
| const [floor, setFloor] = useState<FloorFilter>("all"); | |||||
| const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD")); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [error, setError] = useState<string>(""); | |||||
| const [records, setRecords] = useState<CompletedDoPickOrderResponse[]>([]); | |||||
| const loadData = useCallback(async () => { | |||||
| setLoading(true); | |||||
| setError(""); | |||||
| try { | |||||
| const data = await fetchCompletedDoPickOrdersAll( | |||||
| date ? { targetDate: date } : undefined, | |||||
| ); | |||||
| setRecords(data); | |||||
| } catch (err) { | |||||
| console.error("Failed to load finished good carton dashboard data", err); | |||||
| setError("載入成品出倉出箱數量失敗,請稍後再試。"); | |||||
| setRecords([]); | |||||
| } finally { | |||||
| setLoading(false); | |||||
| } | |||||
| }, [date]); | |||||
| useEffect(() => { | |||||
| loadData(); | |||||
| }, [loadData]); | |||||
| const rows = useMemo<DailySummaryRow[]>(() => { | |||||
| const filtered = | |||||
| floor === "all" ? records : records.filter((record) => record.storeId === floor); | |||||
| const summary = new Map<string, DailySummaryRow>(); | |||||
| filtered.forEach((record) => { | |||||
| const day = dayjs(record.deliveryDate).isValid() | |||||
| ? dayjs(record.deliveryDate).format("YYYY-MM-DD") | |||||
| : "-"; | |||||
| const cartonQty = Number(record.numberOfCartons ?? 0); | |||||
| const current = summary.get(day) ?? { | |||||
| date: day, | |||||
| floor2F: 0, | |||||
| floor4F: 0, | |||||
| truckX: 0, | |||||
| total: 0, | |||||
| }; | |||||
| if (record.storeId === "2/F") { | |||||
| current.floor2F += cartonQty; | |||||
| } | |||||
| if (record.storeId === "4/F") { | |||||
| current.floor4F += cartonQty; | |||||
| } | |||||
| if (String(record.truckLanceCode ?? "").trim() === "車線-X") { | |||||
| current.truckX += cartonQty; | |||||
| } | |||||
| current.total += cartonQty; | |||||
| summary.set(day, current); | |||||
| }); | |||||
| return Array.from(summary.values()).sort((a, b) => b.date.localeCompare(a.date)); | |||||
| }, [records, floor]); | |||||
| const chartOptions = useMemo<ApexOptions>( | |||||
| () => ({ | |||||
| chart: { | |||||
| type: "bar", | |||||
| toolbar: { show: false }, | |||||
| }, | |||||
| colors: ["#1976d2", "#9c27b0", "#ff9800", "#2e7d32"], | |||||
| dataLabels: { enabled: false }, | |||||
| stroke: { show: true, width: 1, colors: ["transparent"] }, | |||||
| plotOptions: { | |||||
| bar: { | |||||
| horizontal: false, | |||||
| borderRadius: 3, | |||||
| columnWidth: "55%", | |||||
| }, | |||||
| }, | |||||
| xaxis: { | |||||
| categories: rows.map((row) => row.date), | |||||
| title: { text: "日期" }, | |||||
| }, | |||||
| yaxis: { | |||||
| title: { text: "箱數" }, | |||||
| labels: { | |||||
| formatter: (val) => Number(val || 0).toLocaleString("zh-HK"), | |||||
| }, | |||||
| }, | |||||
| tooltip: { | |||||
| y: { | |||||
| formatter: (val) => `${Number(val || 0).toLocaleString("zh-HK")} 箱`, | |||||
| }, | |||||
| }, | |||||
| legend: { | |||||
| position: "top", | |||||
| }, | |||||
| noData: { | |||||
| text: "沒有圖表資料", | |||||
| }, | |||||
| }), | |||||
| [rows], | |||||
| ); | |||||
| const chartSeries = useMemo( | |||||
| () => [ | |||||
| { name: "2/F", data: rows.map((row) => row.floor2F) }, | |||||
| { name: "4/F", data: rows.map((row) => row.floor4F) }, | |||||
| { name: "車線-X", data: rows.map((row) => row.truckX) }, | |||||
| { name: "總數", data: rows.map((row) => row.total) }, | |||||
| ], | |||||
| [rows], | |||||
| ); | |||||
| const summary = useMemo(() => { | |||||
| return rows.reduce( | |||||
| (acc, row) => { | |||||
| acc.floor2F += row.floor2F; | |||||
| acc.floor4F += row.floor4F; | |||||
| acc.truckX += row.truckX; | |||||
| acc.total += row.total; | |||||
| return acc; | |||||
| }, | |||||
| { floor2F: 0, floor4F: 0, truckX: 0, total: 0 }, | |||||
| ); | |||||
| }, [rows]); | |||||
| return ( | |||||
| <Box sx={{ width: "100%" }}> | |||||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||||
| 成品出倉出箱數量 | |||||
| </Typography> | |||||
| {error && ( | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | |||||
| {error} | |||||
| </Alert> | |||||
| )} | |||||
| {loading ? ( | |||||
| <Box sx={{ py: 6, display: "flex", justifyContent: "center" }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Stack spacing={2}> | |||||
| <Grid container spacing={1.5}> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| select | |||||
| fullWidth | |||||
| label="樓層" | |||||
| value={floor} | |||||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | |||||
| > | |||||
| <MenuItem value="all">全部</MenuItem> | |||||
| <MenuItem value="2/F">2/F</MenuItem> | |||||
| <MenuItem value="4/F">4/F</MenuItem> | |||||
| </TextField> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="日期" | |||||
| type="date" | |||||
| value={date} | |||||
| InputLabelProps={{ shrink: true }} | |||||
| onChange={(event) => setDate(event.target.value)} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Grid container spacing={1.5} alignItems="stretch"> | |||||
| <Grid item xs={12} md={4}> | |||||
| <TableContainer component={Paper} sx={{ height: "100%" }}> | |||||
| <Table size="small"> | |||||
| <TableBody> | |||||
| <TableRow> | |||||
| <TableCell>2/F 出箱數</TableCell> | |||||
| <TableCell align="right">{summary.floor2F.toLocaleString("zh-HK")}</TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>4/F 出箱數</TableCell> | |||||
| <TableCell align="right">{summary.floor4F.toLocaleString("zh-HK")}</TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>車線-X 出箱數</TableCell> | |||||
| <TableCell align="right">{summary.truckX.toLocaleString("zh-HK")}</TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>總出箱數</TableCell> | |||||
| <TableCell align="right">{summary.total.toLocaleString("zh-HK")}</TableCell> | |||||
| </TableRow> | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={8}> | |||||
| <Paper sx={{ p: 1.5, height: "100%" }}> | |||||
| <SafeApexCharts | |||||
| type="bar" | |||||
| height={240} | |||||
| options={chartOptions} | |||||
| series={chartSeries} | |||||
| chartRevision={`${floor}-${date}-${rows.length}`} | |||||
| /> | |||||
| </Paper> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Stack> | |||||
| )} | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default FinishedGoodCartonDashboardTab; | |||||
| @@ -42,6 +42,7 @@ import { PrinterCombo } from "@/app/api/settings/printer"; | |||||
| import { Autocomplete } from "@mui/material"; | import { Autocomplete } from "@mui/material"; | ||||
| import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable"; | import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable"; | ||||
| import TruckRoutingSummaryTab, { TruckRoutingSummaryFilters } from "./TruckRoutingSummaryTab"; | import TruckRoutingSummaryTab, { TruckRoutingSummaryFilters } from "./TruckRoutingSummaryTab"; | ||||
| import FinishedGoodCartonDashboardTab from "./FinishedGoodCartonDashboardTab"; | |||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { fetchTruckRoutingSummaryPrecheck } from "@/app/(main)/report/truckRoutingSummaryApi"; | import { fetchTruckRoutingSummaryPrecheck } from "@/app/(main)/report/truckRoutingSummaryApi"; | ||||
| @@ -378,7 +379,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo | |||||
| } | } | ||||
| }, [truckRoutingFilters, selectedPrinterForAllDraft, t]); | }, [truckRoutingFilters, selectedPrinterForAllDraft, t]); | ||||
| const isTruckRoutingTab = tabIndex === 5; | |||||
| const isTruckRoutingTab = tabIndex === 6; | |||||
| const canPrintTruckRoutingSummary = Boolean( | const canPrintTruckRoutingSummary = Boolean( | ||||
| truckRoutingFilters.storeId && | truckRoutingFilters.storeId && | ||||
| truckRoutingFilters.truckLanceCode && | truckRoutingFilters.truckLanceCode && | ||||
| @@ -393,7 +394,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo | |||||
| }, [fetchReleasedOrderCount]); | }, [fetchReleasedOrderCount]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (tabIndex === 5) { | |||||
| if (tabIndex === 6) { | |||||
| logFeatureUsage(FEATURE_USAGE.TRUCK_ROUTING_SUMMARY, FEATURE_USAGE_ACTION.PAGE_VIEW); | logFeatureUsage(FEATURE_USAGE.TRUCK_ROUTING_SUMMARY, FEATURE_USAGE_ACTION.PAGE_VIEW); | ||||
| } | } | ||||
| }, [tabIndex]); | }, [tabIndex]); | ||||
| @@ -831,6 +832,7 @@ const handleAssignByLane = useCallback(async ( | |||||
| <Tab label={t("Finished Good Record")} iconPosition="end" /> | <Tab label={t("Finished Good Record")} iconPosition="end" /> | ||||
| <Tab label={t("Ticket Release Table")} iconPosition="end" /> | <Tab label={t("Ticket Release Table")} iconPosition="end" /> | ||||
| <Tab label={t("Finished Good Record (All)")} iconPosition="end" /> | <Tab label={t("Finished Good Record (All)")} iconPosition="end" /> | ||||
| <Tab label="成品出倉出箱數量" iconPosition="end" /> | |||||
| <Tab label="送貨路線摘要" iconPosition="end" /> | <Tab label="送貨路線摘要" iconPosition="end" /> | ||||
| </Tabs> | </Tabs> | ||||
| @@ -887,6 +889,9 @@ const handleAssignByLane = useCallback(async ( | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 5 && ( | {tabIndex === 5 && ( | ||||
| <FinishedGoodCartonDashboardTab /> | |||||
| )} | |||||
| {tabIndex === 6 && ( | |||||
| <TruckRoutingSummaryTab onFiltersChange={setTruckRoutingFilters} /> | <TruckRoutingSummaryTab onFiltersChange={setTruckRoutingFilters} /> | ||||
| )} | )} | ||||
| </Box> | </Box> | ||||
| @@ -32,8 +32,8 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { | import { | ||||
| fetchCompletedJobOrderPickOrdersrecords, | |||||
| fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick, | |||||
| fetchJobOrderPickOrdersrecords, | |||||
| fetchJobOrderPickOrderLotDetailsForPick, | |||||
| PrintPickRecord | PrintPickRecord | ||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| @@ -148,8 +148,8 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| // 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的) | // 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的) | ||||
| const fetchCompletedJobOrderPickOrdersData = useCallback( | |||||
| async (forDate?: string) => { | |||||
| const fetchJobOrderPickOrdersData = useCallback( | |||||
| async (forDate?: string, forStatus?: string) => { | |||||
| if (!currentUserId) return; | if (!currentUserId) return; | ||||
| setCompletedJobOrderPickOrdersLoading(true); | setCompletedJobOrderPickOrdersLoading(true); | ||||
| @@ -160,21 +160,27 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| : searchQuery.completedDate | : searchQuery.completedDate | ||||
| ? String(searchQuery.completedDate) | ? String(searchQuery.completedDate) | ||||
| : dayjs().format("YYYY-MM-DD"); | : dayjs().format("YYYY-MM-DD"); | ||||
| const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords( | |||||
| dateParam.trim() ? dateParam.trim() : null, | |||||
| const statusParam = | |||||
| forStatus !== undefined | |||||
| ? forStatus | |||||
| : searchQuery.pickOrderStatus | |||||
| ? String(searchQuery.pickOrderStatus) | |||||
| : null; | |||||
| const data = await fetchJobOrderPickOrdersrecords( | |||||
| dateParam?.trim() ? dateParam.trim() : null, | |||||
| statusParam?.trim() ? statusParam.trim() : null, | |||||
| ); | ); | ||||
| const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | |||||
| const safeData = Array.isArray(data) ? data : []; | |||||
| setCompletedJobOrderPickOrders(safeData); | setCompletedJobOrderPickOrders(safeData); | ||||
| setFilteredJobOrderPickOrders(safeData); | setFilteredJobOrderPickOrders(safeData); | ||||
| } catch (error) { | |||||
| console.error("❌ Error fetching completed Job Order pick orders:", error); | |||||
| setCompletedJobOrderPickOrders([]); | |||||
| setFilteredJobOrderPickOrders([]); | |||||
| } finally { | } finally { | ||||
| setCompletedJobOrderPickOrdersLoading(false); | setCompletedJobOrderPickOrdersLoading(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [currentUserId, searchQuery.completedDate], | |||||
| [currentUserId, searchQuery.completedDate, searchQuery.pickOrderStatus], | |||||
| ); | ); | ||||
| // 新增:获取 lot 详情数据(使用新的API) | // 新增:获取 lot 详情数据(使用新的API) | ||||
| const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { | const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { | ||||
| @@ -182,7 +188,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| try { | try { | ||||
| console.log("🔍 Fetching lot details for completed pick order:", pickOrderId); | console.log("🔍 Fetching lot details for completed pick order:", pickOrderId); | ||||
| const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId); | |||||
| const lotDetails = await fetchJobOrderPickOrderLotDetailsForPick(pickOrderId); | |||||
| setDetailLotData(Array.isArray(lotDetails) ? lotDetails : []); | setDetailLotData(Array.isArray(lotDetails) ? lotDetails : []); | ||||
| console.log(" Fetched lot details:", lotDetails); | console.log(" Fetched lot details:", lotDetails); | ||||
| @@ -198,9 +204,13 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!currentUserId) return; | if (!currentUserId) return; | ||||
| const d = searchQuery?.completedDate; | const d = searchQuery?.completedDate; | ||||
| const s = searchQuery?.pickOrderStatus; | |||||
| const dateStr = d != null && String(d).trim() !== "" ? String(d).trim() : ""; | const dateStr = d != null && String(d).trim() !== "" ? String(d).trim() : ""; | ||||
| void fetchCompletedJobOrderPickOrdersData(dateStr || undefined); | |||||
| }, [currentUserId, searchQuery?.completedDate, fetchCompletedJobOrderPickOrdersData]); | |||||
| const statusStr = s != null && String(s).trim() !== "" ? String(s).trim() : ""; | |||||
| void fetchJobOrderPickOrdersData(dateStr || undefined, statusStr || undefined); | |||||
| }, [currentUserId, searchQuery?.completedDate, searchQuery?.pickOrderStatus, fetchJobOrderPickOrdersData]); | |||||
| // 修改:搜索功能(只更新 query;实际过滤交给 useEffect + date filter 统一处理) | // 修改:搜索功能(只更新 query;实际过滤交给 useEffect + date filter 统一处理) | ||||
| const handleSearch = useCallback((query: Record<string, any>) => { | const handleSearch = useCallback((query: Record<string, any>) => { | ||||
| @@ -316,6 +326,15 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| paramName: "jobOrderCode", | paramName: "jobOrderCode", | ||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | |||||
| label: t("Pick Order Status"), | |||||
| paramName: "pickOrderStatus", | |||||
| type: "select-labelled", | |||||
| options: [ | |||||
| { label: t("Released"), value: "RELEASED" }, | |||||
| { label: t("Completed"), value: "COMPLETED" }, | |||||
| ], // 依你后端实际枚举 | |||||
| }, | |||||
| { | { | ||||
| label: t("Job Order Item Name"), | label: t("Job Order Item Name"), | ||||
| paramName: "jobOrderName", | paramName: "jobOrderName", | ||||
| @@ -359,7 +378,10 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| })); | })); | ||||
| }, []); | }, []); | ||||
| const handlePickRecord = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { | |||||
| const handlePickRecord = useCallback(async ( | |||||
| jobOrderPickOrder: CompletedJobOrderPickOrder, | |||||
| floor: "2F" | "3F" | "4F" | "ALL" | |||||
| ) => { | |||||
| try { | try { | ||||
| if (!jobOrderPickOrder) { | if (!jobOrderPickOrder) { | ||||
| console.error("No selected job order pick order available"); | console.error("No selected job order pick order available"); | ||||
| @@ -399,7 +421,8 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| const printRequest = { | const printRequest = { | ||||
| pickOrderId: pickOrderId, | pickOrderId: pickOrderId, | ||||
| printerId: printerId, | printerId: printerId, | ||||
| printQty: printQty | |||||
| printQty: printQty, | |||||
| floor, | |||||
| }; | }; | ||||
| console.log("Printing Pick Record with request: ", printRequest); | console.log("Printing Pick Record with request: ", printRequest); | ||||
| @@ -640,7 +663,9 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| </Box> | </Box> | ||||
| ) : ( | ) : ( | ||||
| <Stack spacing={2}> | <Stack spacing={2}> | ||||
| {paginatedData.map((jobOrderPickOrder) => ( | |||||
| {paginatedData.map((jobOrderPickOrder) => { | |||||
| const normalizedStatus = String(jobOrderPickOrder.pickOrderStatus ?? "").toLowerCase(); | |||||
| return ( | |||||
| <Card key={jobOrderPickOrder.id}> | <Card key={jobOrderPickOrder.id}> | ||||
| <CardContent> | <CardContent> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center"> | <Stack direction="row" justifyContent="space-between" alignItems="center"> | ||||
| @@ -660,21 +685,18 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| </Box> | </Box> | ||||
| <Box> | <Box> | ||||
| <Chip | |||||
| label={t(jobOrderPickOrder.pickOrderStatus) } | |||||
| color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | |||||
| size="small" | |||||
| sx={{ mb: 1 }} | |||||
| /> | |||||
| <Chip | |||||
| label={t(jobOrderPickOrder.pickOrderStatus)} | |||||
| color={normalizedStatus === "completed" ? "success" : "default"} | |||||
| size="small" | |||||
| sx={{ mb: 1 }} | |||||
| /> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} | {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} | ||||
| </Typography> | </Typography> | ||||
| <Chip | |||||
| label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")} | |||||
| color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'} | |||||
| size="small" | |||||
| sx={{ mt: 1 }} | |||||
| /> | |||||
| </Box> | </Box> | ||||
| </Stack> | </Stack> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -685,16 +707,39 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| > | > | ||||
| {t("View Details")} | {t("View Details")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| variant="contained" | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={() => handlePickRecord(jobOrderPickOrder, "ALL")} | |||||
| > | |||||
| 打印全部樓層板頭紙 | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={() => handlePickRecord(jobOrderPickOrder, "2F")} | |||||
| > | |||||
| {t("Print Pick Record")} 2F | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={() => handlePickRecord(jobOrderPickOrder, "3F")} | |||||
| > | |||||
| {t("Print Pick Record")} 3F | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | color="primary" | ||||
| onClick={() => handlePickRecord(jobOrderPickOrder)} | |||||
| onClick={() => handlePickRecord(jobOrderPickOrder, "4F")} | |||||
| > | > | ||||
| {t("Print Pick Record")} | |||||
| {t("Print Pick Record")} 4F | |||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| </Card> | </Card> | ||||
| ))} | |||||
| ); | |||||
| })} | |||||
| </Stack> | </Stack> | ||||
| )} | )} | ||||
| @@ -167,6 +167,9 @@ | |||||
| "View Details": "查看詳情", | "View Details": "查看詳情", | ||||
| "Skip": "跳過", | "Skip": "跳過", | ||||
| "Handler": "提料員", | "Handler": "提料員", | ||||
| "RELEASED": "已放單", | |||||
| "Released": "已放單", | |||||
| "COMPLETED": "已完成", | |||||
| "Now": "現時", | "Now": "現時", | ||||
| "Last updated": "最後更新", | "Last updated": "最後更新", | ||||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | ||||