浏览代码

added po number syn m18

MergeProblem1
PC-20260115JRSN\Administrator 1 天前
父节点
当前提交
c9f05abfb0
共有 4 个文件被更改,包括 370 次插入19 次删除
  1. +69
    -0
      src/app/(main)/testing/page.tsx
  2. +156
    -2
      src/components/BagPrint/BagPrintSearch.tsx
  3. +130
    -17
      src/components/PoSearch/PoSearch.tsx
  4. +15
    -0
      src/components/SearchBox/SearchBox.tsx

+ 69
- 0
src/app/(main)/testing/page.tsx 查看文件

@@ -90,6 +90,10 @@ export default function TestingPage() {

// --- 6. GRN Preview (M18) ---
const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16");
// --- 7. M18 PO Sync by Code ---
const [m18PoCode, setM18PoCode] = useState("");
const [isSyncingM18Po, setIsSyncingM18Po] = useState(false);
const [m18PoSyncResult, setM18PoSyncResult] = useState<string>("");

// Generic handler for inline table edits
const handleItemChange = (setter: any, id: number, field: string, value: string) => {
@@ -247,6 +251,33 @@ export default function TestingPage() {
}
};

// M18 PO Sync By Code (Section 7)
const handleSyncM18PoByCode = async () => {
if (!m18PoCode.trim()) {
alert("Please enter PO code.");
return;
}
setIsSyncingM18Po(true);
setM18PoSyncResult("");
try {
const response = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent(m18PoCode.trim())}`,
{ method: "GET" },
);
if (response.status === 401 || response.status === 403) return;
const text = await response.text();
setM18PoSyncResult(text);
if (!response.ok) {
alert(`Sync failed: ${response.status}`);
}
} catch (e) {
console.error("M18 PO Sync By Code Error:", e);
alert("M18 PO sync failed. Check console/network.");
} finally {
setIsSyncingM18Po(false);
}
};

// Layout Helper
const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => (
<Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}>
@@ -268,6 +299,7 @@ export default function TestingPage() {
<Tab label="4. Laser" />
<Tab label="5. HANS600S-M" />
<Tab label="6. GRN Preview" />
<Tab label="7. M18 PO Sync" />
</Tabs>

<TabPanel value={tabValue} index={0}>
@@ -523,6 +555,43 @@ export default function TestingPage() {
</Section>
</TabPanel>

<TabPanel value={tabValue} index={6}>
<Section title="7. M18 PO Sync by Code">
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
<TextField
size="small"
label="PO Code"
value={m18PoCode}
onChange={(e) => setM18PoCode(e.target.value)}
placeholder="e.g. PFP002PO26030341"
sx={{ minWidth: 320 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleSyncM18PoByCode}
disabled={isSyncingM18Po}
>
{isSyncingM18Po ? "Syncing..." : "Sync PO from M18"}
</Button>
</Stack>
<Typography variant="body2" color="textSecondary">
Backend endpoint: <code>/m18/test/po-by-code?code=YOUR_CODE</code>
</Typography>
{m18PoSyncResult ? (
<TextField
fullWidth
multiline
minRows={4}
margin="normal"
label="Sync Result"
value={m18PoSyncResult}
InputProps={{ readOnly: true }}
/>
) : null}
</Section>
</TabPanel>

{/* Dialog for OnPack */}
<Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm">
<DialogTitle sx={{ bgcolor: 'success.main', color: 'white' }}>OnPack Printer Job Details</DialogTitle>


+ 156
- 2
src/components/BagPrint/BagPrintSearch.tsx 查看文件

@@ -27,6 +27,8 @@ import Print from "@mui/icons-material/Print";
import Download from "@mui/icons-material/Download";
import { checkPrinterStatus, downloadOnPackQrZip, fetchJobOrders, JobOrderListItem } from "@/app/api/bagPrint/actions";
import dayjs from "dayjs";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";

// Light blue theme (matching Python Bag1)
const BG_TOP = "#E8F4FC";
@@ -94,6 +96,11 @@ const BagPrintSearch: React.FC = () => {
const [connected, setConnected] = useState(false);
const [printer, setPrinter] = useState<string>("dataflex");
const [selectedId, setSelectedId] = useState<number | null>(null);
const [printDialogOpen, setPrintDialogOpen] = useState(false);
const [printTarget, setPrintTarget] = useState<JobOrderListItem | null>(null);
const [printCount, setPrintCount] = useState(0);
const [printContinuous, setPrintContinuous] = useState(false);
const [printing, setPrinting] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity?: "success" | "info" | "error" }>({ open: false, message: "" });
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
@@ -187,8 +194,76 @@ const BagPrintSearch: React.FC = () => {
const itemCode = jo.itemCode || "—";
const itemName = jo.itemName || "—";
setSnackbar({ open: true, message: `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, severity: "info" });
// TODO: Actual printing would require backend API to proxy to printer (TCP/serial)
// For now, show info. Backend could add /py/print-dataflex, /py/print-label, /py/print-laser

