From 4fb0be7c9eca463cd329b670a57af412ee639642 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 22 Apr 2026 16:34:57 +0800 Subject: [PATCH 1/3] update job order record --- src/app/api/jo/actions.ts | 44 +++++++++++ .../Jodetail/completeJobOrderRecord.tsx | 78 ++++++++++++------- src/i18n/zh/jo.json | 3 + 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 7d92e77..5890d78 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -604,6 +604,9 @@ export interface StockOutLineDetailResponse { location: string | null; availableQty: number | null; noLot: boolean; + /** Workbench API: matched suggest_pick_lot qty for this SOL lot line */ + //suggestedPickQty?: number | null; + //suggestedPickLotId?: number | null; } export interface LotDetailResponse { @@ -712,6 +715,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( + `${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 // immediately when navigating back from JobPickExecution. export const fetchAllJoPickOrders = async (type?: string | null, floor?: string | null) => { @@ -1076,6 +1094,32 @@ export const fetchCompletedJobOrderPickOrdersrecords = async (completedDate?: st 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(`${BASE_API_URL}/jo/job-order-pick-orders${q}`, { + method: "GET", + cache: "no-store", + }); +}; + +export const fetchJobOrderPickOrderLotDetailsForPick = cache(async (pickOrderId: number) => { + return serverFetchJson(`${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) => { return serverFetchJson( `${BASE_API_URL}/jo/joForPrintQrCode/${date}`, diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index 4b28be7..74aec53 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -32,8 +32,8 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { - fetchCompletedJobOrderPickOrdersrecords, - fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick, + fetchJobOrderPickOrdersrecords, + fetchJobOrderPickOrderLotDetailsForPick, PrintPickRecord } from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; @@ -148,8 +148,8 @@ const CompleteJobOrderRecord: React.FC = ({ const errors = formProps.formState.errors; // 修改:使用新的 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; setCompletedJobOrderPickOrdersLoading(true); @@ -160,21 +160,27 @@ const CompleteJobOrderRecord: React.FC = ({ : searchQuery.completedDate ? String(searchQuery.completedDate) : 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); setFilteredJobOrderPickOrders(safeData); - } catch (error) { - console.error("❌ Error fetching completed Job Order pick orders:", error); - setCompletedJobOrderPickOrders([]); - setFilteredJobOrderPickOrders([]); } finally { setCompletedJobOrderPickOrdersLoading(false); } }, - [currentUserId, searchQuery.completedDate], + [currentUserId, searchQuery.completedDate, searchQuery.pickOrderStatus], ); // 新增:获取 lot 详情数据(使用新的API) const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { @@ -182,7 +188,7 @@ const CompleteJobOrderRecord: React.FC = ({ try { 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 : []); console.log(" Fetched lot details:", lotDetails); @@ -198,9 +204,13 @@ const CompleteJobOrderRecord: React.FC = ({ useEffect(() => { if (!currentUserId) return; const d = searchQuery?.completedDate; + const s = searchQuery?.pickOrderStatus; + 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 统一处理) const handleSearch = useCallback((query: Record) => { @@ -316,6 +326,15 @@ const CompleteJobOrderRecord: React.FC = ({ paramName: "jobOrderCode", 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"), paramName: "jobOrderName", @@ -640,7 +659,9 @@ const CompleteJobOrderRecord: React.FC = ({ ) : ( - {paginatedData.map((jobOrderPickOrder) => ( + {paginatedData.map((jobOrderPickOrder) => { + const normalizedStatus = String(jobOrderPickOrder.pickOrderStatus ?? "").toLowerCase(); + return ( @@ -660,21 +681,18 @@ const CompleteJobOrderRecord: React.FC = ({ - + {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} - + + @@ -694,7 +712,9 @@ const CompleteJobOrderRecord: React.FC = ({ - ))} + ); + })} + )} diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 9defda1..9885ad9 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -167,6 +167,9 @@ "View Details": "查看詳情", "Skip": "跳過", "Handler": "提料員", + "RELEASED": "已放單", + "Released": "已放單", + "COMPLETED": "已完成", "Now": "現時", "Last updated": "最後更新", "Auto-refresh every 5 minutes": "每5分鐘自動刷新", From 510d3fd831862f5f44597cb032d6d91cfe456576 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Wed, 22 Apr 2026 17:03:53 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=88=90=E5=93=81=E5=87=BA=E5=80=89?= =?UTF-8?q?=E5=87=BA=E7=AE=B1=E6=95=B8=E9=87=8F=20Update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinishedGoodCartonDashboardTab.tsx | 255 ++++++++++++++++++ .../FinishedGoodSearch/FinishedGoodSearch.tsx | 9 +- 2 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx diff --git a/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx b/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx new file mode 100644 index 0000000..ce97906 --- /dev/null +++ b/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx @@ -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("all"); + const [date, setDate] = useState(dayjs().format("YYYY-MM-DD")); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [records, setRecords] = useState([]); + + 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(() => { + const filtered = + floor === "all" ? records : records.filter((record) => record.storeId === floor); + + const summary = new Map(); + + 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( + () => ({ + 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 ( + + + 成品出倉出箱數量 + + + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( + + + + setFloor(event.target.value as FloorFilter)} + > + 全部 + 2/F + 4/F + + + + setDate(event.target.value)} + /> + + + + + + + + + + 2/F 出箱數 + {summary.floor2F.toLocaleString("zh-HK")} + + + 4/F 出箱數 + {summary.floor4F.toLocaleString("zh-HK")} + + + 車線-X 出箱數 + {summary.truckX.toLocaleString("zh-HK")} + + + 總出箱數 + {summary.total.toLocaleString("zh-HK")} + + +
+
+
+ + + + + +
+
+ )} +
+ ); +}; + +export default FinishedGoodCartonDashboardTab; diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx index 6c1e291..f2c4292 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -42,6 +42,7 @@ import { PrinterCombo } from "@/app/api/settings/printer"; import { Autocomplete } from "@mui/material"; import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable"; import TruckRoutingSummaryTab, { TruckRoutingSummaryFilters } from "./TruckRoutingSummaryTab"; +import FinishedGoodCartonDashboardTab from "./FinishedGoodCartonDashboardTab"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { fetchTruckRoutingSummaryPrecheck } from "@/app/(main)/report/truckRoutingSummaryApi"; @@ -378,7 +379,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState { - if (tabIndex === 5) { + if (tabIndex === 6) { logFeatureUsage(FEATURE_USAGE.TRUCK_ROUTING_SUMMARY, FEATURE_USAGE_ACTION.PAGE_VIEW); } }, [tabIndex]); @@ -831,6 +832,7 @@ const handleAssignByLane = useCallback(async ( + @@ -887,6 +889,9 @@ const handleAssignByLane = useCallback(async ( /> )} {tabIndex === 5 && ( + + )} + {tabIndex === 6 && ( )} From b9a9deb1d44ae113c10eb3690feb34910d65d4f8 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Wed, 22 Apr 2026 19:02:47 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=B7=A5=E5=96=AE=E6=9D=BF=E9=A0=AD?= =?UTF-8?q?=E7=B4=99=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/jo/actions.ts | 4 ++ .../Jodetail/completeJobOrderRecord.tsx | 37 ++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 5890d78..7b0dc2b 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -133,6 +133,7 @@ export interface PrintPickRecordRequest{ pickOrderId: number; printerId: number; printQty: number; + floor?: "2F" | "3F" | "4F" | "ALL"; } export interface PrintPickRecordResponse{ @@ -1318,6 +1319,9 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ if (request.printQty !== null && request.printQty !== undefined) { 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()}`,{ diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index 74aec53..31f521d 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -378,7 +378,10 @@ const CompleteJobOrderRecord: React.FC = ({ })); }, []); - const handlePickRecord = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { + const handlePickRecord = useCallback(async ( + jobOrderPickOrder: CompletedJobOrderPickOrder, + floor: "2F" | "3F" | "4F" | "ALL" + ) => { try { if (!jobOrderPickOrder) { console.error("No selected job order pick order available"); @@ -418,7 +421,8 @@ const CompleteJobOrderRecord: React.FC = ({ const printRequest = { pickOrderId: pickOrderId, printerId: printerId, - printQty: printQty + printQty: printQty, + floor, }; console.log("Printing Pick Record with request: ", printRequest); @@ -703,12 +707,33 @@ const CompleteJobOrderRecord: React.FC = ({ > {t("View Details")} - + + +