| @@ -29,6 +29,62 @@ function TabPanel(props: TabPanelProps) { | |||
| 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>(""); | |||
| @@ -65,7 +121,7 @@ export default function M18SynPage() { | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| setM18PoSyncResult(text); | |||
| setM18PoSyncResult(formatSyncResultText("採購單", text)); | |||
| if (!response.ok) { | |||
| alert(`Sync failed: ${response.status}`); | |||
| } | |||
| @@ -94,7 +150,7 @@ export default function M18SynPage() { | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| setM18DoSyncResult(text); | |||
| setM18DoSyncResult(formatSyncResultText("送貨訂單", text)); | |||
| if (!response.ok) { | |||
| alert(`Sync failed: ${response.status}`); | |||
| } | |||
| @@ -107,27 +163,60 @@ export default function M18SynPage() { | |||
| } | |||
| }; | |||
| /** DO(加單):M18 搜尋需含備註「(加單)」,本地 isEtra = true */ | |||
| /** DO(加單):手動按 code 同步,並寫入本地 isEtra=true(可輸入多個 code,用逗號或換行分隔) */ | |||
| const handleSyncM18DoExtraByCode = async () => { | |||
| if (m18DoExtraInFlightRef.current) return; | |||
| if (!m18DoExtraCode.trim()) { | |||
| alert("Please enter DO / shop PO code (加單)."); | |||
| 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 response = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/m18/test/do-by-code-extra?code=${encodeURIComponent(m18DoExtraCode.trim())}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| setM18DoExtraSyncResult(text); | |||
| if (!response.ok) { | |||
| alert(`Sync failed: ${response.status}`); | |||
| 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."); | |||
| @@ -153,7 +242,7 @@ export default function M18SynPage() { | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| setM18ProductSyncResult(text); | |||
| setM18ProductSyncResult(formatSyncResultText("產品", text)); | |||
| if (!response.ok) { | |||
| alert(`Sync failed: ${response.status}`); | |||
| } | |||
| @@ -185,14 +274,14 @@ export default function M18SynPage() { | |||
| </Typography> | |||
| <Tabs value={tabValue} onChange={(_, v) => setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth"> | |||
| <Tab label="1. PO" id="m18syn-tab-0" /> | |||
| <Tab label="2. DO" id="m18syn-tab-1" /> | |||
| <Tab label="3. DO (加單)" id="m18syn-tab-2" /> | |||
| <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 Purchase Order — sync by code"> | |||
| <Section title="M18 採購單 — sync by code"> | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> | |||
| <TextField | |||
| size="small" | |||
| @@ -221,7 +310,7 @@ export default function M18SynPage() { | |||
| </TabPanel> | |||
| <TabPanel value={tabValue} index={1}> | |||
| <Section title="M18 Delivery Order — sync by code"> | |||
| <Section title="M18 送貨訂單 — sync by code"> | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> | |||
| <TextField | |||
| size="small" | |||
| @@ -250,27 +339,28 @@ export default function M18SynPage() { | |||
| </TabPanel> | |||
| <TabPanel value={tabValue} index={2}> | |||
| <Section title="M18 Delivery Order — 加單 (備註含「(加單)」,isEtra)"> | |||
| <Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}> | |||
| 與「2. DO」相同以單號從 M18 同步 shop PO/送貨單,但 M18 列表條件會限制備註含「(加單)」;同步寫入之 delivery_order.isEtra = true。 | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }}> | |||
| <Section title="M18 送貨訂單 — 加單 (isEtra)"> | |||
| <Stack spacing={2} sx={{ mb: 2 }}> | |||
| <TextField | |||
| size="small" | |||
| label="DO / Shop PO Code(加單)" | |||
| value={m18DoExtraCode} | |||
| onChange={(e) => setM18DoExtraCode(e.target.value)} | |||
| placeholder="e.g. 與一般 DO 相同單號,但須為加單單據" | |||
| sx={{ minWidth: 320 }} | |||
| placeholder="可輸入多個 code,用逗號或換行分隔" | |||
| fullWidth | |||
| multiline | |||
| minRows={6} | |||
| maxRows={14} | |||
| /> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={handleSyncM18DoExtraByCode} | |||
| disabled={isSyncingM18DoExtra} | |||
| > | |||
| {isSyncingM18DoExtra ? "Syncing..." : "Sync DO(加單)from M18"} | |||
| </Button> | |||
| <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 | |||