| @@ -29,6 +29,62 @@ function TabPanel(props: TabPanelProps) { | |||||
| export default function M18SynPage() { | export default function M18SynPage() { | ||||
| const [tabValue, setTabValue] = useState(0); | 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 [m18PoCode, setM18PoCode] = useState(""); | ||||
| const [isSyncingM18Po, setIsSyncingM18Po] = useState(false); | const [isSyncingM18Po, setIsSyncingM18Po] = useState(false); | ||||
| const [m18PoSyncResult, setM18PoSyncResult] = useState<string>(""); | const [m18PoSyncResult, setM18PoSyncResult] = useState<string>(""); | ||||
| @@ -65,7 +121,7 @@ export default function M18SynPage() { | |||||
| ); | ); | ||||
| if (response.status === 401 || response.status === 403) return; | if (response.status === 401 || response.status === 403) return; | ||||
| const text = await response.text(); | const text = await response.text(); | ||||
| setM18PoSyncResult(text); | |||||
| setM18PoSyncResult(formatSyncResultText("採購單", text)); | |||||
| if (!response.ok) { | if (!response.ok) { | ||||
| alert(`Sync failed: ${response.status}`); | alert(`Sync failed: ${response.status}`); | ||||
| } | } | ||||
| @@ -94,7 +150,7 @@ export default function M18SynPage() { | |||||
| ); | ); | ||||
| if (response.status === 401 || response.status === 403) return; | if (response.status === 401 || response.status === 403) return; | ||||
| const text = await response.text(); | const text = await response.text(); | ||||
| setM18DoSyncResult(text); | |||||
| setM18DoSyncResult(formatSyncResultText("送貨訂單", text)); | |||||
| if (!response.ok) { | if (!response.ok) { | ||||
| alert(`Sync failed: ${response.status}`); | 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 () => { | const handleSyncM18DoExtraByCode = async () => { | ||||
| if (m18DoExtraInFlightRef.current) return; | 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; | return; | ||||
| } | } | ||||
| m18DoExtraInFlightRef.current = true; | m18DoExtraInFlightRef.current = true; | ||||
| setIsSyncingM18DoExtra(true); | setIsSyncingM18DoExtra(true); | ||||
| setM18DoExtraSyncResult(""); | setM18DoExtraSyncResult(""); | ||||
| try { | 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) { | } catch (e) { | ||||
| console.error("M18 DO (加單) Sync By Code Error:", e); | console.error("M18 DO (加單) Sync By Code Error:", e); | ||||
| alert("M18 DO (加單) sync failed. Check console/network."); | alert("M18 DO (加單) sync failed. Check console/network."); | ||||
| @@ -153,7 +242,7 @@ export default function M18SynPage() { | |||||
| ); | ); | ||||
| if (response.status === 401 || response.status === 403) return; | if (response.status === 401 || response.status === 403) return; | ||||
| const text = await response.text(); | const text = await response.text(); | ||||
| setM18ProductSyncResult(text); | |||||
| setM18ProductSyncResult(formatSyncResultText("產品", text)); | |||||
| if (!response.ok) { | if (!response.ok) { | ||||
| alert(`Sync failed: ${response.status}`); | alert(`Sync failed: ${response.status}`); | ||||
| } | } | ||||
| @@ -185,14 +274,14 @@ export default function M18SynPage() { | |||||
| </Typography> | </Typography> | ||||
| <Tabs value={tabValue} onChange={(_, v) => setTabValue(v)} aria-label="M18 sync by code" centered variant="fullWidth"> | <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" /> | <Tab label="4. Product" id="m18syn-tab-3" /> | ||||
| </Tabs> | </Tabs> | ||||
| <TabPanel value={tabValue} index={0}> | <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" }}> | <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> | ||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| @@ -221,7 +310,7 @@ export default function M18SynPage() { | |||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tabValue} index={1}> | <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" }}> | <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "center" }}> | ||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| @@ -250,27 +339,28 @@ export default function M18SynPage() { | |||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tabValue} index={2}> | <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 | <TextField | ||||
| size="small" | |||||
| label="DO / Shop PO Code(加單)" | label="DO / Shop PO Code(加單)" | ||||
| value={m18DoExtraCode} | value={m18DoExtraCode} | ||||
| onChange={(e) => setM18DoExtraCode(e.target.value)} | 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> | </Stack> | ||||
| {m18DoExtraSyncResult ? ( | {m18DoExtraSyncResult ? ( | ||||
| <TextField | <TextField | ||||