Sfoglia il codice sorgente

Merge branch 'MergeProblem1' of https://git.2fi-solutions.com/jason/FPSMS-frontend into MergeProblem1

stable1
[email protected] 2 settimane fa
parent
commit
e03c97ba13
5 ha cambiato i file con 393 aggiunte e 37 eliminazioni
  1. +48
    -0
      src/app/api/jo/actions.ts
  2. +255
    -0
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
  3. +7
    -2
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  4. +80
    -35
      src/components/Jodetail/completeJobOrderRecord.tsx
  5. +3
    -0
      src/i18n/zh/jo.json

+ 48
- 0
src/app/api/jo/actions.ts Vedi File

@@ -133,6 +133,7 @@ export interface PrintPickRecordRequest{
pickOrderId: number;
printerId: number;
printQty: number;
floor?: "2F" | "3F" | "4F" | "ALL";
}

export interface PrintPickRecordResponse{
@@ -604,6 +605,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 +716,21 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder
},
);
});

/** JO Workbench: in−out available (matches scan-pick); stockouts include suggestedPickQty / suggestedPickLotId when SPL matches SOL lot line */
/*
export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache(
async (pickOrderId: number) => {
return serverFetchJson<JobOrderLotsHierarchicalResponse>(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`,
{
method: "GET",
next: { tags: ["jo-hierarchical-workbench"] },
},
);
},
);
*/
// NOTE: Do NOT wrap in `cache()` because the list needs to reflect just-completed lines
// immediately when navigating back from JobPickExecution.
export const fetchAllJoPickOrders = async (type?: string | null, floor?: string | null) => {
@@ -1076,6 +1095,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<any>(`${BASE_API_URL}/jo/job-order-pick-orders${q}`, {
method: "GET",
cache: "no-store",
});
};

export const fetchJobOrderPickOrderLotDetailsForPick = cache(async (pickOrderId: number) => {
return serverFetchJson<any[]>(`${BASE_API_URL}/jo/job-order-pick-order-lot-details/${pickOrderId}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
})
})
export const fetchJoForPrintQrCode = cache(async (date: string) => {
return serverFetchJson<JobOrderListForPrintQrCodeResponse[]>(
`${BASE_API_URL}/jo/joForPrintQrCode/${date}`,
@@ -1274,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()}`,{


+ 255
- 0
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx Vedi File

@@ -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;

+ 7
- 2
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Vedi File

@@ -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<PrinterCo
}
}, [truckRoutingFilters, selectedPrinterForAllDraft, t]);

const isTruckRoutingTab = tabIndex === 5;
const isTruckRoutingTab = tabIndex === 6;
const canPrintTruckRoutingSummary = Boolean(
truckRoutingFilters.storeId &&
truckRoutingFilters.truckLanceCode &&
@@ -393,7 +394,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo
}, [fetchReleasedOrderCount]);

useEffect(() => {
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 (
<Tab label={t("Finished Good Record")} iconPosition="end" />
<Tab label={t("Ticket Release Table")} iconPosition="end" />
<Tab label={t("Finished Good Record (All)")} iconPosition="end" />
<Tab label="成品出倉出箱數量" iconPosition="end" />
<Tab label="送貨路線摘要" iconPosition="end" />
</Tabs>
@@ -887,6 +889,9 @@ const handleAssignByLane = useCallback(async (
/>
)}
{tabIndex === 5 && (
<FinishedGoodCartonDashboardTab />
)}
{tabIndex === 6 && (
<TruckRoutingSummaryTab onFiltersChange={setTruckRoutingFilters} />
)}
</Box>


+ 80
- 35
src/components/Jodetail/completeJobOrderRecord.tsx Vedi File

@@ -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<Props> = ({
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<Props> = ({
: 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<Props> = ({
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<Props> = ({
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<string, any>) => {
@@ -316,6 +326,15 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
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",
@@ -359,7 +378,10 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
}));
}, []);

const handlePickRecord = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
const handlePickRecord = useCallback(async (
jobOrderPickOrder: CompletedJobOrderPickOrder,
floor: "2F" | "3F" | "4F" | "ALL"
) => {
try {
if (!jobOrderPickOrder) {
console.error("No selected job order pick order available");
@@ -399,7 +421,8 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
const printRequest = {
pickOrderId: pickOrderId,
printerId: printerId,
printQty: printQty
printQty: printQty,
floor,
};

console.log("Printing Pick Record with request: ", printRequest);
@@ -640,7 +663,9 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
</Box>
) : (
<Stack spacing={2}>
{paginatedData.map((jobOrderPickOrder) => (
{paginatedData.map((jobOrderPickOrder) => {
const normalizedStatus = String(jobOrderPickOrder.pickOrderStatus ?? "").toLowerCase();
return (
<Card key={jobOrderPickOrder.id}>
<CardContent>
<Stack direction="row" justifyContent="space-between" alignItems="center">
@@ -660,21 +685,18 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
</Box>
<Box>
<Chip
label={t(jobOrderPickOrder.pickOrderStatus) }
color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'}
size="small"
sx={{ mb: 1 }}
/>
<Chip
label={t(jobOrderPickOrder.pickOrderStatus)}
color={normalizedStatus === "completed" ? "success" : "default"}
size="small"
sx={{ mb: 1 }}
/>
<Typography variant="body2" color="text.secondary">
{jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")}
</Typography>
<Chip
label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")}
color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'}
size="small"
sx={{ mt: 1 }}
/>
</Box>
</Stack>
</CardContent>
@@ -685,16 +707,39 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
>
{t("View Details")}
</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"
onClick={() => handlePickRecord(jobOrderPickOrder)}
onClick={() => handlePickRecord(jobOrderPickOrder, "4F")}
>
{t("Print Pick Record")}
{t("Print Pick Record")} 4F
</Button>
</CardActions>
</Card>
))}
);
})}
</Stack>
)}



+ 3
- 0
src/i18n/zh/jo.json Vedi File

@@ -167,6 +167,9 @@
"View Details": "查看詳情",
"Skip": "跳過",
"Handler": "提料員",
"RELEASED": "已放單",
"Released": "已放單",
"COMPLETED": "已完成",
"Now": "現時",
"Last updated": "最後更新",
"Auto-refresh every 5 minutes": "每5分鐘自動刷新",


Caricamento…
Annulla
Salva