| @@ -112,6 +112,22 @@ export default function TestingPage() { | |||
| const [bomByItemCodeResult, setBomByItemCodeResult] = useState<string | null>( | |||
| null, | |||
| ); | |||
| const whatsAppTestInFlightRef = useRef(false); | |||
| const [whatsAppVar1, setWhatsAppVar1] = useState("12/1"); | |||
| const [whatsAppVar2, setWhatsAppVar2] = useState("3pm"); | |||
| const [whatsAppTestLoading, setWhatsAppTestLoading] = useState(false); | |||
| const [whatsAppTestResult, setWhatsAppTestResult] = useState<string | null>( | |||
| null, | |||
| ); | |||
| const emailTestInFlightRef = useRef(false); | |||
| const [emailTestSubject, setEmailTestSubject] = useState( | |||
| "FPSMS M18 sync alert [TEST]", | |||
| ); | |||
| const [emailTestMessage, setEmailTestMessage] = useState( | |||
| "FPSMS sync alert test message from /testing page.", | |||
| ); | |||
| const [emailTestLoading, setEmailTestLoading] = useState(false); | |||
| const [emailTestResult, setEmailTestResult] = useState<string | null>(null); | |||
| const onpackPayload = useMemo( | |||
| () => buildOnPackJobOrdersPayload(onpackJobOrders), | |||
| @@ -353,6 +369,70 @@ export default function TestingPage() { | |||
| } | |||
| }; | |||
| const handleWhatsAppSyncAlertTest = async () => { | |||
| if (whatsAppTestInFlightRef.current) return; | |||
| whatsAppTestInFlightRef.current = true; | |||
| setWhatsAppTestLoading(true); | |||
| setWhatsAppTestResult(null); | |||
| try { | |||
| const params = new URLSearchParams(); | |||
| const v1 = whatsAppVar1.trim(); | |||
| const v2 = whatsAppVar2.trim(); | |||
| if (v1) params.set("var1", v1); | |||
| if (v2) params.set("var2", v2); | |||
| const qs = params.toString(); | |||
| const response = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/scheduler/trigger/sync-alert-test-whatsapp${qs ? `?${qs}` : ""}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| if (!response.ok) { | |||
| setWhatsAppTestResult(`HTTP ${response.status}\n\n${text}`); | |||
| return; | |||
| } | |||
| setWhatsAppTestResult(text); | |||
| } catch (e) { | |||
| const msg = e instanceof Error ? e.message : String(e); | |||
| setWhatsAppTestResult(`Error: ${msg}`); | |||
| } finally { | |||
| setWhatsAppTestLoading(false); | |||
| whatsAppTestInFlightRef.current = false; | |||
| } | |||
| }; | |||
| const handleSyncAlertTestEmail = async () => { | |||
| if (emailTestInFlightRef.current) return; | |||
| emailTestInFlightRef.current = true; | |||
| setEmailTestLoading(true); | |||
| setEmailTestResult(null); | |||
| try { | |||
| const params = new URLSearchParams(); | |||
| const subj = emailTestSubject.trim(); | |||
| const msg = emailTestMessage.trim(); | |||
| if (subj) params.set("subject", subj); | |||
| if (msg) params.set("message", msg); | |||
| const qs = params.toString(); | |||
| const response = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/scheduler/trigger/sync-alert-test-email${qs ? `?${qs}` : ""}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (response.status === 401 || response.status === 403) return; | |||
| const text = await response.text(); | |||
| if (!response.ok) { | |||
| setEmailTestResult(`HTTP ${response.status}\n\n${text}`); | |||
| return; | |||
| } | |||
| setEmailTestResult(text); | |||
| } catch (e) { | |||
| const msg = e instanceof Error ? e.message : String(e); | |||
| setEmailTestResult(`Error: ${msg}`); | |||
| } finally { | |||
| setEmailTestLoading(false); | |||
| emailTestInFlightRef.current = false; | |||
| } | |||
| }; | |||
| const handleBomLookupByItemCode = async () => { | |||
| if (bomByItemCodeInFlightRef.current) return; | |||
| const code = bomByItemCodeInput.trim(); | |||
| @@ -440,6 +520,7 @@ export default function TestingPage() { | |||
| <Tab label="3. Laser Bag2 自動送" /> | |||
| <Tab label="4. 批號標籤列印" /> | |||
| <Tab label="5. M18 BOM shop" /> | |||
| <Tab label="6. Sync alert email" /> | |||
| </Tabs> | |||
| <TabPanel value={tabValue} index={0}> | |||
| @@ -888,6 +969,65 @@ export default function TestingPage() { | |||
| ) : null} | |||
| </Section> | |||
| </TabPanel> | |||
| <TabPanel value={tabValue} index={5}> | |||
| <Section title="6. M18 sync alert email"> | |||
| <Alert severity="info" sx={{ mb: 2 }}> | |||
| Production sync errors email{" "} | |||
| <strong>[email protected]</strong> and{" "} | |||
| <strong>[email protected]</strong> (see{" "} | |||
| <code>scheduler.sync-alert.email.to-addresses</code>). WhatsApp/Twilio | |||
| is disabled. SMTP from DB <code>MAIL.smtp.*</code> (e.g. Gmail{" "} | |||
| <code>[email protected]</code> + app password). | |||
| </Alert> | |||
| <Stack spacing={2} sx={{ mb: 2, maxWidth: 720 }}> | |||
| <TextField | |||
| size="small" | |||
| label="Email subject" | |||
| value={emailTestSubject} | |||
| onChange={(e) => setEmailTestSubject(e.target.value)} | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| label="Test message" | |||
| value={emailTestMessage} | |||
| onChange={(e) => setEmailTestMessage(e.target.value)} | |||
| multiline | |||
| minRows={3} | |||
| fullWidth | |||
| /> | |||
| <Stack direction="row" spacing={2} sx={{ flexWrap: "wrap" }}> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => void handleSyncAlertTestEmail()} | |||
| disabled={emailTestLoading} | |||
| > | |||
| {emailTestLoading ? "Sending…" : "Send test email message"} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| {emailTestResult ? ( | |||
| <TextField | |||
| fullWidth | |||
| multiline | |||
| minRows={2} | |||
| label="Email test response" | |||
| value={emailTestResult} | |||
| InputProps={{ readOnly: true }} | |||
| sx={{ fontFamily: "monospace", mb: 3 }} | |||
| /> | |||
| ) : null} | |||
| <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}> | |||
| <code> | |||
| GET /scheduler/trigger/sync-alert-test-email?subject=…&message=… | |||
| </code> | |||
| <br /> | |||
| <code>GET /scheduler/trigger/sync-alert-check</code> — run alert rules | |||
| now (empty = OK) | |||
| </Typography> | |||
| </Section> | |||
| </TabPanel> | |||
| </Box> | |||
| ); | |||
| } | |||