// Align with Bag2.py "click row -> ask bag count -> print" for DataFlex.
if (printer === "dataflex") {
setPrintTarget(jo);
setPrintCount(0);
setPrintContinuous(false);
setPrintDialogOpen(true);
}
};

const confirmPrintDataFlex = async () => {
if (!printTarget) return;
if (printer !== "dataflex") {
setSnackbar({ open: true, message: "此頁目前只支援打袋機 DataFlex 列印", severity: "error" });
return;
}

if (!printContinuous && printCount < 1) {
setSnackbar({ open: true, message: "請先按 +50、+10、+5 或 +1 選擇數量。", severity: "error" });
return;
}

const qty = printContinuous ? -1 : printCount;
const printerIp = settings.dabag_ip;
const printerPort = Number(settings.dabag_port || 3008);

if (!printerIp) {
setSnackbar({ open: true, message: "請先在設定中填寫打袋機 DataFlex 的 IP。", severity: "error" });
return;
}

setPrinting(true);
try {
const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
itemCode: printTarget.itemCode || "—",
itemName: printTarget.itemName || "—",
lotNo: printTarget.lotNo || "—",
// DataFlex zpl (Bag2.py) only needs itemId + stockInLineId for QR payload (optional).
itemId: printTarget.itemId,
stockInLineId: printTarget.stockInLineId,
printerIp,
printerPort,
printQty: qty,
}),
});

if (resp.status === 401 || resp.status === 403) return;

if (!resp.ok) {
const msg = await resp.text().catch(() => "");
setSnackbar({
open: true,
message: `DataFlex 列印失敗(狀態碼 ${resp.status})。${msg ? msg.slice(0, 120) : ""}`,
severity: "error",
});
return;
}

const batch = getBatch(printTarget);
const printedText = qty === -1 ? "連續 (C)" : `${qty}`;
setSnackbar({ open: true, message: `已送出列印:批次 ${batch} x ${printedText}`, severity: "success" });
setPrintDialogOpen(false);
} catch (e) {
setSnackbar({ open: true, message: e instanceof Error ? e.message : "DataFlex 列印失敗", severity: "error" });
} finally {
setPrinting(false);
}
};

const handleDownloadOnPackQr = async () => {
@@ -374,6 +449,85 @@ const BagPrintSearch: React.FC = () => {
)}
</Paper>

