|
- "use client";
-
- import React, { useRef, useState } from "react";
- import { Box, Button, Paper, Stack, Tab, Tabs, TextField, Typography } from "@mui/material";
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
- import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
-
- interface TabPanelProps {
- children?: React.ReactNode;
- index: number;
- value: number;
- }
-
- function TabPanel(props: TabPanelProps) {
- const { children, value, index, ...other } = props;
- return (
- <div
- role="tabpanel"
- hidden={value !== index}
- id={`m18syn-tabpanel-${index}`}
- aria-labelledby={`m18syn-tab-${index}`}
- {...other}
- >
- {value === index && <Box sx={{ p: 3 }}>{children}</Box>}
- </div>
- );
- }
-
- export default function M18SynPage() {
- const [tabValue, setTabValue] = useState(0);
-
- type SyncResultDto = {
- totalProcessed: number;
- totalSuccess: number;
- totalFail: number;
- query?: string | null;
- };
-
- type ParsedSyncResult =
- | { kind: "exists"; message: string }
- | { kind: "not_found"; message: string }
- | { kind: "success"; message: string; totalSuccess: number }
- | { kind: "fail"; message: string; totalFail: number }
- | { kind: "mixed"; message: string; totalSuccess: number; totalFail: number }
- | { kind: "raw"; message: string };
-
- const parseSyncResult = (docName: string, rawText: string): ParsedSyncResult => {
- const text = rawText?.trim() ?? "";
- if (!text) return { kind: "raw", message: "未收到回應" };
- try {
- const obj = JSON.parse(text) as Partial<SyncResultDto>;
- const totalSuccess = Number(obj.totalSuccess ?? 0);
- const totalFail = Number(obj.totalFail ?? 0);
- const totalProcessed = Number(obj.totalProcessed ?? 0);
- const query = (obj.query ?? "").toString();
-
- // newOnly skip message from backend
- if (query.includes("skipped") && query.includes("already exists")) {
- return { kind: "exists", message: `${docName}已存在系統` };
- }
-
- // "not found in M18" (sync-by-code returns processed=1, fail=1)
- if (
- docName === "送貨訂單" &&
- totalProcessed === 1 &&
- totalSuccess === 0 &&
- totalFail === 1 &&
- query.includes("code=equal=")
- ) {
- return { kind: "not_found", message: `在M18找不到${docName}` };
- }
-
- if (totalSuccess > 0 && totalFail === 0) {
- return { kind: "success", totalSuccess, message: `成功同步: ${totalSuccess}張${docName}` };
- }
- if (totalSuccess === 0 && totalFail > 0) {
- return { kind: "fail", totalFail, message: `失敗: 無法同步${docName}` };
- }
- return { kind: "mixed", totalSuccess, totalFail, message: `完成: 成功 ${totalSuccess} / 失敗 ${totalFail} (${docName})` };
- } catch {
- // Non-JSON error body or plain text
- return { kind: "raw", message: text };
- }
- };
-
- const formatSyncResultText = (docName: string, rawText: string): string => parseSyncResult(docName, rawText).message;
-
- const [m18PoCode, setM18PoCode] = useState("");
- const [isSyncingM18Po, setIsSyncingM18Po] = useState(false);
- const [m18PoSyncResult, setM18PoSyncResult] = useState<string>("");
- const m18PoInFlightRef = useRef(false);
-
- const [m18DoCode, setM18DoCode] = useState("");
- const [isSyncingM18Do, setIsSyncingM18Do] = useState(false);
- const [m18DoSyncResult, setM18DoSyncResult] = useState<string>("");
- const m18DoInFlightRef = useRef(false);
-
- const [m18DoExtraCode, setM18DoExtraCode] = useState("");
- const [isSyncingM18DoExtra, setIsSyncingM18DoExtra] = useState(false);
- const [m18DoExtraSyncResult, setM18DoExtraSyncResult] = useState<string>("");
- const m18DoExtraInFlightRef = useRef(false);
-
- const [m18ProductCode, setM18ProductCode] = useState("");
- const [isSyncingM18Product, setIsSyncingM18Product] = useState(false);
- const [m18ProductSyncResult, setM18ProductSyncResult] = useState<string>("");
- const m18ProductInFlightRef = useRef(false);
-
- const handleSyncM18PoByCode = async () => {
- if (m18PoInFlightRef.current) return;
- if (!m18PoCode.trim()) {
- alert("Please enter PO code.");
- return;
- }
- m18PoInFlightRef.current = true;
- 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(formatSyncResultText("採購單", 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);
- m18PoInFlightRef.current = false;
- }
- };
-
- const handleSyncM18DoByCode = async () => {
- if (m18DoInFlightRef.current) return;
- if (!m18DoCode.trim()) {
- alert("Please enter DO / shop PO code.");
- return;
- }
- m18DoInFlightRef.current = true;
- setIsSyncingM18Do(true);
- setM18DoSyncResult("");
- try {
- const response = await clientAuthFetch(
- `${NEXT_PUBLIC_API_URL}/m18/test/do-by-code?code=${encodeURIComponent(m18DoCode.trim())}`,
- { method: "GET" },
- );
- if (response.status === 401 || response.status === 403) return;
- const text = await response.text();
- setM18DoSyncResult(formatSyncResultText("送貨訂單", text));
- if (!response.ok) {
- alert(`Sync failed: ${response.status}`);
- }
- } catch (e) {
- console.error("M18 DO Sync By Code Error:", e);
- alert("M18 DO sync failed. Check console/network.");
- } finally {
- setIsSyncingM18Do(false);
- m18DoInFlightRef.current = false;
- }
- };
-
- /** DO(加單):手動按 code 同步,並寫入本地 isExtra=true(可輸入多個 code,用逗號或換行分隔) */
- const handleSyncM18DoExtraByCode = async () => {
- if (m18DoExtraInFlightRef.current) return;
- const raw = m18DoExtraCode.trim();
- if (!raw) {
- alert("Please enter DO / shop PO code(s) (加單).");
- return;
- }
- const codes = raw
- .split(/[\n,]+/g)
- .map((s) => s.trim())
- .filter(Boolean);
- if (codes.length === 0) {
- alert("Please enter at least one code.");
- return;
- }
- m18DoExtraInFlightRef.current = true;
- setIsSyncingM18DoExtra(true);
- setM18DoExtraSyncResult("");
- try {
- const outputs: string[] = [];
- const okCodes: string[] = [];
- const existsCodes: string[] = [];
- const notFoundCodes: string[] = [];
- const otherFailCodes: string[] = [];
- for (const code of codes) {
- const response = await clientAuthFetch(
- `${NEXT_PUBLIC_API_URL}/m18/test/do-by-code-extra?code=${encodeURIComponent(code)}`,
- { method: "GET" },
- );
- if (response.status === 401 || response.status === 403) return;
- const text = await response.text();
- const parsed = parseSyncResult("送貨訂單", text || "");
- if (parsed.kind === "success") okCodes.push(code);
- else if (parsed.kind === "exists") existsCodes.push(code);
- else if (parsed.kind === "not_found") notFoundCodes.push(code);
- else otherFailCodes.push(code);
-
- const perCodeMsg =
- parsed.kind === "success" ? "成功同步" :
- parsed.kind === "exists" ? `失敗: ${parsed.message}` :
- parsed.kind === "not_found" ? parsed.message :
- parsed.message;
- outputs.push(`${code}: ${perCodeMsg}${response.ok ? "" : ` (HTTP ${response.status})`}`);
- }
-
- // Summary
- outputs.push("");
- outputs.push(`共${okCodes.length}張送貨訂單成功`);
- if (notFoundCodes.length > 0) outputs.push(`共${notFoundCodes.length}張在M18找不到訂單 (${notFoundCodes.join(", ")})`);
- if (existsCodes.length > 0) outputs.push(`共${existsCodes.length}張訂單已存在系統 (${existsCodes.join(", ")})`);
- if (otherFailCodes.length > 0) outputs.push(`共${otherFailCodes.length}張同步失敗 (${otherFailCodes.join(", ")})`);
-
- setM18DoExtraSyncResult(outputs.join("\n"));
- } catch (e) {
- console.error("M18 DO (加單) Sync By Code Error:", e);
- alert("M18 DO (加單) sync failed. Check console/network.");
- } finally {
- setIsSyncingM18DoExtra(false);
- m18DoExtraInFlightRef.current = false;
- }
- };
-
- const handleSyncM18ProductByCode = async () => {
- if (m18ProductInFlightRef.current) return;
- if (!m18ProductCode.trim()) {
- alert("Please enter M18 item / product code.");
- return;
- }
- m18ProductInFlightRef.current = true;
- setIsSyncingM18Product(true);
- setM18ProductSyncResult("");
- try {
- const response = await clientAuthFetch(
- `${NEXT_PUBLIC_API_URL}/m18/test/product-by-code?code=${encodeURIComponent(m18ProductCode.trim())}`,
- { method: "GET" },
- );
- if (response.status === 401 || response.status === 403) return;
- const text = await response.text();
- setM18ProductSyncResult(formatSyncResultText("產品", text));
- if (!response.ok) {
- alert(`Sync failed: ${response.status}`);
- }
- } catch (e) {
- console.error("M18 Product Sync By Code Error:", e);
- alert("M18 product sync failed. Check console/network.");
- } finally {
- setIsSyncingM18Product(false);
- m18ProductInFlightRef.current = false;
- }
- };
-
- const Section = ({ title, children }: { title: string; children?: React.ReactNode }) => (
- <Paper sx={{ p: 3, minHeight: "450px", display: "flex", flexDirection: "column" }}>
- <Typography variant="h5" gutterBottom color="primary" sx={{ borderBottom: "2px solid #f0f0f0", pb: 1, mb: 2 }}>
- {title}
- </Typography>
- {children || <Typography color="textSecondary" sx={{ m: "auto" }}>Waiting for implementation...</Typography>}
- </Paper>
- );
-
- return (
- <Box sx={{ p: 4 }}>
- <Typography variant="h4" sx={{ mb: 4, fontWeight: "bold" }}>
- M18 Sync (by code)
- </Typography>
- <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
- ADMIN only. Sync Purchase Order, Delivery Order, or product/material from M18 using document or item code.
- </Typography>
-
- <Tabs value={tabValue} onChange={(_, v) => setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth">
- <Tab label="1. 採購單" id="m18syn-tab-0" />
- <Tab label="2. 送貨訂單" id="m18syn-tab-1" />
- <Tab label="3. 送貨訂單 (加單)" id="m18syn-tab-2" />
- <Tab label="4. Product" id="m18syn-tab-3" />
- </Tabs>
-
- <TabPanel value={tabValue} index={0}>
- <Section title="M18 採購單 — 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>
- {m18PoSyncResult ? (
- <TextField
- fullWidth
- multiline
- minRows={4}
- margin="normal"
- label="Sync Result"
- value={m18PoSyncResult}
- InputProps={{ readOnly: true }}
- />
- ) : null}
- </Section>
- </TabPanel>
-
- <TabPanel value={tabValue} index={1}>
- <Section title="M18 送貨訂單 — sync by code">
- <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
- <TextField
- size="small"
- label="DO / Shop PO Code"
- value={m18DoCode}
- onChange={(e) => setM18DoCode(e.target.value)}
- placeholder="e.g. same document code as M18 shop PO"
- sx={{ minWidth: 320 }}
- />
- <Button variant="contained" color="primary" onClick={handleSyncM18DoByCode} disabled={isSyncingM18Do}>
- {isSyncingM18Do ? "Syncing..." : "Sync DO from M18"}
- </Button>
- </Stack>
- {m18DoSyncResult ? (
- <TextField
- fullWidth
- multiline
- minRows={4}
- margin="normal"
- label="Sync Result"
- value={m18DoSyncResult}
- InputProps={{ readOnly: true }}
- />
- ) : null}
- </Section>
- </TabPanel>
-
- <TabPanel value={tabValue} index={2}>
- <Section title="M18 送貨訂單 — 加單 (isExtra)">
- <Stack spacing={2} sx={{ mb: 2 }}>
- <TextField
- label="DO / Shop PO Code(加單)"
- value={m18DoExtraCode}
- onChange={(e) => setM18DoExtraCode(e.target.value)}
- placeholder="可輸入多個 code,用逗號或換行分隔"
- fullWidth
- multiline
- minRows={6}
- maxRows={14}
- />
- <Box sx={{ display: "flex", justifyContent: "flex-end" }}>
- <Button
- variant="contained"
- color="primary"
- onClick={handleSyncM18DoExtraByCode}
- disabled={isSyncingM18DoExtra}
- >
- {isSyncingM18DoExtra ? "Syncing..." : "Sync 送貨訂單(加單)from M18"}
- </Button>
- </Box>
- </Stack>
- {m18DoExtraSyncResult ? (
- <TextField
- fullWidth
- multiline
- minRows={4}
- margin="normal"
- label="Sync Result"
- value={m18DoExtraSyncResult}
- InputProps={{ readOnly: true }}
- />
- ) : null}
- </Section>
- </TabPanel>
-
- <TabPanel value={tabValue} index={3}>
- <Section title="M18 Product / material — sync by code">
- <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}>
- <TextField
- size="small"
- label="Item / product code"
- value={m18ProductCode}
- onChange={(e) => setM18ProductCode(e.target.value)}
- placeholder="e.g. PP1175 (M18 item code)"
- sx={{ minWidth: 320 }}
- />
- <Button
- variant="contained"
- color="primary"
- onClick={handleSyncM18ProductByCode}
- disabled={isSyncingM18Product}
- >
- {isSyncingM18Product ? "Syncing..." : "Sync product from M18"}
- </Button>
- </Stack>
- {m18ProductSyncResult ? (
- <TextField
- fullWidth
- multiline
- minRows={4}
- margin="normal"
- label="Sync Result"
- value={m18ProductSyncResult}
- InputProps={{ readOnly: true }}
- />
- ) : null}
- </Section>
- </TabPanel>
-
- </Box>
- );
- }
|