| @@ -1,7 +1,7 @@ | |||||
| "use server"; | "use server"; | ||||
| import { cache } from 'react'; | 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 { JobOrder, JoStatus, Machine, Operator } from "."; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| @@ -134,6 +134,10 @@ export interface PrintPickRecordRequest{ | |||||
| printerId: number; | printerId: number; | ||||
| printQty: number; | printQty: number; | ||||
| floor?: "2F" | "3F" | "4F" | "ALL"; | floor?: "2F" | "3F" | "4F" | "ALL"; | ||||
| plasticBoxCartonQty?: number; | |||||
| plasticBoxCartonQty2f?: number; | |||||
| plasticBoxCartonQty3f?: number; | |||||
| plasticBoxCartonQty4f?: number; | |||||
| } | } | ||||
| export interface PrintPickRecordResponse{ | export interface PrintPickRecordResponse{ | ||||
| @@ -141,6 +145,23 @@ export interface PrintPickRecordResponse{ | |||||
| message?: string | message?: string | ||||
| } | } | ||||
| export interface PickRecordPlasticBoxCartonQtyResponse { | |||||
| plasticBoxCartonQty2f: number | null; | |||||
| plasticBoxCartonQty3f: number | null; | |||||
| plasticBoxCartonQty4f: number | null; | |||||
| } | |||||
| export const fetchPickRecordPlasticBoxCartonQty = async ( | |||||
| pickOrderId: number, | |||||
| ): Promise<PickRecordPlasticBoxCartonQtyResponse> => { | |||||
| return serverFetchJson<PickRecordPlasticBoxCartonQtyResponse>( | |||||
| `${BASE_API_URL}/jo/pick-record-plastic-box-carton-qty/${pickOrderId}`, | |||||
| { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export interface PrintFGStockInLabelRequest { | export interface PrintFGStockInLabelRequest { | ||||
| stockInLineId: number; | 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<CompletedJobOrderPickOrderDashboardRecord[]> => { | |||||
| const params = new URLSearchParams({ | |||||
| from: from.trim(), | |||||
| to: to.trim(), | |||||
| }); | |||||
| return serverFetchJson<CompletedJobOrderPickOrderDashboardRecord[]>( | |||||
| `${BASE_API_URL}/jo/plastic-box-carton-qty-dashboard?${params.toString()}`, | |||||
| { | |||||
| method: "GET", | |||||
| cache: "no-store", | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const fetchCompletedJobOrderPickOrdersrecords = async ( | |||||
| completedDate?: string | null, | |||||
| ): Promise<CompletedJobOrderPickOrderDashboardRecord[]> => { | |||||
| const q = | const q = | ||||
| completedDate && String(completedDate).trim() !== "" | completedDate && String(completedDate).trim() !== "" | ||||
| ? `?date=${encodeURIComponent(String(completedDate).trim())}` | ? `?date=${encodeURIComponent(String(completedDate).trim())}` | ||||
| : ""; | : ""; | ||||
| return serverFetchJson<any>(`${BASE_API_URL}/jo/completed-job-order-pick-orders-only${q}`, { | |||||
| method: "GET", | |||||
| cache: "no-store", | |||||
| }); | |||||
| return serverFetchJson<CompletedJobOrderPickOrderDashboardRecord[]>( | |||||
| `${BASE_API_URL}/jo/completed-job-order-pick-orders-only${q}`, | |||||
| { | |||||
| method: "GET", | |||||
| cache: "no-store", | |||||
| }, | |||||
| ); | |||||
| }; | }; | ||||
| export const fetchJobOrderPickOrdersrecords = async ( | export const fetchJobOrderPickOrdersrecords = async ( | ||||
| date?: string | null, | date?: string | null, | ||||
| @@ -1368,13 +1424,37 @@ export async function PrintPickRecord(request: PrintPickRecordRequest){ | |||||
| if (request.floor) { | if (request.floor) { | ||||
| params.append('floor', 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 { | export interface ExportFGStockInLabelRequest { | ||||
| @@ -78,6 +78,12 @@ import { | |||||
| } from "@/app/api/doworkbench/actions"; | } from "@/app/api/doworkbench/actions"; | ||||
| import type { PrinterCombo } from "@/app/api/settings/printer"; | import type { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { msg, msgError } from "@/components/Swal/CustomAlerts"; | import { msg, msgError } from "@/components/Swal/CustomAlerts"; | ||||
| import { | |||||
| buildPrintPickRecordRequest, | |||||
| promptAllFloorsPlasticBoxCartonQty, | |||||
| promptPlasticBoxCartonQty, | |||||
| type PickRecordFloor, | |||||
| } from "@/components/Jodetail/pickRecordHelpers"; | |||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| //onSwitchToRecordTab: () => void; | //onSwitchToRecordTab: () => void; | ||||
| @@ -664,6 +670,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| printerOptions.length > 0 ? printerOptions[0] : null, | printerOptions.length > 0 ? printerOptions[0] : null, | ||||
| ); | ); | ||||
| const [printQty, setPrintQty] = useState<number>(1); | const [printQty, setPrintQty] = useState<number>(1); | ||||
| const pickRecordPrintInFlightRef = useRef(false); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // Keep selected printer valid when combo list changes. | // Keep selected printer valid when combo list changes. | ||||
| @@ -685,7 +692,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| }, [printerCombo, a4Printers, printerOptions]); | }, [printerCombo, a4Printers, printerOptions]); | ||||
| const handlePickRecord = useCallback( | const handlePickRecord = useCallback( | ||||
| async (floor: "2F" | "3F" | "4F" | "ALL") => { | |||||
| async (floor: PickRecordFloor) => { | |||||
| if (pickRecordPrintInFlightRef.current) return; | |||||
| try { | try { | ||||
| const pickOrderId = jobOrderData?.pickOrder?.id; | const pickOrderId = jobOrderData?.pickOrder?.id; | ||||
| if (!pickOrderId) { | if (!pickOrderId) { | ||||
| @@ -701,12 +709,36 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| return; | 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) { | if (response?.success) { | ||||
| msg(t("Printed Successfully.")); | msg(t("Printed Successfully.")); | ||||
| @@ -716,6 +748,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCo | |||||
| } catch (e) { | } catch (e) { | ||||
| console.error(e); | console.error(e); | ||||
| msgError(t("An error occurred while printing")); | msgError(t("An error occurred while printing")); | ||||
| } finally { | |||||
| pickRecordPrintInFlightRef.current = false; | |||||
| } | } | ||||
| }, | }, | ||||
| [jobOrderData, printQty, selectedPrinter, t], | [jobOrderData, printQty, selectedPrinter, t], | ||||
| @@ -39,6 +39,7 @@ import { fetchPrinterCombo } from "@/app/api/settings/printer"; | |||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import JoPickOrderDetail from "./JoPickOrderDetail"; | import JoPickOrderDetail from "./JoPickOrderDetail"; | ||||
| import MaterialPickStatusTable from "./MaterialPickStatusTable"; | import MaterialPickStatusTable from "./MaterialPickStatusTable"; | ||||
| import PlasticBoxCartonDashboardTab from "./PlasticBoxCartonDashboardTab"; | |||||
| interface Props { | interface Props { | ||||
| //pickOrders: PickOrderResult[]; | //pickOrders: PickOrderResult[]; | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| @@ -245,26 +246,6 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||||
| setIsOpenCreateModal(false) | 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(() => { | useEffect(() => { | ||||
| const handleCompletionStatusChange = (event: CustomEvent) => { | const handleCompletionStatusChange = (event: CustomEvent) => { | ||||
| const { allLotsCompleted } = event.detail; | const { allLotsCompleted } = event.detail; | ||||
| @@ -499,6 +480,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | ||||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | ||||
| <Tab label={t("Material Pick Status")} iconPosition="end" /> | <Tab label={t("Material Pick Status")} iconPosition="end" /> | ||||
| <Tab label={t("Plastic box carton qty dashboard")} iconPosition="end" /> | |||||
| </Tabs> | </Tabs> | ||||
| </Box> | </Box> | ||||
| @@ -514,6 +496,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => { | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 2 && <MaterialPickStatusTable />} | {tabIndex === 2 && <MaterialPickStatusTable />} | ||||
| {tabIndex === 3 && <PlasticBoxCartonDashboardTab />} | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -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<FloorFilter>("all"); | |||||
| const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD")); | |||||
| const [loading, setLoading] = useState(false); | |||||
| const [isExporting, setIsExporting] = useState(false); | |||||
| const [error, setError] = useState<string>(""); | |||||
| const [records, setRecords] = useState<CompletedJobOrderPickOrderDashboardRecord[]>([]); | |||||
| 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<DailySummaryRow[]>(() => { | |||||
| const summary = new Map<string, DailySummaryRow>(); | |||||
| 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<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: 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<string, DailySummaryRow>(); | |||||
| 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 ( | |||||
| <Box sx={{ width: "100%" }}> | |||||
| <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}> | |||||
| <Typography variant="h6">{t("Plastic box carton qty dashboard")}</Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={handleDownloadExcel} | |||||
| disabled={loading || isExporting} | |||||
| > | |||||
| {isExporting ? t("Exporting...") : t("Download Excel")} | |||||
| </Button> | |||||
| </Stack> | |||||
| {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={t("Floor")} | |||||
| value={floor} | |||||
| onChange={(event) => setFloor(event.target.value as FloorFilter)} | |||||
| > | |||||
| <MenuItem value="all">{t("All")}</MenuItem> | |||||
| <MenuItem value="2/F">2/F</MenuItem> | |||||
| <MenuItem value="3/F">3/F</MenuItem> | |||||
| <MenuItem value="4/F">4/F</MenuItem> | |||||
| </TextField> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t("Date")} | |||||
| 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>{t("2/F plastic box carton qty count")}</TableCell> | |||||
| <TableCell align="right"> | |||||
| {summary.floor2F.toLocaleString("zh-HK")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>{t("3/F plastic box carton qty count")}</TableCell> | |||||
| <TableCell align="right"> | |||||
| {summary.floor3F.toLocaleString("zh-HK")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>{t("4/F plastic box carton qty count")}</TableCell> | |||||
| <TableCell align="right"> | |||||
| {summary.floor4F.toLocaleString("zh-HK")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| <TableRow> | |||||
| <TableCell>{t("Total plastic box carton qty")}</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 PlasticBoxCartonDashboardTab; | |||||
| @@ -46,6 +46,12 @@ import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { | |||||
| buildPrintPickRecordRequest, | |||||
| promptAllFloorsPlasticBoxCartonQty, | |||||
| promptPlasticBoxCartonQty, | |||||
| type PickRecordFloor, | |||||
| } from "./pickRecordHelpers"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| @@ -137,7 +143,8 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| // Use props with fallback | // Use props with fallback | ||||
| const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); | const selectedPrinter = selectedPrinterProp ?? (printerCombo && printerCombo.length > 0 ? printerCombo[0] : null); | ||||
| const printQty = printQtyProp ?? 1; | const printQty = printQtyProp ?? 1; | ||||
| const pickRecordPrintInFlightRef = useRef(false); | |||||
| // 修改:分页状态 | // 修改:分页状态 | ||||
| const [paginationController, setPaginationController] = useState({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -380,15 +387,15 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| const handlePickRecord = useCallback(async ( | const handlePickRecord = useCallback(async ( | ||||
| jobOrderPickOrder: CompletedJobOrderPickOrder, | jobOrderPickOrder: CompletedJobOrderPickOrder, | ||||
| floor: "2F" | "3F" | "4F" | "ALL" | |||||
| floor: PickRecordFloor, | |||||
| ) => { | ) => { | ||||
| if (pickRecordPrintInFlightRef.current) return; | |||||
| try { | try { | ||||
| if (!jobOrderPickOrder) { | if (!jobOrderPickOrder) { | ||||
| console.error("No selected job order pick order available"); | console.error("No selected job order pick order available"); | ||||
| return; | return; | ||||
| } | } | ||||
| // 检查是否已选择打印机 | |||||
| if (!selectedPrinter) { | if (!selectedPrinter) { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| position: "bottom-end", | position: "bottom-end", | ||||
| @@ -400,7 +407,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| return; | return; | ||||
| } | } | ||||
| // 检查打印数量是否有效 | |||||
| if (!printQty || printQty < 1) { | if (!printQty || printQty < 1) { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| position: "bottom-end", | position: "bottom-end", | ||||
| @@ -413,24 +419,37 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| } | } | ||||
| const pickOrderId = jobOrderPickOrder.pickOrderId; | 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); | const response = await PrintPickRecord(printRequest); | ||||
| console.log("Print Pick Record response: ", response); | |||||
| if (response.success) { | if (response.success) { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| position: "bottom-end", | position: "bottom-end", | ||||
| @@ -440,7 +459,6 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| timer: 1500 | timer: 1500 | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| console.error("Print failed: ", response.message); | |||||
| Swal.fire({ | Swal.fire({ | ||||
| position: "bottom-end", | position: "bottom-end", | ||||
| icon: "error", | icon: "error", | ||||
| @@ -458,6 +476,8 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| showConfirmButton: false, | showConfirmButton: false, | ||||
| timer: 1500 | timer: 1500 | ||||
| }); | }); | ||||
| } finally { | |||||
| pickRecordPrintInFlightRef.current = false; | |||||
| } | } | ||||
| }, [t, selectedPrinter, printQty]); | }, [t, selectedPrinter, printQty]); | ||||
| // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | ||||
| @@ -700,7 +720,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ | |||||
| </Box> | </Box> | ||||
| </Stack> | </Stack> | ||||
| </CardContent> | </CardContent> | ||||
| <CardActions sx={{ alignItems: "center", gap: 1 }}> | |||||
| <CardActions sx={{ alignItems: "center", gap: 1, flexWrap: "wrap" }}> | |||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| onClick={() => handleDetailClick(jobOrderPickOrder)} | onClick={() => handleDetailClick(jobOrderPickOrder)} | ||||
| @@ -0,0 +1,184 @@ | |||||
| "use client"; | |||||
| import Swal from "sweetalert2"; | |||||
| import type { TFunction } from "i18next"; | |||||
| import { fetchPickRecordPlasticBoxCartonQty } from "@/app/api/jo/actions"; | |||||
| export type PickRecordFloor = "2F" | "3F" | "4F" | "ALL"; | |||||
| export interface AllFloorsPlasticBoxCartonQtyResult { | |||||
| qty2f: number; | |||||
| qty3f: number; | |||||
| qty4f: number; | |||||
| sum: number; | |||||
| } | |||||
| const inputStyle = | |||||
| "margin:0;flex:1;outline:none;box-shadow:none;border:1px solid #d9d9d9;"; | |||||
| /** Non-negative integers only; empty string allowed when allowEmpty. */ | |||||
| function parseWholeNumberQty(raw: string, allowEmpty: boolean): number | null { | |||||
| const trimmed = raw.trim(); | |||||
| if (trimmed === "") { | |||||
| return allowEmpty ? 0 : null; | |||||
| } | |||||
| if (!/^\d+$/.test(trimmed)) { | |||||
| return null; | |||||
| } | |||||
| return parseInt(trimmed, 10); | |||||
| } | |||||
| function buildFloorQtyInputRow( | |||||
| inputId: string, | |||||
| label: string, | |||||
| placeholder: string, | |||||
| value: number | null | undefined, | |||||
| ): string { | |||||
| const valueAttr = | |||||
| value !== null && value !== undefined ? `value="${value}"` : ""; | |||||
| return ` | |||||
| <div style="display:flex;align-items:center;gap:12px;"> | |||||
| <label for="${inputId}" style="min-width:140px;">${label}</label> | |||||
| <input id="${inputId}" class="swal2-input" type="number" min="0" step="1" inputmode="numeric" placeholder="${placeholder}" ${valueAttr} style="${inputStyle}" onfocus="this.style.outline='none';this.style.boxShadow='none';this.style.borderColor='#d9d9d9';" /> | |||||
| </div> | |||||
| `; | |||||
| } | |||||
| function parseFloorQtyInput(inputId: string): number | null { | |||||
| const raw = | |||||
| (document.getElementById(inputId) as HTMLInputElement | null)?.value?.trim() ?? | |||||
| ""; | |||||
| return parseWholeNumberQty(raw, true); | |||||
| } | |||||
| export async function promptPlasticBoxCartonQty( | |||||
| t: TFunction, | |||||
| ): Promise<number | null> { | |||||
| const result = await Swal.fire({ | |||||
| title: t("Enter plastic box carton qty"), | |||||
| icon: "info", | |||||
| input: "number", | |||||
| inputPlaceholder: t("Plastic box carton Qty"), | |||||
| inputAttributes: { | |||||
| min: "1", | |||||
| step: "1", | |||||
| inputmode: "numeric", | |||||
| }, | |||||
| inputValidator: (value) => { | |||||
| if (!value) { | |||||
| return t("You need to enter a number"); | |||||
| } | |||||
| const trimmed = String(value).trim(); | |||||
| if (!/^\d+$/.test(trimmed)) { | |||||
| return t("Qty must be a whole number"); | |||||
| } | |||||
| if (parseInt(trimmed, 10) < 1) { | |||||
| return t("Number must be at least 1"); | |||||
| } | |||||
| return null; | |||||
| }, | |||||
| showCancelButton: true, | |||||
| confirmButtonText: t("Confirm"), | |||||
| cancelButtonText: t("Cancel"), | |||||
| confirmButtonColor: "#8dba00", | |||||
| cancelButtonColor: "#F04438", | |||||
| }); | |||||
| if (!result.isConfirmed) { | |||||
| return null; | |||||
| } | |||||
| const qty = parseWholeNumberQty(String(result.value ?? ""), false); | |||||
| if (qty === null || qty < 1) { | |||||
| return null; | |||||
| } | |||||
| return qty; | |||||
| } | |||||
| export async function promptAllFloorsPlasticBoxCartonQty( | |||||
| t: TFunction, | |||||
| pickOrderId: number, | |||||
| ): Promise<AllFloorsPlasticBoxCartonQtyResult | null> { | |||||
| let existing = { | |||||
| plasticBoxCartonQty2f: null as number | null, | |||||
| plasticBoxCartonQty3f: null as number | null, | |||||
| plasticBoxCartonQty4f: null as number | null, | |||||
| }; | |||||
| try { | |||||
| existing = await fetchPickRecordPlasticBoxCartonQty(pickOrderId); | |||||
| } catch (e) { | |||||
| console.warn("Failed to load existing plastic box carton qty", e); | |||||
| } | |||||
| const placeholder = t("Plastic box carton Qty"); | |||||
| const result = await Swal.fire({ | |||||
| title: t("Enter all floors plastic box carton qty"), | |||||
| html: ` | |||||
| <div style="display:flex;flex-direction:column;gap:10px;text-align:left;"> | |||||
| ${buildFloorQtyInputRow("swal-qty-2f", t("2/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty2f)} | |||||
| ${buildFloorQtyInputRow("swal-qty-3f", t("3/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty3f)} | |||||
| ${buildFloorQtyInputRow("swal-qty-4f", t("4/F plastic box carton Qty"), placeholder, existing.plasticBoxCartonQty4f)} | |||||
| </div> | |||||
| `, | |||||
| showCancelButton: true, | |||||
| confirmButtonText: t("Confirm"), | |||||
| cancelButtonText: t("Cancel"), | |||||
| confirmButtonColor: "#8dba00", | |||||
| cancelButtonColor: "#F04438", | |||||
| focusConfirm: false, | |||||
| preConfirm: () => { | |||||
| const qty2f = parseFloorQtyInput("swal-qty-2f"); | |||||
| const qty3f = parseFloorQtyInput("swal-qty-3f"); | |||||
| const qty4f = parseFloorQtyInput("swal-qty-4f"); | |||||
| if (qty2f === null || qty3f === null || qty4f === null) { | |||||
| Swal.showValidationMessage(t("Qty must be a whole number")); | |||||
| return null; | |||||
| } | |||||
| if (qty2f < 0 || qty3f < 0 || qty4f < 0) { | |||||
| Swal.showValidationMessage(t("Qty cannot be negative")); | |||||
| return null; | |||||
| } | |||||
| return { | |||||
| qty2f, | |||||
| qty3f, | |||||
| qty4f, | |||||
| sum: qty2f + qty3f + qty4f, | |||||
| } as AllFloorsPlasticBoxCartonQtyResult; | |||||
| }, | |||||
| }); | |||||
| if (!result.isConfirmed || !result.value) { | |||||
| return null; | |||||
| } | |||||
| return result.value as AllFloorsPlasticBoxCartonQtyResult; | |||||
| } | |||||
| export function buildPrintPickRecordRequest(params: { | |||||
| pickOrderId: number; | |||||
| printerId: number; | |||||
| printQty: number; | |||||
| floor: PickRecordFloor; | |||||
| plasticBoxCartonQty?: number; | |||||
| allFloorsQty?: AllFloorsPlasticBoxCartonQtyResult; | |||||
| }) { | |||||
| if (params.floor === "ALL" && params.allFloorsQty) { | |||||
| return { | |||||
| pickOrderId: params.pickOrderId, | |||||
| printerId: params.printerId, | |||||
| printQty: params.printQty, | |||||
| floor: params.floor, | |||||
| plasticBoxCartonQty2f: params.allFloorsQty.qty2f, | |||||
| plasticBoxCartonQty3f: params.allFloorsQty.qty3f, | |||||
| plasticBoxCartonQty4f: params.allFloorsQty.qty4f, | |||||
| }; | |||||
| } | |||||
| return { | |||||
| pickOrderId: params.pickOrderId, | |||||
| printerId: params.printerId, | |||||
| printQty: params.printQty, | |||||
| floor: params.floor, | |||||
| plasticBoxCartonQty: params.plasticBoxCartonQty!, | |||||
| }; | |||||
| } | |||||
| @@ -632,5 +632,33 @@ | |||||
| "BOM Description": "BOM 說明", | "BOM Description": "BOM 說明", | ||||
| "Floor": "樓層", | "Floor": "樓層", | ||||
| "Finish": "完成", | "Finish": "完成", | ||||
| "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "請先點選一列並開啟「挑號 QR 碼」,再掃描 {2fic} 以使用手動換批。" | |||||
| "Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "請先點選一列並開啟「挑號 QR 碼」,再掃描 {2fic} 以使用手動換批。", | |||||
| "Enter plastic box carton qty": "請輸入膠茜數目", | |||||
| "Enter all floors plastic box carton qty": "請輸入各樓層膠茜數目", | |||||
| "2/F plastic box carton Qty": "2/F 膠茜數目", | |||||
| "3/F plastic box carton Qty": "3/F 膠茜數目", | |||||
| "4/F plastic box carton Qty": "4/F 膠茜數目", | |||||
| "Qty cannot be negative": "數量不可為負數", | |||||
| "Qty must be a whole number": "請輸入整數(不可為小數)", | |||||
| "Plastic box carton Qty": "膠茜數目", | |||||
| "Plastic box carton qty dashboard": "膠茜數目使用數量", | |||||
| "Plastic box carton qty usage": "膠茜數目", | |||||
| "Failed to load plastic box carton qty dashboard": "載入膠茜數目使用數量失敗,請稍後再試。", | |||||
| "No chart data": "沒有圖表資料", | |||||
| "2/F plastic box carton qty count": "2/F 膠茜數目", | |||||
| "3/F plastic box carton qty count": "3/F 膠茜數目", | |||||
| "4/F plastic box carton qty count": "4/F 膠茜數目", | |||||
| "Total plastic box carton qty": "總膠茜數目", | |||||
| "All floors": "全部樓層", | |||||
| "Download Excel": "下載 Excel", | |||||
| "Exporting...": "匯出中...", | |||||
| "Summary": "彙總", | |||||
| "Last 7 days": "最近7天", | |||||
| "This month": "本月", | |||||
| "This year": "本年", | |||||
| "Plastic box carton qty report last 7 days": "膠茜數目使用數量(最近7天)", | |||||
| "Plastic box carton qty report this month": "膠茜數目使用數量(本月)", | |||||
| "Plastic box carton qty report this year": "膠茜數目使用數量(本年)", | |||||
| "Plastic box carton qty multi period report": "膠茜數目使用數量_多時段報表", | |||||
| "All": "全部" | |||||
| } | } | ||||