{/* Print count dialog (DataFlex) */}
<Dialog open={printDialogOpen} onClose={() => (printing ? null : setPrintDialogOpen(false))} maxWidth="xs" fullWidth>
<DialogTitle>打袋機 DataFlex 列印數量</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 700 }}>
列印多少個袋?
</Typography>
<Typography variant="body2" color="text.secondary">
{printContinuous ? "連續 (C)" : `數量: ${printCount}`}
</Typography>
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap">
<Button
size="small"
variant="contained"
onClick={() => {
setPrintContinuous(false);
setPrintCount((c) => c + 50);
}}
disabled={printing}
>
+50
</Button>
<Button
size="small"
variant="contained"
onClick={() => {
setPrintContinuous(false);
setPrintCount((c) => c + 10);
}}
disabled={printing}
>
+10
</Button>
<Button
size="small"
variant="contained"
onClick={() => {
setPrintContinuous(false);
setPrintCount((c) => c + 5);
}}
disabled={printing}
>
+5
</Button>
<Button
size="small"
variant="contained"
onClick={() => {
setPrintContinuous(false);
setPrintCount((c) => c + 1);
}}
disabled={printing}
>
+1
</Button>
<Button
size="small"
variant={printContinuous ? "contained" : "outlined"}
onClick={() => {
setPrintContinuous(true);
}}
disabled={printing}
>
連續 (C)
</Button>
</Stack>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setPrintDialogOpen(false)} disabled={printing}>
取消
</Button>
<Button variant="contained" onClick={() => void confirmPrintDataFlex()} disabled={printing}>
{printing ? <CircularProgress size={16} /> : "確認送出"}
</Button>
</DialogActions>
</Dialog>

{/* Settings dialog */}
<Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>設定</DialogTitle>


+ 130
- 17
src/components/PoSearch/PoSearch.tsx 查看文件

