diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index f7eb995..21f5506 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -1,7 +1,7 @@ "use server"; import { cache } from 'react'; -import { Pageable, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { Pageable, ServerFetchError, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { JobOrder, JoStatus, Machine, Operator } from "."; import { BASE_API_URL } from "@/config/api"; import { revalidateTag } from "next/cache"; @@ -134,6 +134,10 @@ export interface PrintPickRecordRequest{ printerId: number; printQty: number; floor?: "2F" | "3F" | "4F" | "ALL"; + plasticBoxCartonQty?: number; + plasticBoxCartonQty2f?: number; + plasticBoxCartonQty3f?: number; + plasticBoxCartonQty4f?: number; } export interface PrintPickRecordResponse{ @@ -141,6 +145,23 @@ export interface PrintPickRecordResponse{ message?: string } +export interface PickRecordPlasticBoxCartonQtyResponse { + plasticBoxCartonQty2f: number | null; + plasticBoxCartonQty3f: number | null; + plasticBoxCartonQty4f: number | null; +} + +export const fetchPickRecordPlasticBoxCartonQty = async ( + pickOrderId: number, +): Promise => { + return serverFetchJson( + `${BASE_API_URL}/jo/pick-record-plastic-box-carton-qty/${pickOrderId}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); +}; export interface PrintFGStockInLabelRequest { stockInLineId: number; @@ -1131,15 +1152,50 @@ export const fetchCompletedJobOrderPickOrdersrecords = cache(async () => { ); }); */ -export const fetchCompletedJobOrderPickOrdersrecords = async (completedDate?: string | null) => { +export interface CompletedJobOrderPickOrderDashboardRecord { + id?: number; + pickOrderId?: number; + pickOrderCode?: string; + /** YYYY-MM-DD from backend (preferred for grouping) */ + statDate?: string | null; + completedDate?: string | null; + planStart?: string | null; + plasticBoxCartonQty2f?: number | null; + plasticBoxCartonQty3f?: number | null; + plasticBoxCartonQty4f?: number | null; +} + +export const fetchPlasticBoxCartonQtyDashboard = async ( + from: string, + to: string, +): Promise => { + const params = new URLSearchParams({ + from: from.trim(), + to: to.trim(), + }); + return serverFetchJson( + `${BASE_API_URL}/jo/plastic-box-carton-qty-dashboard?${params.toString()}`, + { + method: "GET", + cache: "no-store", + }, + ); +}; + +export const fetchCompletedJobOrderPickOrdersrecords = async ( + completedDate?: string | null, +): Promise => { const q = completedDate && String(completedDate).trim() !== "" ? `?date=${encodeURIComponent(String(completedDate).trim())}` : ""; - return serverFetchJson(`${BASE_API_URL}/jo/completed-job-order-pick-orders-only${q}`, { - method: "GET", - cache: "no-store", - }); + return serverFetchJson( + `${BASE_API_URL}/jo/completed-job-order-pick-orders-only${q}`, + { + method: "GET", + cache: "no-store", + }, + ); }; export const fetchJobOrderPickOrdersrecords = async ( date?: string | null, @@ -1368,13 +1424,37 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ if (request.floor) { params.append('floor', request.floor); } + if (request.plasticBoxCartonQty !== null && request.plasticBoxCartonQty !== undefined) { + params.append('plasticBoxCartonQty', request.plasticBoxCartonQty.toString()); + } + if (request.plasticBoxCartonQty2f !== null && request.plasticBoxCartonQty2f !== undefined) { + params.append('plasticBoxCartonQty2f', request.plasticBoxCartonQty2f.toString()); + } + if (request.plasticBoxCartonQty3f !== null && request.plasticBoxCartonQty3f !== undefined) { + params.append('plasticBoxCartonQty3f', request.plasticBoxCartonQty3f.toString()); + } + if (request.plasticBoxCartonQty4f !== null && request.plasticBoxCartonQty4f !== undefined) { + params.append('plasticBoxCartonQty4f', request.plasticBoxCartonQty4f.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()}`,{ - method: "GET" - }); - - return { success: true, message: "Print job sent successfully (Pick Record)" } as PrintPickRecordResponse; + try { + await serverFetchWithNoContent( + `${BASE_API_URL}/jo/print-PickRecord?${params.toString()}`, + { method: "GET" }, + ); + return { + success: true, + message: "Print job sent successfully (Pick Record)", + } as PrintPickRecordResponse; + } catch (error) { + const message = + error instanceof ServerFetchError + ? error.message + : error instanceof Error + ? error.message + : "Print failed"; + return { success: false, message } as PrintPickRecordResponse; + } } export interface ExportFGStockInLabelRequest { diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx index 8d594da..a9e8ba3 100644 --- a/src/components/JoWorkbench/newJobPickExecution.tsx +++ b/src/components/JoWorkbench/newJobPickExecution.tsx @@ -78,6 +78,12 @@ import { } from "@/app/api/doworkbench/actions"; import type { PrinterCombo } from "@/app/api/settings/printer"; import { msg, msgError } from "@/components/Swal/CustomAlerts"; +import { + buildPrintPickRecordRequest, + promptAllFloorsPlasticBoxCartonQty, + promptPlasticBoxCartonQty, + type PickRecordFloor, +} from "@/components/Jodetail/pickRecordHelpers"; interface Props { filterArgs: Record; //onSwitchToRecordTab: () => void; @@ -664,6 +670,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCo printerOptions.length > 0 ? printerOptions[0] : null, ); const [printQty, setPrintQty] = useState(1); + const pickRecordPrintInFlightRef = useRef(false); useEffect(() => { // Keep selected printer valid when combo list changes. @@ -685,7 +692,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCo }, [printerCombo, a4Printers, printerOptions]); const handlePickRecord = useCallback( - async (floor: "2F" | "3F" | "4F" | "ALL") => { + async (floor: PickRecordFloor) => { + if (pickRecordPrintInFlightRef.current) return; try { const pickOrderId = jobOrderData?.pickOrder?.id; if (!pickOrderId) { @@ -701,12 +709,36 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCo return; } - const response = await PrintPickRecord({ - pickOrderId, - printerId: selectedPrinter.id, - printQty, - floor, - }); + let printRequest; + if (floor === "ALL") { + const allFloorsQty = await promptAllFloorsPlasticBoxCartonQty(t, pickOrderId); + if (allFloorsQty === null) { + return; + } + printRequest = buildPrintPickRecordRequest({ + pickOrderId, + printerId: selectedPrinter.id, + printQty, + floor, + allFloorsQty, + }); + } else { + const plasticBoxCartonQty = await promptPlasticBoxCartonQty(t); + if (plasticBoxCartonQty === null) { + return; + } + printRequest = buildPrintPickRecordRequest({ + pickOrderId, + printerId: selectedPrinter.id, + printQty, + floor, + plasticBoxCartonQty, + }); + } + + pickRecordPrintInFlightRef.current = true; + + const response = await PrintPickRecord(printRequest); if (response?.success) { msg(t("Printed Successfully.")); @@ -716,6 +748,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCo } catch (e) { console.error(e); msgError(t("An error occurred while printing")); + } finally { + pickRecordPrintInFlightRef.current = false; } }, [jobOrderData, printQty, selectedPrinter, t], diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index be37cf9..efec1a3 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -39,6 +39,7 @@ import { fetchPrinterCombo } from "@/app/api/settings/printer"; import { PrinterCombo } from "@/app/api/settings/printer"; import JoPickOrderDetail from "./JoPickOrderDetail"; import MaterialPickStatusTable from "./MaterialPickStatusTable"; +import PlasticBoxCartonDashboardTab from "./PlasticBoxCartonDashboardTab"; interface Props { //pickOrders: PickOrderResult[]; printerCombo: PrinterCombo[]; @@ -245,26 +246,6 @@ const JodetailSearch: React.FC = ({ printerCombo }) => { setIsOpenCreateModal(false) }, []) - - useEffect(() => { - - if (tabIndex === 3) { - const loadItems = async () => { - try { - const itemsData = await fetchAllItemsInClient(); - console.log("PickOrderSearch loaded items:", itemsData.length); - setItems(itemsData); - } catch (error) { - console.error("Error loading items in PickOrderSearch:", error); - } - }; - - // 如果还没有数据,则加载 - if (items.length === 0) { - loadItems(); - } - } - }, [tabIndex, items.length]); useEffect(() => { const handleCompletionStatusChange = (event: CustomEvent) => { const { allLotsCompleted } = event.detail; @@ -499,6 +480,7 @@ const JodetailSearch: React.FC = ({ printerCombo }) => { + @@ -514,6 +496,7 @@ const JodetailSearch: React.FC = ({ printerCombo }) => { /> )} {tabIndex === 2 && } + {tabIndex === 3 && } ); diff --git a/src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx b/src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx new file mode 100644 index 0000000..e9d9f16 --- /dev/null +++ b/src/components/Jodetail/PlasticBoxCartonDashboardTab.tsx @@ -0,0 +1,537 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Alert, + Box, + Button, + CircularProgress, + Grid, + MenuItem, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import type { ApexOptions } from "apexcharts"; +import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; +import * as XLSX from "xlsx-js-style"; +import { + CompletedJobOrderPickOrderDashboardRecord, + fetchPlasticBoxCartonQtyDashboard, +} from "@/app/api/jo/actions"; +import SafeApexCharts from "@/components/charts/SafeApexCharts"; + +type FloorFilter = "all" | "2/F" | "3/F" | "4/F"; + +type DailySummaryRow = { + date: string; + floor2F: number; + floor3F: number; + floor4F: number; + total: number; +}; + +const hasPlasticQty = (record: CompletedJobOrderPickOrderDashboardRecord): boolean => + record.plasticBoxCartonQty2f != null || + record.plasticBoxCartonQty3f != null || + record.plasticBoxCartonQty4f != null; + +/** Backend may return LocalDateTime as [y,m,d,h,mi,s] — align with completeJobOrderRecord.toDateYMD */ +const parseRecordDate = (record: CompletedJobOrderPickOrderDashboardRecord): dayjs.Dayjs => { + if (record.statDate) { + const d = dayjs(record.statDate, "YYYY-MM-DD", true); + if (d.isValid()) return d; + } + + const raw = record.completedDate ?? record.planStart; + if (!raw) return dayjs.invalid(); + + if (Array.isArray(raw)) { + const [year, month, day] = raw; + if (year != null && month != null && day != null) { + return dayjs(new Date(Number(year), Number(month) - 1, Number(day))); + } + return dayjs.invalid(); + } + + if (typeof raw === "string") { + const strict = dayjs(raw, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ss", "YYYY-MM-DDTHH:mm:ss.SSS"], true); + return strict.isValid() ? strict : dayjs(raw); + } + + return dayjs(raw); +}; + +const addRecordQty = ( + current: DailySummaryRow, + record: CompletedJobOrderPickOrderDashboardRecord, + selectedFloor: FloorFilter, +) => { + const q2 = Number(record.plasticBoxCartonQty2f ?? 0); + const q3 = Number(record.plasticBoxCartonQty3f ?? 0); + const q4 = Number(record.plasticBoxCartonQty4f ?? 0); + + if (selectedFloor === "all" || selectedFloor === "2/F") { + current.floor2F += q2; + } + if (selectedFloor === "all" || selectedFloor === "3/F") { + current.floor3F += q3; + } + if (selectedFloor === "all" || selectedFloor === "4/F") { + current.floor4F += q4; + } + + if (selectedFloor === "all") { + current.total += q2 + q3 + q4; + } else if (selectedFloor === "2/F") { + current.total += q2; + } else if (selectedFloor === "3/F") { + current.total += q3; + } else { + current.total += q4; + } +}; + +const PlasticBoxCartonDashboardTab: React.FC = () => { + const { t } = useTranslation("jo"); + const exportInFlightRef = useRef(false); + const [floor, setFloor] = useState("all"); + const [date, setDate] = useState(dayjs().format("YYYY-MM-DD")); + const [loading, setLoading] = useState(false); + const [isExporting, setIsExporting] = useState(false); + const [error, setError] = useState(""); + const [records, setRecords] = useState([]); + + const loadData = useCallback(async () => { + setLoading(true); + setError(""); + try { + const day = date && dayjs(date).isValid() ? dayjs(date).format("YYYY-MM-DD") : dayjs().format("YYYY-MM-DD"); + const data = await fetchPlasticBoxCartonQtyDashboard(day, day); + setRecords(data); + } catch (err) { + console.error("Failed to load plastic box carton dashboard data", err); + setError(t("Failed to load plastic box carton qty dashboard")); + setRecords([]); + } finally { + setLoading(false); + } + }, [date, t]); + + useEffect(() => { + loadData(); + }, [loadData]); + + const rows = useMemo(() => { + const summary = new Map(); + + records.forEach((record) => { + const dayObj = parseRecordDate(record); + const day = dayObj.isValid() ? dayObj.format("YYYY-MM-DD") : "-"; + + const current = summary.get(day) ?? { + date: day, + floor2F: 0, + floor3F: 0, + floor4F: 0, + total: 0, + }; + + addRecordQty(current, record, floor); + 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: t("Date") }, + }, + yaxis: { + title: { text: t("Plastic box carton Qty") }, + labels: { + formatter: (val) => Number(val || 0).toLocaleString("zh-HK"), + }, + }, + tooltip: { + y: { + formatter: (val) => + `${Number(val || 0).toLocaleString("zh-HK")} ${t("Plastic box carton Qty")}`, + }, + }, + legend: { + position: "top", + }, + noData: { + text: t("No chart data"), + }, + }), + [rows, t], + ); + + const chartSeries = useMemo( + () => [ + { name: "2/F", data: rows.map((row) => row.floor2F) }, + { name: "3/F", data: rows.map((row) => row.floor3F) }, + { name: "4/F", data: rows.map((row) => row.floor4F) }, + { name: t("Total plastic box carton qty"), data: rows.map((row) => row.total) }, + ], + [rows, t], + ); + + const summary = useMemo(() => { + return rows.reduce( + (acc, row) => { + acc.floor2F += row.floor2F; + acc.floor3F += row.floor3F; + acc.floor4F += row.floor4F; + acc.total += row.total; + return acc; + }, + { floor2F: 0, floor3F: 0, floor4F: 0, total: 0 }, + ); + }, [rows]); + + const buildDailyRowsFromRecords = useCallback( + ( + sourceRecords: CompletedJobOrderPickOrderDashboardRecord[], + startDate: dayjs.Dayjs, + endDate: dayjs.Dayjs, + selectedFloor: FloorFilter, + ): DailySummaryRow[] => { + const summaryMap = new Map(); + const start = startDate.startOf("day"); + const end = endDate.endOf("day"); + + sourceRecords.filter(hasPlasticQty).forEach((record) => { + const recordDay = parseRecordDate(record); + if (!recordDay.isValid() || recordDay.isBefore(start) || recordDay.isAfter(end)) { + return; + } + + const dayKey = recordDay.format("YYYY-MM-DD"); + const current = summaryMap.get(dayKey) ?? { + date: dayKey, + floor2F: 0, + floor3F: 0, + floor4F: 0, + total: 0, + }; + + addRecordQty(current, record, selectedFloor); + summaryMap.set(dayKey, current); + }); + + return Array.from(summaryMap.values()).sort((a, b) => a.date.localeCompare(b.date)); + }, + [], + ); + + const calcSummary = useCallback((dailyRows: DailySummaryRow[]) => { + return dailyRows.reduce( + (acc, row) => { + acc.floor2F += row.floor2F; + acc.floor3F += row.floor3F; + acc.floor4F += row.floor4F; + acc.total += row.total; + return acc; + }, + { floor2F: 0, floor3F: 0, floor4F: 0, total: 0 }, + ); + }, []); + + const styleWorksheet = useCallback((worksheet: XLSX.WorkSheet, dataRowsCount: number) => { + const summaryTitleRow = 4 + dataRowsCount; + const summaryStartRow = 5 + dataRowsCount; + + worksheet["!cols"] = [{ wch: 16 }, { wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 14 }]; + worksheet["!merges"] = [ + { s: { r: 0, c: 0 }, e: { r: 0, c: 4 } }, + { s: { r: summaryTitleRow, c: 0 }, e: { r: summaryTitleRow, c: 4 } }, + ]; + + const titleStyle = { + font: { bold: true, sz: 14, color: { rgb: "1F2D3D" } }, + alignment: { horizontal: "center", vertical: "center" }, + fill: { fgColor: { rgb: "EAF3FF" } }, + }; + const headerStyle = { + font: { bold: true, color: { rgb: "FFFFFF" } }, + fill: { fgColor: { rgb: "1976D2" } }, + alignment: { horizontal: "center", vertical: "center" }, + border: { + top: { style: "thin", color: { rgb: "B0BEC5" } }, + bottom: { style: "thin", color: { rgb: "B0BEC5" } }, + left: { style: "thin", color: { rgb: "B0BEC5" } }, + right: { style: "thin", color: { rgb: "B0BEC5" } }, + }, + }; + const cellStyle = { + alignment: { vertical: "center" }, + border: { + top: { style: "thin", color: { rgb: "D0D7DE" } }, + bottom: { style: "thin", color: { rgb: "D0D7DE" } }, + left: { style: "thin", color: { rgb: "D0D7DE" } }, + right: { style: "thin", color: { rgb: "D0D7DE" } }, + }, + }; + const numberStyle = { + ...cellStyle, + alignment: { horizontal: "right", vertical: "center" }, + numFmt: "#,##0", + }; + const summaryTitleStyle = { + font: { bold: true, color: { rgb: "1F2D3D" } }, + fill: { fgColor: { rgb: "F1F8E9" } }, + alignment: { horizontal: "left", vertical: "center" }, + }; + + for (let c = 0; c <= 4; c += 1) { + const headerCell = XLSX.utils.encode_cell({ r: 2, c }); + if (worksheet[headerCell]) worksheet[headerCell].s = headerStyle; + } + + for (let r = 3; r < 3 + dataRowsCount; r += 1) { + for (let c = 0; c <= 4; c += 1) { + const addr = XLSX.utils.encode_cell({ r, c }); + if (!worksheet[addr]) continue; + worksheet[addr].s = c === 0 ? cellStyle : numberStyle; + } + } + + for (let r = summaryStartRow; r <= summaryStartRow + 3; r += 1) { + const labelAddr = XLSX.utils.encode_cell({ r, c: 0 }); + const valueAddr = XLSX.utils.encode_cell({ r, c: 1 }); + if (worksheet[labelAddr]) worksheet[labelAddr].s = cellStyle; + if (worksheet[valueAddr]) worksheet[valueAddr].s = numberStyle; + } + + if (worksheet["A1"]) worksheet["A1"].s = titleStyle; + const summaryTitleAddr = XLSX.utils.encode_cell({ r: summaryTitleRow, c: 0 }); + if (worksheet[summaryTitleAddr]) worksheet[summaryTitleAddr].s = summaryTitleStyle; + }, []); + + const addReportSheet = useCallback( + ( + workbook: XLSX.WorkBook, + sheetName: string, + reportTitle: string, + dailyRows: DailySummaryRow[], + ) => { + const reportSummary = calcSummary(dailyRows); + const aoa: (string | number)[][] = [ + [reportTitle, "", "", "", ""], + ["", "", "", "", ""], + [ + t("Date"), + t("2/F plastic box carton qty count"), + t("3/F plastic box carton qty count"), + t("4/F plastic box carton qty count"), + t("Total plastic box carton qty"), + ], + ...dailyRows.map((row) => [row.date, row.floor2F, row.floor3F, row.floor4F, row.total]), + ["", "", "", "", ""], + [t("Summary"), "", "", "", ""], + [t("2/F plastic box carton qty count"), reportSummary.floor2F, "", "", ""], + [t("3/F plastic box carton qty count"), reportSummary.floor3F, "", "", ""], + [t("4/F plastic box carton qty count"), reportSummary.floor4F, "", "", ""], + [t("Total plastic box carton qty"), reportSummary.total, "", "", ""], + ]; + + const worksheet = XLSX.utils.aoa_to_sheet(aoa); + styleWorksheet(worksheet, dailyRows.length); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }, + [calcSummary, styleWorksheet, t], + ); + + const handleDownloadExcel = useCallback(async () => { + if (exportInFlightRef.current) return; + exportInFlightRef.current = true; + setIsExporting(true); + try { + const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); + const rangeFrom = baseDate.startOf("year").format("YYYY-MM-DD"); + const rangeTo = baseDate.endOf("year").format("YYYY-MM-DD"); + const allRecords = await fetchPlasticBoxCartonQtyDashboard(rangeFrom, rangeTo); + const floorLabel = + floor === "all" ? t("All floors") : floor; + const dateLabel = baseDate.format("YYYY-MM-DD"); + + const last7Rows = buildDailyRowsFromRecords( + allRecords, + baseDate.subtract(6, "day"), + baseDate, + floor, + ); + const monthRows = buildDailyRowsFromRecords( + allRecords, + baseDate.startOf("month"), + baseDate.endOf("month"), + floor, + ); + const yearRows = buildDailyRowsFromRecords( + allRecords, + baseDate.startOf("year"), + baseDate.endOf("year"), + floor, + ); + + const workbook = XLSX.utils.book_new(); + addReportSheet( + workbook, + t("Last 7 days"), + `${t("Plastic box carton qty report last 7 days")} - ${floorLabel} - ${dateLabel}`, + last7Rows, + ); + addReportSheet( + workbook, + t("This month"), + `${t("Plastic box carton qty report this month")} - ${floorLabel} - ${baseDate.format("YYYY年MM月")}`, + monthRows, + ); + addReportSheet( + workbook, + t("This year"), + `${t("Plastic box carton qty report this year")} - ${floorLabel} - ${baseDate.format("YYYY年")}`, + yearRows, + ); + + XLSX.writeFile( + workbook, + `${t("Plastic box carton qty multi period report")}_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`, + ); + } finally { + setIsExporting(false); + exportInFlightRef.current = false; + } + }, [date, floor, buildDailyRowsFromRecords, addReportSheet, t]); + + return ( + + + {t("Plastic box carton qty dashboard")} + + + + {error && ( + + {error} + + )} + + {loading ? ( + + + + ) : ( + + + + setFloor(event.target.value as FloorFilter)} + > + {t("All")} + 2/F + 3/F + 4/F + + + + setDate(event.target.value)} + /> + + + + + + + + + + {t("2/F plastic box carton qty count")} + + {summary.floor2F.toLocaleString("zh-HK")} + + + + {t("3/F plastic box carton qty count")} + + {summary.floor3F.toLocaleString("zh-HK")} + + + + {t("4/F plastic box carton qty count")} + + {summary.floor4F.toLocaleString("zh-HK")} + + + + {t("Total plastic box carton qty")} + + {summary.total.toLocaleString("zh-HK")} + + + +
+
+
+ + + + + +
+
+ )} +
+ ); +}; + +export default PlasticBoxCartonDashboardTab; diff --git a/src/components/Jodetail/completeJobOrderRecord.tsx b/src/components/Jodetail/completeJobOrderRecord.tsx index 31f521d..77ef206 100644 --- a/src/components/Jodetail/completeJobOrderRecord.tsx +++ b/src/components/Jodetail/completeJobOrderRecord.tsx @@ -46,6 +46,12 @@ import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import Swal from "sweetalert2"; import { PrinterCombo } from "@/app/api/settings/printer"; +import { + buildPrintPickRecordRequest, + promptAllFloorsPlasticBoxCartonQty, + promptPlasticBoxCartonQty, + type PickRecordFloor, +} from "./pickRecordHelpers"; import dayjs from "dayjs"; interface Props { filterArgs: Record; @@ -137,7 +143,8 @@ const CompleteJobOrderRecord: React.FC = ({ // Use props with fallback const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); const printQty = printQtyProp ?? 1; - + const pickRecordPrintInFlightRef = useRef(false); + // 修改:分页状态 const [paginationController, setPaginationController] = useState({ pageNum: 0, @@ -380,15 +387,15 @@ const CompleteJobOrderRecord: React.FC = ({ const handlePickRecord = useCallback(async ( jobOrderPickOrder: CompletedJobOrderPickOrder, - floor: "2F" | "3F" | "4F" | "ALL" + floor: PickRecordFloor, ) => { + if (pickRecordPrintInFlightRef.current) return; try { if (!jobOrderPickOrder) { console.error("No selected job order pick order available"); return; } - // 检查是否已选择打印机 if (!selectedPrinter) { Swal.fire({ position: "bottom-end", @@ -400,7 +407,6 @@ const CompleteJobOrderRecord: React.FC = ({ return; } - // 检查打印数量是否有效 if (!printQty || printQty < 1) { Swal.fire({ position: "bottom-end", @@ -413,24 +419,37 @@ const CompleteJobOrderRecord: React.FC = ({ } const pickOrderId = jobOrderPickOrder.pickOrderId; - console.log("Pick Order ID:", pickOrderId); - - // 使用已选择的打印机和数量 - const printerId = selectedPrinter.id; - - const printRequest = { - pickOrderId: pickOrderId, - printerId: printerId, - printQty: printQty, - floor, - }; + let printRequest; + if (floor === "ALL") { + const allFloorsQty = await promptAllFloorsPlasticBoxCartonQty(t, pickOrderId); + if (allFloorsQty === null) { + return; + } + printRequest = buildPrintPickRecordRequest({ + pickOrderId, + printerId: selectedPrinter.id, + printQty, + floor, + allFloorsQty, + }); + } else { + const plasticBoxCartonQty = await promptPlasticBoxCartonQty(t); + if (plasticBoxCartonQty === null) { + return; + } + printRequest = buildPrintPickRecordRequest({ + pickOrderId, + printerId: selectedPrinter.id, + printQty, + floor, + plasticBoxCartonQty, + }); + } - console.log("Printing Pick Record with request: ", printRequest); + pickRecordPrintInFlightRef.current = true; const response = await PrintPickRecord(printRequest); - console.log("Print Pick Record response: ", response); - if (response.success) { Swal.fire({ position: "bottom-end", @@ -440,7 +459,6 @@ const CompleteJobOrderRecord: React.FC = ({ timer: 1500 }); } else { - console.error("Print failed: ", response.message); Swal.fire({ position: "bottom-end", icon: "error", @@ -458,6 +476,8 @@ const CompleteJobOrderRecord: React.FC = ({ showConfirmButton: false, timer: 1500 }); + } finally { + pickRecordPrintInFlightRef.current = false; } }, [t, selectedPrinter, printQty]); // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 @@ -700,7 +720,7 @@ const CompleteJobOrderRecord: React.FC = ({ - +