| @@ -90,6 +90,10 @@ export default function TestingPage() { | |||
| // --- 6. GRN Preview (M18) --- | |||
| const [grnPreviewReceiptDate, setGrnPreviewReceiptDate] = useState("2026-03-16"); | |||
| // --- 7. M18 PO Sync by Code --- | |||
| const [m18PoCode, setM18PoCode] = useState(""); | |||
| const [isSyncingM18Po, setIsSyncingM18Po] = useState(false); | |||
| const [m18PoSyncResult, setM18PoSyncResult] = useState<string>(""); | |||
| // Generic handler for inline table edits | |||
| const handleItemChange = (setter: any, id: number, field: string, value: string) => { | |||
| @@ -247,6 +251,33 @@ export default function TestingPage() { | |||
| } | |||
| }; | |||
| // M18 PO Sync By Code (Section 7) | |||
| const handleSyncM18PoByCode = async () => { | |||
| if (!m18PoCode.trim()) { | |||
| alert("Please enter PO code."); | |||
| return; | |||
| } | |||
| 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(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); | |||
| } | |||
| }; | |||
| // Layout Helper | |||
| const Section = ({ title, children }: { title: string, children?: React.ReactNode }) => ( | |||
| <Paper sx={{ p: 3, minHeight: '450px', display: 'flex', flexDirection: 'column' }}> | |||
| @@ -268,6 +299,7 @@ export default function TestingPage() { | |||
| <Tab label="4. Laser" /> | |||
| <Tab label="5. HANS600S-M" /> | |||
| <Tab label="6. GRN Preview" /> | |||
| <Tab label="7. M18 PO Sync" /> | |||
| </Tabs> | |||
| <TabPanel value={tabValue} index={0}> | |||
| @@ -523,6 +555,43 @@ export default function TestingPage() { | |||
| </Section> | |||
| </TabPanel> | |||
| <TabPanel value={tabValue} index={6}> | |||
| <Section title="7. M18 PO 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> | |||
| <Typography variant="body2" color="textSecondary"> | |||
| Backend endpoint: <code>/m18/test/po-by-code?code=YOUR_CODE</code> | |||
| </Typography> | |||
| {m18PoSyncResult ? ( | |||
| <TextField | |||
| fullWidth | |||
| multiline | |||
| minRows={4} | |||
| margin="normal" | |||
| label="Sync Result" | |||
| value={m18PoSyncResult} | |||
| InputProps={{ readOnly: true }} | |||
| /> | |||
| ) : null} | |||
| </Section> | |||
| </TabPanel> | |||
| {/* Dialog for OnPack */} | |||
| <Dialog open={isPrinterModalOpen} onClose={() => setIsPrinterModalOpen(false)} fullWidth maxWidth="sm"> | |||
| <DialogTitle sx={{ bgcolor: 'success.main', color: 'white' }}>OnPack Printer Job Details</DialogTitle> | |||
| @@ -27,6 +27,8 @@ import Print from "@mui/icons-material/Print"; | |||
| import Download from "@mui/icons-material/Download"; | |||
| import { checkPrinterStatus, downloadOnPackQrZip, fetchJobOrders, JobOrderListItem } from "@/app/api/bagPrint/actions"; | |||
| import dayjs from "dayjs"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| // Light blue theme (matching Python Bag1) | |||
| const BG_TOP = "#E8F4FC"; | |||
| @@ -94,6 +96,11 @@ const BagPrintSearch: React.FC = () => { | |||
| const [connected, setConnected] = useState(false); | |||
| const [printer, setPrinter] = useState<string>("dataflex"); | |||
| const [selectedId, setSelectedId] = useState<number | null>(null); | |||
| const [printDialogOpen, setPrintDialogOpen] = useState(false); | |||
| const [printTarget, setPrintTarget] = useState<JobOrderListItem | null>(null); | |||
| const [printCount, setPrintCount] = useState(0); | |||
| const [printContinuous, setPrintContinuous] = useState(false); | |||
| const [printing, setPrinting] = useState(false); | |||
| const [settingsOpen, setSettingsOpen] = useState(false); | |||
| const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity?: "success" | "info" | "error" }>({ open: false, message: "" }); | |||
| const [settings, setSettings] = useState(DEFAULT_SETTINGS); | |||
| @@ -187,8 +194,76 @@ const BagPrintSearch: React.FC = () => { | |||
| const itemCode = jo.itemCode || "—"; | |||
| const itemName = jo.itemName || "—"; | |||
| setSnackbar({ open: true, message: `已點選:批次 ${batch} 品號 ${itemCode} ${itemName}`, severity: "info" }); | |||
| // TODO: Actual printing would require backend API to proxy to printer (TCP/serial) | |||
| // For now, show info. Backend could add /py/print-dataflex, /py/print-label, /py/print-laser | |||
| // Align with Bag2.py "click row -> ask bag count -> print" for DataFlex. | |||
| if (printer === "dataflex") { | |||
| setPrintTarget(jo); | |||
| setPrintCount(0); | |||
| setPrintContinuous(false); | |||
| setPrintDialogOpen(true); | |||
| } | |||
| }; | |||
| const confirmPrintDataFlex = async () => { | |||
| if (!printTarget) return; | |||
| if (printer !== "dataflex") { | |||
| setSnackbar({ open: true, message: "此頁目前只支援打袋機 DataFlex 列印", severity: "error" }); | |||
| return; | |||
| } | |||
| if (!printContinuous && printCount < 1) { | |||
| setSnackbar({ open: true, message: "請先按 +50、+10、+5 或 +1 選擇數量。", severity: "error" }); | |||
| return; | |||
| } | |||
| const qty = printContinuous ? -1 : printCount; | |||
| const printerIp = settings.dabag_ip; | |||
| const printerPort = Number(settings.dabag_port || 3008); | |||
| if (!printerIp) { | |||
| setSnackbar({ open: true, message: "請先在設定中填寫打袋機 DataFlex 的 IP。", severity: "error" }); | |||
| return; | |||
| } | |||
| setPrinting(true); | |||
| try { | |||
| const resp = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify({ | |||
| itemCode: printTarget.itemCode || "—", | |||
| itemName: printTarget.itemName || "—", | |||
| lotNo: printTarget.lotNo || "—", | |||
| // DataFlex zpl (Bag2.py) only needs itemId + stockInLineId for QR payload (optional). | |||
| itemId: printTarget.itemId, | |||
| stockInLineId: printTarget.stockInLineId, | |||
| printerIp, | |||
| printerPort, | |||
| printQty: qty, | |||
| }), | |||
| }); | |||
| if (resp.status === 401 || resp.status === 403) return; | |||
| if (!resp.ok) { | |||
| const msg = await resp.text().catch(() => ""); | |||
| setSnackbar({ | |||
| open: true, | |||
| message: `DataFlex 列印失敗(狀態碼 ${resp.status})。${msg ? msg.slice(0, 120) : ""}`, | |||
| severity: "error", | |||
| }); | |||
| return; | |||
| } | |||
| const batch = getBatch(printTarget); | |||
| const printedText = qty === -1 ? "連續 (C)" : `${qty}`; | |||
| setSnackbar({ open: true, message: `已送出列印:批次 ${batch} x ${printedText}`, severity: "success" }); | |||
| setPrintDialogOpen(false); | |||
| } catch (e) { | |||
| setSnackbar({ open: true, message: e instanceof Error ? e.message : "DataFlex 列印失敗", severity: "error" }); | |||
| } finally { | |||
| setPrinting(false); | |||
| } | |||
| }; | |||
| const handleDownloadOnPackQr = async () => { | |||
| @@ -374,6 +449,85 @@ const BagPrintSearch: React.FC = () => { | |||
| )} | |||
| </Paper> | |||
| {/* Print count dialog (DataFlex) */} | |||
| <Dialog open={printDialogOpen} onClose={() => (printing ? null : setPrintDialogOpen(false))} maxWidth="xs" fullWidth> | |||
| <DialogTitle>打袋機 DataFlex 列印數量</DialogTitle> | |||
| <DialogContent> | |||
| <Stack spacing={2} sx={{ mt: 1 }}> | |||
| <Typography variant="body1" sx={{ fontWeight: 700 }}> | |||
| 列印多少個袋? | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {printContinuous ? "連續 (C)" : `數量: ${printCount}`} | |||
| </Typography> | |||
| <Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap"> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => { | |||
| setPrintContinuous(false); | |||
| setPrintCount((c) => c + 50); | |||
| }} | |||
| disabled={printing} | |||
| > | |||
| +50 | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => { | |||
| setPrintContinuous(false); | |||
| setPrintCount((c) => c + 10); | |||
| }} | |||
| disabled={printing} | |||
| > | |||
| +10 | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => { | |||
| setPrintContinuous(false); | |||
| setPrintCount((c) => c + 5); | |||
| }} | |||
| disabled={printing} | |||
| > | |||
| +5 | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| onClick={() => { | |||
| setPrintContinuous(false); | |||
| setPrintCount((c) => c + 1); | |||
| }} | |||
| disabled={printing} | |||
| > | |||
| +1 | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| variant={printContinuous ? "contained" : "outlined"} | |||
| onClick={() => { | |||
| setPrintContinuous(true); | |||
| }} | |||
| disabled={printing} | |||
| > | |||
| 連續 (C) | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={() => setPrintDialogOpen(false)} disabled={printing}> | |||
| 取消 | |||
| </Button> | |||
| <Button variant="contained" onClick={() => void confirmPrintDataFlex()} disabled={printing}> | |||
| {printing ? <CircularProgress size={16} /> : "確認送出"} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| {/* Settings dialog */} | |||
| <Dialog open={settingsOpen} onClose={() => setSettingsOpen(false)} maxWidth="sm" fullWidth> | |||
| <DialogTitle>設定</DialogTitle> | |||
| @@ -7,7 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { Button, Grid, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||
| import { Backdrop, Button, CircularProgress, Grid, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||
| import QrModal from "../PoDetail/QrModal"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import NotificationIcon from "@mui/icons-material/NotificationImportant"; | |||
| @@ -18,6 +18,8 @@ import dayjs from "dayjs"; | |||
| import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { Checkbox, Box } from "@mui/material"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | |||
| dayjs.extend(arraySupport); | |||
| type Props = { | |||
| @@ -263,6 +265,9 @@ const PoSearch: React.FC<Props> = ({ | |||
| }, [po]); | |||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||
| const [autoSyncStatus, setAutoSyncStatus] = useState<string | null>(null); | |||
| const [isM18LookupLoading, setIsM18LookupLoading] = useState(false); | |||
| const autoSyncInProgressRef = React.useRef(false); | |||
| const onOpenScanner = useCallback(() => { | |||
| setOpenScanner(true); | |||
| }, []); | |||
| @@ -282,12 +287,96 @@ const PoSearch: React.FC<Props> = ({ | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| }; | |||
| setAutoSyncStatus(null); | |||
| const res = await fetchPoListClient(params); | |||
| // const res = await testing(params); | |||
| if (res) { | |||
| console.log(res); | |||
| if (!res) return; | |||
| if (res.records && res.records.length > 0) { | |||
| setFilteredPo(res.records); | |||
| setTotalCount(res.total); | |||
| return; | |||
| } | |||
| const searchedCodeRaw = (filterArgs as any)?.code; | |||
| const searchedCode = | |||
| typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : ""; | |||
| const shouldAutoSyncFromM18 = | |||
| searchedCode.length > 14 && | |||
| (searchedCode.startsWith("PP") || searchedCode.startsWith("PF")); | |||
| if (!shouldAutoSyncFromM18 || autoSyncInProgressRef.current) { | |||
| setFilteredPo(res.records); | |||
| setTotalCount(res.total); | |||
| return; | |||
| } | |||
| try { | |||
| autoSyncInProgressRef.current = true; | |||
| setIsM18LookupLoading(true); | |||
| setAutoSyncStatus("正在從M18找尋PO..."); | |||
| const syncResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent( | |||
| searchedCode, | |||
| )}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (!syncResp.ok) { | |||
| throw new Error(`M18 sync failed: ${syncResp.status}`); | |||
| } | |||
| let syncJson: any = null; | |||
| try { | |||
| syncJson = await syncResp.json(); | |||
| } catch { | |||
| // Some endpoints may respond with plain text | |||
| const txt = await syncResp.text(); | |||
| syncJson = { raw: txt }; | |||
| } | |||
| const syncOk = Boolean(syncJson?.totalSuccess && syncJson.totalSuccess > 0); | |||
| if (syncOk) { | |||
| setAutoSyncStatus("成功找到PO"); | |||
| // Re-fetch /po/list directly from client to avoid cached server action results. | |||
| const cleanedQuery: Record<string, string> = {}; | |||
| Object.entries(params).forEach(([k, v]) => { | |||
| if (v === undefined || v === null) return; | |||
| if (typeof v === "string" && v.trim() === "") return; | |||
| cleanedQuery[k] = String(v); | |||
| }); | |||
| const listResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | |||
| cleanedQuery, | |||
| ).toString()}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (listResp.ok) { | |||
| const listJson = await listResp.json(); | |||
| setFilteredPo(listJson.records ?? []); | |||
| setTotalCount(listJson.total ?? 0); | |||
| setAutoSyncStatus("成功找到PO"); | |||
| return; | |||
| } | |||
| setAutoSyncStatus("找不到PO"); | |||
| } else { | |||
| setAutoSyncStatus("找不到PO"); | |||
| } | |||
| // Ensure UI updates even if sync didn't change results | |||
| setFilteredPo(res.records); | |||
| setTotalCount(res.total ?? 0); | |||
| } catch (e) { | |||
| console.error("Auto sync error:", e); | |||
| setAutoSyncStatus("找不到PO"); | |||
| setFilteredPo(res.records); | |||
| setTotalCount(res.total ?? 0); | |||
| } finally { | |||
| setIsM18LookupLoading(false); | |||
| autoSyncInProgressRef.current = false; | |||
| } | |||
| }, | |||
| [], | |||
| @@ -328,26 +417,43 @@ const PoSearch: React.FC<Props> = ({ | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| disabled={isM18LookupLoading} | |||
| onSearch={(query) => { | |||
| if (isM18LookupLoading) return; | |||
| console.log(query); | |||
| setFilterArgs({ | |||
| code: query.code, | |||
| supplier: query.supplier, | |||
| status: query.status === "All" ? "" : query.status, | |||
| escalated: | |||
| query.escalated === "All" | |||
| ? undefined | |||
| : query.escalated === t("Escalated"), | |||
| estimatedArrivalDate: query.estimatedArrivalDate === "Invalid Date" ? "" : query.estimatedArrivalDate, | |||
| estimatedArrivalDateTo: query.estimatedArrivalDateTo === "Invalid Date" ? "" : query.estimatedArrivalDateTo, | |||
| orderDate: query.orderDate === "Invalid Date" ? "" : query.orderDate, | |||
| orderDateTo: query.orderDateTo === "Invalid Date" ? "" : query.orderDateTo, | |||
| }); | |||
| const code = typeof query.code === "string" ? query.code.trim() : ""; | |||
| if (code) { | |||
| // When PO code is provided, ignore other search criteria (especially date ranges). | |||
| setFilterArgs({ code }); | |||
| } else { | |||
| setFilterArgs({ | |||
| code: query.code, | |||
| supplier: query.supplier, | |||
| status: query.status === "All" ? "" : query.status, | |||
| escalated: | |||
| query.escalated === "All" | |||
| ? undefined | |||
| : query.escalated === t("Escalated"), | |||
| estimatedArrivalDate: query.estimatedArrivalDate === "Invalid Date" ? "" : query.estimatedArrivalDate, | |||
| estimatedArrivalDateTo: query.estimatedArrivalDateTo === "Invalid Date" ? "" : query.estimatedArrivalDateTo, | |||
| orderDate: query.orderDate === "Invalid Date" ? "" : query.orderDate, | |||
| orderDateTo: query.orderDateTo === "Invalid Date" ? "" : query.orderDateTo, | |||
| }); | |||
| } | |||
| setSelectedPoIds([]); // reset selected po ids | |||
| setSelectAll(false); // reset select all | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| {autoSyncStatus ? ( | |||
| <Typography | |||
| variant="body2" | |||
| color={isM18LookupLoading ? "warning.main" : "text.secondary"} | |||
| sx={{ mb: 1 }} | |||
| > | |||
| {autoSyncStatus} | |||
| </Typography> | |||
| ) : null} | |||
| <SearchResults<PoResult> | |||
| items={filteredPo} | |||
| columns={columns} | |||
| @@ -374,6 +480,13 @@ const PoSearch: React.FC<Props> = ({ | |||
| {t("View Selected")} ({selectedPoIds.length}) | |||
| </Button> | |||
| </Box> | |||
| <Backdrop | |||
| open={isM18LookupLoading} | |||
| sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.modal + 1, flexDirection: "column", gap: 1 }} | |||
| > | |||
| <CircularProgress color="inherit" /> | |||
| <Typography variant="body1">正在從M18找尋PO...</Typography> | |||
| </Backdrop> | |||
| </> | |||
| </> | |||
| ); | |||
| @@ -122,6 +122,8 @@ interface Props<T extends string> { | |||
| onReset?: () => void; | |||
| /** Optional actions rendered in the same row as Reset/Search (e.g. Download, Upload buttons) */ | |||
| extraActions?: React.ReactNode; | |||
| /** Disable inputs/actions while external task is running */ | |||
| disabled?: boolean; | |||
| } | |||
| function SearchBox<T extends string>({ | |||
| @@ -129,6 +131,7 @@ function SearchBox<T extends string>({ | |||
| onSearch, | |||
| onReset, | |||
| extraActions, | |||
| disabled = false, | |||
| }: Props<T>) { | |||
| const { t } = useTranslation("common"); | |||
| const defaultAll: AutocompleteOptions = { | |||
| @@ -295,6 +298,7 @@ function SearchBox<T extends string>({ | |||
| placeholder={c.placeholder} | |||
| onChange={makeInputChangeHandler(c.paramName)} | |||
| value={inputs[c.paramName]} | |||
| disabled={disabled} | |||
| /> | |||
| )} | |||
| {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} | |||
| @@ -314,6 +318,7 @@ function SearchBox<T extends string>({ | |||
| label={t(c.label)} | |||
| onChange={makeSelectChangeHandler(c.paramName)} | |||
| value={inputs[c.paramName] ?? "All"} | |||
| disabled={disabled} | |||
| > | |||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||
| {c.options.map((option) => ( | |||
| @@ -331,6 +336,7 @@ function SearchBox<T extends string>({ | |||
| label={t(c.label)} | |||
| onChange={makeSelectChangeHandler(c.paramName)} | |||
| value={inputs[c.paramName] ?? "All"} | |||
| disabled={disabled} | |||
| > | |||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||
| {c.options.map((option) => ( | |||
| @@ -356,6 +362,7 @@ function SearchBox<T extends string>({ | |||
| noOptionsText={c.noOptionsText ?? t("No options")} | |||
| disableClearable | |||
| fullWidth | |||
| disabled={disabled} | |||
| value={ | |||
| c.multiple | |||
| ? intersectionWith( | |||
| @@ -455,6 +462,7 @@ function SearchBox<T extends string>({ | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| value={ | |||
| dayjs(inputs[c.paramName]).isValid() | |||
| ? dayjs(inputs[c.paramName]) | |||
| @@ -475,6 +483,7 @@ function SearchBox<T extends string>({ | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={c.label2 ? t(c.label2) : null} | |||
| onChange={makeDateToChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| value={ | |||
| dayjs(inputs[`${c.paramName}To`]).isValid() | |||
| ? dayjs(inputs[`${c.paramName}To`]) | |||
| @@ -498,6 +507,7 @@ function SearchBox<T extends string>({ | |||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDatetimeChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| value={ | |||
| dayjs(inputs[c.paramName]).isValid() | |||
| ? dayjs(inputs[c.paramName]) | |||
| @@ -519,6 +529,7 @@ function SearchBox<T extends string>({ | |||
| format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||
| label={c.label2 ? t(c.label2) : null} | |||
| onChange={makeDatetimeToChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| value={ | |||
| dayjs(inputs[`${c.paramName}To`]).isValid() | |||
| ? dayjs(inputs[`${c.paramName}To`]) | |||
| @@ -541,6 +552,7 @@ function SearchBox<T extends string>({ | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| label={t(c.label)} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| /> | |||
| </FormControl> | |||
| </Box> | |||
| @@ -556,6 +568,7 @@ function SearchBox<T extends string>({ | |||
| format="HH:mm" | |||
| label={t(c.label)} | |||
| onChange={makeTimeChangeHandler(c.paramName)} | |||
| disabled={disabled} | |||
| value={ | |||
| inputs[c.paramName] && dayjs(inputs[c.paramName], "HH:mm").isValid() | |||
| ? dayjs(inputs[c.paramName], "HH:mm") | |||
| @@ -574,6 +587,7 @@ function SearchBox<T extends string>({ | |||
| variant="outlined" | |||
| startIcon={<RestartAlt />} | |||
| onClick={handleReset} | |||
| disabled={disabled} | |||
| sx={{ borderColor: "#e2e8f0", color: "#334155" }} | |||
| > | |||
| {t("Reset")} | |||
| @@ -583,6 +597,7 @@ function SearchBox<T extends string>({ | |||
| color="primary" | |||
| startIcon={<Search />} | |||
| onClick={handleSearch} | |||
| disabled={disabled} | |||
| > | |||
| {t("Search")} | |||
| </Button> | |||