| @@ -222,7 +222,7 @@ export default function TestingPage() { | |||||
| const lim = parseInt(laserAutoLimit.trim(), 10); | const lim = parseInt(laserAutoLimit.trim(), 10); | ||||
| const report = await runLaserBag2AutoSend({ | const report = await runLaserBag2AutoSend({ | ||||
| planStart: laserAutoPlanDate, | planStart: laserAutoPlanDate, | ||||
| limitPerRun: Number.isFinite(lim) ? lim : 0, | |||||
| limitPerRun: Number.isFinite(lim) ? lim : 1, | |||||
| }); | }); | ||||
| setLaserAutoReport(report); | setLaserAutoReport(report); | ||||
| try { | try { | ||||
| @@ -531,11 +531,11 @@ export default function TestingPage() { | |||||
| /> | /> | ||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| label="limitPerRun(0=全部符合)" | |||||
| label="limitPerRun(目前固定只送第一筆)" | |||||
| value={laserAutoLimit} | value={laserAutoLimit} | ||||
| onChange={(e) => setLaserAutoLimit(e.target.value)} | onChange={(e) => setLaserAutoLimit(e.target.value)} | ||||
| sx={{ width: 200 }} | sx={{ width: 200 }} | ||||
| helperText="手動測試建議 1;排程預設每分鐘最多 1 筆" | |||||
| helperText="目前後端會限制為第一筆;此欄位保留給未來調整" | |||||
| /> | /> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -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{ | ||||
| @@ -1318,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()}`,{ | ||||
| @@ -167,7 +167,7 @@ export interface LaserBag2AutoSendReport { | |||||
| /** | /** | ||||
| * Same workflow as /laserPrint row click: list job orders (LASER_PRINT.itemCodes) for planStart, | * Same workflow as /laserPrint row click: list job orders (LASER_PRINT.itemCodes) for planStart, | ||||
| * then TCP send(s) using DB LASER_PRINT.host / port. limitPerRun 0 = all matches. | |||||
| * then TCP send using DB LASER_PRINT.host / port (server currently sends first matching job only). | |||||
| */ | */ | ||||
| export async function runLaserBag2AutoSend(params?: { | export async function runLaserBag2AutoSend(params?: { | ||||
| planStart?: string; | planStart?: string; | ||||
| @@ -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> | ||||
| @@ -378,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"); | ||||
| @@ -418,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); | ||||
| @@ -703,12 +707,33 @@ 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> | ||||