@@ -7,7 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import SearchBox, { Criterion } from "../SearchBox";
import SearchResults, { Column } from "../SearchResults";
import { EditNote } from "@mui/icons-material";
import { Button, Grid, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Backdrop, Button, CircularProgress, Grid, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import QrModal from "../PoDetail/QrModal";
import { WarehouseResult } from "@/app/api/warehouse";
import NotificationIcon from "@mui/icons-material/NotificationImportant";
@@ -18,6 +18,8 @@ import dayjs from "dayjs";
import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil";
import arraySupport from "dayjs/plugin/arraySupport";
import { Checkbox, Box } from "@mui/material";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
dayjs.extend(arraySupport);

type Props = {
@@ -263,6 +265,9 @@ const PoSearch: React.FC<Props> = ({
}, [po]);

const [isOpenScanner, setOpenScanner] = useState(false);
const [autoSyncStatus, setAutoSyncStatus] = useState<string | null>(null);
const [isM18LookupLoading, setIsM18LookupLoading] = useState(false);
const autoSyncInProgressRef = React.useRef(false);
const onOpenScanner = useCallback(() => {
setOpenScanner(true);
}, []);
@@ -282,12 +287,96 @@ const PoSearch: React.FC<Props> = ({
...pagingController,
...filterArgs,
};
setAutoSyncStatus(null);

const res = await fetchPoListClient(params);
// const res = await testing(params);
if (res) {
console.log(res);
if (!res) return;

if (res.records && res.records.length > 0) {
setFilteredPo(res.records);
setTotalCount(res.total);
return;
}

const searchedCodeRaw = (filterArgs as any)?.code;
const searchedCode =
typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : "";

const shouldAutoSyncFromM18 =
searchedCode.length > 14 &&
(searchedCode.startsWith("PP") || searchedCode.startsWith("PF"));

if (!shouldAutoSyncFromM18 || autoSyncInProgressRef.current) {
setFilteredPo(res.records);
setTotalCount(res.total);
return;
}

try {
autoSyncInProgressRef.current = true;
setIsM18LookupLoading(true);
setAutoSyncStatus("正在從M18找尋PO...");
const syncResp = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent(
searchedCode,
)}`,
{ method: "GET" },
);

if (!syncResp.ok) {
throw new Error(`M18 sync failed: ${syncResp.status}`);
}

let syncJson: any = null;
try {
syncJson = await syncResp.json();
} catch {
// Some endpoints may respond with plain text
const txt = await syncResp.text();
syncJson = { raw: txt };
}

const syncOk = Boolean(syncJson?.totalSuccess && syncJson.totalSuccess > 0);
if (syncOk) {
setAutoSyncStatus("成功找到PO");

// Re-fetch /po/list directly from client to avoid cached server action results.
const cleanedQuery: Record<string, string> = {};
Object.entries(params).forEach(([k, v]) => {
if (v === undefined || v === null) return;
if (typeof v === "string" && v.trim() === "") return;
cleanedQuery[k] = String(v);
});

const listResp = await clientAuthFetch(
`${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams(
cleanedQuery,
).toString()}`,
{ method: "GET" },
);
if (listResp.ok) {
const listJson = await listResp.json();
setFilteredPo(listJson.records ?? []);
setTotalCount(listJson.total ?? 0);
setAutoSyncStatus("成功找到PO");
return;
}
setAutoSyncStatus("找不到PO");
} else {
setAutoSyncStatus("找不到PO");
}

// Ensure UI updates even if sync didn't change results
setFilteredPo(res.records);
setTotalCount(res.total ?? 0);
} catch (e) {
console.error("Auto sync error:", e);
setAutoSyncStatus("找不到PO");
setFilteredPo(res.records);
setTotalCount(res.total ?? 0);
} finally {
setIsM18LookupLoading(false);
autoSyncInProgressRef.current = false;
}
},
[],
@@ -328,26 +417,43 @@ const PoSearch: React.FC<Props> = ({
<>
<SearchBox
criteria={searchCriteria}
disabled={isM18LookupLoading}
onSearch={(query) => {
if (isM18LookupLoading) return;
console.log(query);
setFilterArgs({
code: query.code,
supplier: query.supplier,
status: query.status === "All" ? "" : query.status,
escalated:
query.escalated === "All"
? undefined
: query.escalated === t("Escalated"),
estimatedArrivalDate: query.estimatedArrivalDate === "Invalid Date" ? "" : query.estimatedArrivalDate,
estimatedArrivalDateTo: query.estimatedArrivalDateTo === "Invalid Date" ? "" : query.estimatedArrivalDateTo,
orderDate: query.orderDate === "Invalid Date" ? "" : query.orderDate,
orderDateTo: query.orderDateTo === "Invalid Date" ? "" : query.orderDateTo,
});
const code = typeof query.code === "string" ? query.code.trim() : "";
if (code) {
// When PO code is provided, ignore other search criteria (especially date ranges).
setFilterArgs({ code });
} else {
setFilterArgs({
code: query.code,
supplier: query.supplier,
status: query.status === "All" ? "" : query.status,
escalated:
query.escalated === "All"
? undefined
: query.escalated === t("Escalated"),
estimatedArrivalDate: query.estimatedArrivalDate === "Invalid Date" ? "" : query.estimatedArrivalDate,
estimatedArrivalDateTo: query.estimatedArrivalDateTo === "Invalid Date" ? "" : query.estimatedArrivalDateTo,
orderDate: query.orderDate === "Invalid Date" ? "" : query.orderDate,
orderDateTo: query.orderDateTo === "Invalid Date" ? "" : query.orderDateTo,
});
}
setSelectedPoIds([]); // reset selected po ids
setSelectAll(false); // reset select all
}}
onReset={onReset}
/>
{autoSyncStatus ? (
<Typography
variant="body2"
color={isM18LookupLoading ? "warning.main" : "text.secondary"}
sx={{ mb: 1 }}
>
{autoSyncStatus}
</Typography>
) : null}
<SearchResults<PoResult>
items={filteredPo}
columns={columns}
@@ -374,6 +480,13 @@ const PoSearch: React.FC<Props> = ({
{t("View Selected")} ({selectedPoIds.length})
</Button>
</Box>
<Backdrop
open={isM18LookupLoading}
sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.modal + 1, flexDirection: "column", gap: 1 }}
>
<CircularProgress color="inherit" />
<Typography variant="body1">正在從M18找尋PO...</Typography>
</Backdrop>
</>
</>
);


+ 15
- 0
src/components/SearchBox/SearchBox.tsx 查看文件

@@ -122,6 +122,8 @@ interface Props<T extends string> {
onReset?: () => void;
/** Optional actions rendered in the same row as Reset/Search (e.g. Download, Upload buttons) */
extraActions?: React.ReactNode;
/** Disable inputs/actions while external task is running */
disabled?: boolean;
}

function SearchBox<T extends string>({
@@ -129,6 +131,7 @@ function SearchBox<T extends string>({
onSearch,
onReset,
extraActions,
disabled = false,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultAll: AutocompleteOptions = {
@@ -295,6 +298,7 @@ function SearchBox<T extends string>({
placeholder={c.placeholder}
onChange={makeInputChangeHandler(c.paramName)}
value={inputs[c.paramName]}
disabled={disabled}
/>
)}
{/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
@@ -314,6 +318,7 @@ function SearchBox<T extends string>({
label={t(c.label)}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName] ?? "All"}
disabled={disabled}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option) => (
@@ -331,6 +336,7 @@ function SearchBox<T extends string>({
label={t(c.label)}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName] ?? "All"}
disabled={disabled}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option) => (
@@ -356,6 +362,7 @@ function SearchBox<T extends string>({
noOptionsText={c.noOptionsText ?? t("No options")}
disableClearable
fullWidth
disabled={disabled}
value={
c.multiple
? intersectionWith(
@@ -455,6 +462,7 @@ function SearchBox<T extends string>({
format={`${OUTPUT_DATE_FORMAT}`}
label={t(c.label)}
onChange={makeDateChangeHandler(c.paramName)}
disabled={disabled}
value={
dayjs(inputs[c.paramName]).isValid()
? dayjs(inputs[c.paramName])
@@ -475,6 +483,7 @@ function SearchBox<T extends string>({
format={`${OUTPUT_DATE_FORMAT}`}
label={c.label2 ? t(c.label2) : null}
onChange={makeDateToChangeHandler(c.paramName)}
disabled={disabled}
value={
dayjs(inputs[`${c.paramName}To`]).isValid()
? dayjs(inputs[`${c.paramName}To`])
@@ -498,6 +507,7 @@ function SearchBox<T extends string>({
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
label={t(c.label)}
onChange={makeDatetimeChangeHandler(c.paramName)}
disabled={disabled}
value={
dayjs(inputs[c.paramName]).isValid()
? dayjs(inputs[c.paramName])
@@ -519,6 +529,7 @@ function SearchBox<T extends string>({
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
label={c.label2 ? t(c.label2) : null}
onChange={makeDatetimeToChangeHandler(c.paramName)}
disabled={disabled}
value={
dayjs(inputs[`${c.paramName}To`]).isValid()
? dayjs(inputs[`${c.paramName}To`])
@@ -541,6 +552,7 @@ function SearchBox<T extends string>({
format={`${OUTPUT_DATE_FORMAT}`}
label={t(c.label)}
onChange={makeDateChangeHandler(c.paramName)}
disabled={disabled}
/>
</FormControl>
</Box>
@@ -556,6 +568,7 @@ function SearchBox<T extends string>({
format="HH:mm"
label={t(c.label)}
onChange={makeTimeChangeHandler(c.paramName)}
disabled={disabled}
value={
inputs[c.paramName] && dayjs(inputs[c.paramName], "HH:mm").isValid()
? dayjs(inputs[c.paramName], "HH:mm")
@@ -574,6 +587,7 @@ function SearchBox<T extends string>({
variant="outlined"
startIcon={<RestartAlt />}
onClick={handleReset}
disabled={disabled}
sx={{ borderColor: "#e2e8f0", color: "#334155" }}
>
{t("Reset")}
@@ -583,6 +597,7 @@ function SearchBox<T extends string>({
color="primary"
startIcon={<Search />}
onClick={handleSearch}
disabled={disabled}
>
{t("Search")}
</Button>


正在加载...
取消
保存