| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import React, { useEffect, useMemo, useState } from "react"; | |||||
| import React, { useEffect, useMemo, useRef, useState } from "react"; | |||||
| import { | import { | ||||
| Alert, | Alert, | ||||
| Box, | Box, | ||||
| @@ -94,6 +94,19 @@ export default function TestingPage() { | |||||
| const [laserAutoError, setLaserAutoError] = useState<string | null>(null); | const [laserAutoError, setLaserAutoError] = useState<string | null>(null); | ||||
| const [laserLastReceive, setLaserLastReceive] = | const [laserLastReceive, setLaserLastReceive] = | ||||
| useState<LaserLastReceiveSuccess | null>(null); | useState<LaserLastReceiveSuccess | null>(null); | ||||
| const bomShopSyncInFlightRef = useRef(false); | |||||
| const [bomShopSyncBomId, setBomShopSyncBomId] = useState("78"); | |||||
| const [bomShopM18HeaderId, setBomShopM18HeaderId] = useState(""); | |||||
| const [bomShopSyncLoading, setBomShopSyncLoading] = useState(false); | |||||
| const [bomShopSyncResult, setBomShopSyncResult] = useState<string | null>( | |||||
| null, | |||||
| ); | |||||
| const bomByItemCodeInFlightRef = useRef(false); | |||||
| const [bomByItemCodeInput, setBomByItemCodeInput] = useState(""); | |||||
| const [bomByItemCodeLoading, setBomByItemCodeLoading] = useState(false); | |||||
| const [bomByItemCodeResult, setBomByItemCodeResult] = useState<string | null>( | |||||
| null, | |||||
| ); | |||||
| const onpackPayload = useMemo( | const onpackPayload = useMemo( | ||||
| () => buildOnPackJobOrdersPayload(onpackJobOrders), | () => buildOnPackJobOrdersPayload(onpackJobOrders), | ||||
| @@ -259,6 +272,87 @@ export default function TestingPage() { | |||||
| } | } | ||||
| }; | }; | ||||
| const handleBomShopSyncM18 = async () => { | |||||
| if (bomShopSyncInFlightRef.current) return; | |||||
| const id = parseInt(bomShopSyncBomId.trim(), 10); | |||||
| if (!Number.isFinite(id) || id <= 0) { | |||||
| alert("Enter a valid BOM id (positive integer)."); | |||||
| return; | |||||
| } | |||||
| bomShopSyncInFlightRef.current = true; | |||||
| setBomShopSyncLoading(true); | |||||
| setBomShopSyncResult(null); | |||||
| try { | |||||
| const m18H = bomShopM18HeaderId.trim(); | |||||
| const qs = | |||||
| m18H && /^\d+$/.test(m18H) | |||||
| ? `?m18HeaderId=${encodeURIComponent(m18H)}` | |||||
| : ""; | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/m18/test/bom-shop-sync/${id}${qs}`, | |||||
| { method: "POST" }, | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const text = await response.text(); | |||||
| let display = text; | |||||
| try { | |||||
| const parsed: unknown = JSON.parse(text); | |||||
| display = JSON.stringify(parsed, null, 2); | |||||
| } catch { | |||||
| /* keep raw */ | |||||
| } | |||||
| if (!response.ok) { | |||||
| setBomShopSyncResult(`HTTP ${response.status}\n\n${display}`); | |||||
| return; | |||||
| } | |||||
| setBomShopSyncResult(display); | |||||
| } catch (e) { | |||||
| const msg = e instanceof Error ? e.message : String(e); | |||||
| setBomShopSyncResult(`Error: ${msg}`); | |||||
| } finally { | |||||
| setBomShopSyncLoading(false); | |||||
| bomShopSyncInFlightRef.current = false; | |||||
| } | |||||
| }; | |||||
| const handleBomLookupByItemCode = async () => { | |||||
| if (bomByItemCodeInFlightRef.current) return; | |||||
| const code = bomByItemCodeInput.trim(); | |||||
| if (!code) { | |||||
| alert("Enter an item code."); | |||||
| return; | |||||
| } | |||||
| bomByItemCodeInFlightRef.current = true; | |||||
| setBomByItemCodeLoading(true); | |||||
| setBomByItemCodeResult(null); | |||||
| try { | |||||
| const response = await clientAuthFetch( | |||||
| `${NEXT_PUBLIC_API_URL}/bom/by-item-code?code=${encodeURIComponent(code)}`, | |||||
| { method: "GET" }, | |||||
| ); | |||||
| if (response.status === 401 || response.status === 403) return; | |||||
| const text = await response.text(); | |||||
| let display = text; | |||||
| try { | |||||
| const parsed: unknown = JSON.parse(text); | |||||
| display = JSON.stringify(parsed, null, 2); | |||||
| } catch { | |||||
| /* keep raw */ | |||||
| } | |||||
| setBomByItemCodeResult(display); | |||||
| if (!response.ok) { | |||||
| alert(`Lookup failed: HTTP ${response.status}`); | |||||
| } | |||||
| } catch (e) { | |||||
| const msg = e instanceof Error ? e.message : String(e); | |||||
| setBomByItemCodeResult(`Error: ${msg}`); | |||||
| alert(msg); | |||||
| } finally { | |||||
| setBomByItemCodeLoading(false); | |||||
| bomByItemCodeInFlightRef.current = false; | |||||
| } | |||||
| }; | |||||
| const Section = ({ | const Section = ({ | ||||
| title, | title, | ||||
| children, | children, | ||||
| @@ -307,6 +401,7 @@ export default function TestingPage() { | |||||
| <Tab label="2. OnPack NGPCL" /> | <Tab label="2. OnPack NGPCL" /> | ||||
| <Tab label="3. Laser Bag2 自動送" /> | <Tab label="3. Laser Bag2 自動送" /> | ||||
| <Tab label="4. 批號標籤列印" /> | <Tab label="4. 批號標籤列印" /> | ||||
| <Tab label="5. M18 BOM shop" /> | |||||
| </Tabs> | </Tabs> | ||||
| <TabPanel value={tabValue} index={0}> | <TabPanel value={tabValue} index={0}> | ||||
| @@ -601,6 +696,127 @@ export default function TestingPage() { | |||||
| /> | /> | ||||
| </Section> | </Section> | ||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={tabValue} index={4}> | |||||
| <Section title="5. M18 BOM shop sync (udfBomForShop)"> | |||||
| <Alert severity="info" sx={{ mb: 2 }}> | |||||
| Requires setting <code>M18.bom.shop.sync.enabled=true</code>. Use{" "} | |||||
| <code>m18HeaderId</code> query param (or the field below) so M18{" "} | |||||
| <strong>updates</strong> the existing udfBomForShop header instead of creating a duplicate. | |||||
| </Alert> | |||||
| <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}> | |||||
| Lookup BOM id by item code (like PO-by-code) | |||||
| </Typography> | |||||
| <Typography variant="body2" color="textSecondary" sx={{ mb: 1 }}> | |||||
| Uses <code>GET /bom/by-item-code?code=…</code> — item is the BOM | |||||
| header product (<code>bom.item</code>), same as FPSMS finished-good | |||||
| <code> items.code</code>. | |||||
| </Typography> | |||||
| <Stack | |||||
| direction={{ xs: "column", sm: "row" }} | |||||
| spacing={2} | |||||
| sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }} | |||||
| > | |||||
| <TextField | |||||
| size="small" | |||||
| label="Item code" | |||||
| value={bomByItemCodeInput} | |||||
| onChange={(e) => setBomByItemCodeInput(e.target.value)} | |||||
| sx={{ width: 220 }} | |||||
| /> | |||||
| <Button | |||||
| variant="outlined" | |||||
| onClick={() => void handleBomLookupByItemCode()} | |||||
| disabled={bomByItemCodeLoading} | |||||
| > | |||||
| {bomByItemCodeLoading ? "Looking up…" : "Lookup BOM id"} | |||||
| </Button> | |||||
| </Stack> | |||||
| {bomByItemCodeResult ? ( | |||||
| <Stack spacing={1} sx={{ mb: 3 }}> | |||||
| <TextField | |||||
| fullWidth | |||||
| multiline | |||||
| minRows={4} | |||||
| label="Lookup response (BomIdByItemCodeResponse)" | |||||
| value={bomByItemCodeResult} | |||||
| InputProps={{ readOnly: true }} | |||||
| sx={{ fontFamily: "monospace" }} | |||||
| /> | |||||
| <Button | |||||
| size="small" | |||||
| variant="text" | |||||
| onClick={() => { | |||||
| try { | |||||
| const o = JSON.parse(bomByItemCodeResult) as { | |||||
| bomId?: number; | |||||
| bomM18Id?: number; | |||||
| }; | |||||
| if (o?.bomId != null) | |||||
| setBomShopSyncBomId(String(o.bomId)); | |||||
| if (o?.bomM18Id != null) | |||||
| setBomShopM18HeaderId(String(o.bomM18Id)); | |||||
| } catch { | |||||
| /* ignore */ | |||||
| } | |||||
| }} | |||||
| > | |||||
| Copy bomId + bomM18Id to sync fields below | |||||
| </Button> | |||||
| </Stack> | |||||
| ) : null} | |||||
| <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}> | |||||
| M18 udfBomForShop sync | |||||
| </Typography> | |||||
| <Typography variant="body2" color="textSecondary" sx={{ mb: 1 }}> | |||||
| POST <code>/m18/test/bom-shop-sync/:bomId</code> with optional{" "} | |||||
| <code>?m18HeaderId=</code> (M18 udfBomForShop header id for{" "} | |||||
| <strong>update</strong>; from lookup <code>bomM18Id</code> or{" "} | |||||
| <code>Bom.m18Id</code> in DB). If omitted, backend uses{" "} | |||||
| <code>bom.m18Id</code> when set; otherwise creates a new M18 row. | |||||
| </Typography> | |||||
| <Stack | |||||
| direction={{ xs: "column", sm: "row" }} | |||||
| spacing={2} | |||||
| sx={{ mb: 2, alignItems: "center", flexWrap: "wrap" }} | |||||
| > | |||||
| <TextField | |||||
| size="small" | |||||
| label="BOM id" | |||||
| value={bomShopSyncBomId} | |||||
| onChange={(e) => setBomShopSyncBomId(e.target.value)} | |||||
| sx={{ width: 160 }} | |||||
| /> | |||||
| <TextField | |||||
| size="small" | |||||
| label="M18 header id (optional, update)" | |||||
| value={bomShopM18HeaderId} | |||||
| onChange={(e) => setBomShopM18HeaderId(e.target.value)} | |||||
| sx={{ width: 220 }} | |||||
| helperText="e.g. 255 from bomM18Id — forces main.id in payload" | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={() => void handleBomShopSyncM18()} | |||||
| disabled={bomShopSyncLoading} | |||||
| > | |||||
| {bomShopSyncLoading ? "Syncing…" : "Sync BOM to M18"} | |||||
| </Button> | |||||
| </Stack> | |||||
| {bomShopSyncResult ? ( | |||||
| <TextField | |||||
| fullWidth | |||||
| multiline | |||||
| minRows={10} | |||||
| label="Response (M18BomShopSyncTriggerResult)" | |||||
| value={bomShopSyncResult} | |||||
| InputProps={{ readOnly: true }} | |||||
| sx={{ fontFamily: "monospace" }} | |||||
| /> | |||||
| ) : null} | |||||
| </Section> | |||||
| </TabPanel> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||