| @@ -0,0 +1,22 @@ | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import { Suspense } from "react"; | |||||
| import CreatePrinter from "@/components/CreatePrinter"; | |||||
| const CreatePrinterPage: React.FC = async () => { | |||||
| const { t } = await getServerI18n("common"); | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Create Printer") || "新增列印機"}</Typography> | |||||
| <I18nProvider namespaces={["common"]}> | |||||
| <Suspense fallback={<CreatePrinter.Loading />}> | |||||
| <CreatePrinter /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreatePrinterPage; | |||||
| @@ -0,0 +1,38 @@ | |||||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import isString from "lodash/isString"; | |||||
| import { notFound } from "next/navigation"; | |||||
| import { Suspense } from "react"; | |||||
| import EditPrinter from "@/components/EditPrinter"; | |||||
| import { fetchPrinterDetails } from "@/app/api/settings/printer/actions"; | |||||
| type Props = {} & SearchParams; | |||||
| const EditPrinterPage: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("common"); | |||||
| const id = isString(searchParams["id"]) | |||||
| ? parseInt(searchParams["id"]) | |||||
| : undefined; | |||||
| if (!id) { | |||||
| notFound(); | |||||
| } | |||||
| const printer = await fetchPrinterDetails(id); | |||||
| if (!printer) { | |||||
| notFound(); | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Edit")} {t("Printer")}</Typography> | |||||
| <I18nProvider namespaces={["common"]}> | |||||
| <Suspense fallback={<div>Loading...</div>}> | |||||
| <EditPrinter printer={printer} /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default EditPrinterPage; | |||||
| @@ -15,6 +15,7 @@ export interface PrinterInputs { | |||||
| description?: string; | description?: string; | ||||
| ip?: string; | ip?: string; | ||||
| port?: number; | port?: number; | ||||
| dpi?: number; | |||||
| } | } | ||||
| export const fetchPrinterDetails = async (id: number) => { | export const fetchPrinterDetails = async (id: number) => { | ||||
| @@ -51,3 +52,9 @@ export const deletePrinter = async (id: number) => { | |||||
| revalidateTag("printers"); | revalidateTag("printers"); | ||||
| return result; | return result; | ||||
| }; | }; | ||||
| export const fetchPrinterDescriptions = async () => { | |||||
| return serverFetchJson<string[]>(`${BASE_API_URL}/printers/descriptions`, { | |||||
| next: { tags: ["printers"] }, | |||||
| }); | |||||
| }; | |||||
| @@ -24,6 +24,7 @@ export interface PrinterResult { | |||||
| description?: string; | description?: string; | ||||
| ip?: string; | ip?: string; | ||||
| port?: number; | port?: number; | ||||
| dpi?: number; | |||||
| } | } | ||||
| export const fetchPrinterCombo = cache(async () => { | export const fetchPrinterCombo = cache(async () => { | ||||
| @@ -36,4 +37,10 @@ export const fetchPrinters = cache(async () => { | |||||
| return serverFetchJson<PrinterResult[]>(`${BASE_API_URL}/printers`, { | return serverFetchJson<PrinterResult[]>(`${BASE_API_URL}/printers`, { | ||||
| next: { tags: ["printers"] }, | next: { tags: ["printers"] }, | ||||
| }); | }); | ||||
| }); | |||||
| export const fetchPrinterDescriptions = cache(async () => { | |||||
| return serverFetchJson<string[]>(`${BASE_API_URL}/printers/descriptions`, { | |||||
| next: { tags: ["printers"] }, | |||||
| }); | |||||
| }); | }); | ||||
| @@ -41,10 +41,30 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||||
| case 401: | case 401: | ||||
| signOutUser(); | signOutUser(); | ||||
| default: | default: | ||||
| const errorText = await response.text(); | |||||
| console.error(`Server error (${response.status}):`, errorText); | |||||
| let errorMessage = "Something went wrong fetching data in server."; | |||||
| try { | |||||
| const contentType = response.headers.get("content-type"); | |||||
| if (contentType && contentType.includes("application/json")) { | |||||
| const errorJson = await response.json(); | |||||
| if (errorJson.error) { | |||||
| errorMessage = errorJson.error; | |||||
| } else if (errorJson.message) { | |||||
| errorMessage = errorJson.message; | |||||
| } else if (errorJson.traceId) { | |||||
| errorMessage = `Error occurred (traceId: ${errorJson.traceId}). Check server logs for details.`; | |||||
| } | |||||
| } else { | |||||
| const errorText = await response.text(); | |||||
| if (errorText && errorText.trim()) { | |||||
| errorMessage = errorText; | |||||
| } | |||||
| } | |||||
| } catch (e) { | |||||
| console.error("Error parsing error response:", e); | |||||
| } | |||||
| console.error(`Server error (${response.status}):`, errorMessage); | |||||
| throw new ServerFetchError( | throw new ServerFetchError( | ||||
| `Server error: ${response.status} ${response.statusText}. ${errorText || "Something went wrong fetching data in server."}`, | |||||
| `Server error: ${response.status} ${response.statusText}. ${errorMessage}`, | |||||
| response | response | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -74,7 +94,7 @@ type FetchParams = Parameters<typeof fetch>; | |||||
| export async function serverFetchJson<T>(...args: FetchParams) { | export async function serverFetchJson<T>(...args: FetchParams) { | ||||
| const response = await serverFetch(...args); | const response = await serverFetch(...args); | ||||
| console.log(response.status); | |||||
| console.log("serverFetchJson - Status:", response.status, "URL:", args[0]); | |||||
| if (response.ok) { | if (response.ok) { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| return response.status as T; | return response.status as T; | ||||
| @@ -82,12 +102,14 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||||
| return response.json() as T; | return response.json() as T; | ||||
| } else { | } else { | ||||
| const errorText = await response.text().catch(() => "Unable to read error response"); | |||||
| console.error("serverFetchJson - Error response:", response.status, errorText); | |||||
| switch (response.status) { | switch (response.status) { | ||||
| case 401: | case 401: | ||||
| signOutUser(); | signOutUser(); | ||||
| default: | default: | ||||
| throw new ServerFetchError( | throw new ServerFetchError( | ||||
| "Something went wrong fetching data in server.", | |||||
| `Server error: ${response.status} ${response.statusText}. ${errorText}`, | |||||
| response, | response, | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -21,6 +21,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
| "/settings/shop": "ShopAndTruck", | "/settings/shop": "ShopAndTruck", | ||||
| "/settings/shop/detail": "Shop Detail", | "/settings/shop/detail": "Shop Detail", | ||||
| "/settings/shop/truckdetail": "Truck Lane Detail", | "/settings/shop/truckdetail": "Truck Lane Detail", | ||||
| "/settings/printer": "Printer", | |||||
| "/scheduling/rough": "Demand Forecast", | "/scheduling/rough": "Demand Forecast", | ||||
| "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", | "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", | ||||
| "/scheduling/detailed": "Detail Scheduling", | "/scheduling/detailed": "Detail Scheduling", | ||||
| @@ -0,0 +1,220 @@ | |||||
| "use client"; | |||||
| import { createPrinter, PrinterInputs, fetchPrinterDescriptions } from "@/app/api/settings/printer/actions"; | |||||
| import { successDialog } from "@/components/Swal/CustomAlerts"; | |||||
| import { ArrowBack, Check } from "@mui/icons-material"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Button, | |||||
| FormControl, | |||||
| Grid, | |||||
| InputLabel, | |||||
| MenuItem, | |||||
| Select, | |||||
| SelectChangeEvent, | |||||
| Stack, | |||||
| TextField, | |||||
| } from "@mui/material"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| const CreatePrinter: React.FC = () => { | |||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | |||||
| const [isSubmitting, setIsSubmitting] = useState(false); | |||||
| const [descriptions, setDescriptions] = useState<string[]>([]); | |||||
| const [formData, setFormData] = useState<PrinterInputs>({ | |||||
| name: "", | |||||
| ip: "", | |||||
| port: undefined, | |||||
| type: "A4", | |||||
| dpi: undefined, | |||||
| description: "", | |||||
| }); | |||||
| useEffect(() => { | |||||
| const loadDescriptions = async () => { | |||||
| try { | |||||
| const descs = await fetchPrinterDescriptions(); | |||||
| setDescriptions(descs); | |||||
| } catch (error) { | |||||
| console.error("Failed to load descriptions:", error); | |||||
| } | |||||
| }; | |||||
| loadDescriptions(); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| if (formData.type !== "Label") { | |||||
| setFormData((prev) => ({ ...prev, dpi: undefined })); | |||||
| } | |||||
| }, [formData.type]); | |||||
| const handleChange = useCallback((field: keyof PrinterInputs) => { | |||||
| return (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const value = e.target.value; | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| [field]: | |||||
| field === "port" || field === "dpi" | |||||
| ? value === "" | |||||
| ? undefined | |||||
| : parseInt(value, 10) | |||||
| : value, | |||||
| })); | |||||
| }; | |||||
| }, []); | |||||
| const handleTypeChange = useCallback((e: SelectChangeEvent) => { | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| type: e.target.value, | |||||
| })); | |||||
| }, []); | |||||
| const handleDescriptionChange = useCallback((_e: any, newValue: string | null) => { | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| description: newValue || "", | |||||
| })); | |||||
| }, []); | |||||
| const handleSubmit = useCallback(async () => { | |||||
| setIsSubmitting(true); | |||||
| try { | |||||
| const needDpi = formData.type === "Label"; | |||||
| const missing: string[] = []; | |||||
| if (!formData.ip || formData.ip.trim() === "") missing.push("IP"); | |||||
| if (formData.port === undefined || formData.port === null || Number.isNaN(formData.port)) missing.push("Port"); | |||||
| if (!formData.type || formData.type.trim() === "") missing.push(t("Type") || "類型"); | |||||
| if (needDpi && (formData.dpi === undefined || formData.dpi === null || Number.isNaN(formData.dpi))) missing.push("DPI"); | |||||
| if (missing.length > 0) { | |||||
| alert(`請必須輸入 ${missing.join("、")}`); | |||||
| setIsSubmitting(false); | |||||
| return; | |||||
| } | |||||
| await createPrinter(formData); | |||||
| successDialog(t("Create Printer") || "新增列印機", t); | |||||
| router.push("/settings/printer"); | |||||
| router.refresh(); | |||||
| } catch (error) { | |||||
| const errorMessage = | |||||
| error instanceof Error | |||||
| ? error.message | |||||
| : t("Error saving data") || "儲存失敗"; | |||||
| alert(errorMessage); | |||||
| } finally { | |||||
| setIsSubmitting(false); | |||||
| } | |||||
| }, [formData, router, t]); | |||||
| return ( | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Grid container spacing={3}> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t("Name")} | |||||
| value={formData.name} | |||||
| onChange={handleChange("name")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="IP" | |||||
| value={formData.ip} | |||||
| onChange={handleChange("ip")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="Port" | |||||
| type="number" | |||||
| value={formData.port ?? ""} | |||||
| onChange={handleChange("port")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Type")}</InputLabel> | |||||
| <Select | |||||
| label={t("Type")} | |||||
| value={formData.type ?? "A4"} | |||||
| onChange={handleTypeChange} | |||||
| > | |||||
| <MenuItem value={"A4"}>A4</MenuItem> | |||||
| <MenuItem value={"Label"}>Label</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="DPI" | |||||
| type="number" | |||||
| value={formData.dpi ?? ""} | |||||
| onChange={handleChange("dpi")} | |||||
| variant="outlined" | |||||
| disabled={formData.type !== "Label"} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <Autocomplete | |||||
| freeSolo | |||||
| options={descriptions} | |||||
| value={formData.description || null} | |||||
| onChange={handleDescriptionChange} | |||||
| onInputChange={(_e, newInputValue) => { | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| description: newInputValue, | |||||
| })); | |||||
| }} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t("Description")} | |||||
| variant="outlined" | |||||
| fullWidth | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<ArrowBack />} | |||||
| onClick={() => router.push("/settings/printer")} | |||||
| > | |||||
| {t("Back")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| onClick={handleSubmit} | |||||
| disabled={isSubmitting} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| const CreatePrinterLoading: React.FC = () => { | |||||
| return null; | |||||
| }; | |||||
| export default Object.assign(CreatePrinter, { Loading: CreatePrinterLoading }); | |||||
| @@ -0,0 +1,2 @@ | |||||
| export { default } from "./CreatePrinter"; | |||||
| @@ -0,0 +1,161 @@ | |||||
| "use client"; | |||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { PrinterResult } from "@/app/api/settings/printer"; | |||||
| import { editPrinter, PrinterInputs } from "@/app/api/settings/printer/actions"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| FormControl, | |||||
| Grid, | |||||
| InputLabel, | |||||
| MenuItem, | |||||
| Select, | |||||
| SelectChangeEvent, | |||||
| Stack, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Check, ArrowBack } from "@mui/icons-material"; | |||||
| import { successDialog } from "../Swal/CustomAlerts"; | |||||
| type Props = { | |||||
| printer: PrinterResult; | |||||
| }; | |||||
| const EditPrinter: React.FC<Props> = ({ printer }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const router = useRouter(); | |||||
| const [isSubmitting, setIsSubmitting] = useState(false); | |||||
| const [formData, setFormData] = useState<PrinterInputs>({ | |||||
| name: printer.name || "", | |||||
| ip: printer.ip || "", | |||||
| port: printer.port || undefined, | |||||
| type: printer.type || "", | |||||
| dpi: printer.dpi || undefined, | |||||
| }); | |||||
| useEffect(() => { | |||||
| if (formData.type !== "Label") { | |||||
| setFormData((prev) => ({ ...prev, dpi: undefined })); | |||||
| } | |||||
| }, [formData.type]); | |||||
| const handleChange = useCallback((field: keyof PrinterInputs) => { | |||||
| return (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const value = e.target.value; | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| [field]: field === "port" || field === "dpi" | |||||
| ? (value === "" ? undefined : parseInt(value, 10)) | |||||
| : value, | |||||
| })); | |||||
| }; | |||||
| }, []); | |||||
| const handleTypeChange = useCallback((e: SelectChangeEvent) => { | |||||
| const value = e.target.value; | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| type: value, | |||||
| })); | |||||
| }, []); | |||||
| const handleSubmit = useCallback(async () => { | |||||
| setIsSubmitting(true); | |||||
| try { | |||||
| await editPrinter(printer.id, formData); | |||||
| successDialog(t("Save") || "儲存成功", t); | |||||
| router.push("/settings/printer"); | |||||
| router.refresh(); | |||||
| } catch (error) { | |||||
| console.error("Failed to update printer:", error); | |||||
| const errorMessage = error instanceof Error ? error.message : (t("Error saving data") || "儲存失敗"); | |||||
| alert(errorMessage); | |||||
| } finally { | |||||
| setIsSubmitting(false); | |||||
| } | |||||
| }, [formData, printer.id, router, t]); | |||||
| return ( | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Grid container spacing={3}> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label={t("Name")} | |||||
| value={formData.name} | |||||
| onChange={handleChange("name")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="IP" | |||||
| value={formData.ip} | |||||
| onChange={handleChange("ip")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="Port" | |||||
| type="number" | |||||
| value={formData.port || ""} | |||||
| onChange={handleChange("port")} | |||||
| variant="outlined" | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <FormControl fullWidth> | |||||
| <InputLabel>{t("Type")}</InputLabel> | |||||
| <Select | |||||
| label={t("Type")} | |||||
| value={formData.type ?? ""} | |||||
| onChange={handleTypeChange} | |||||
| > | |||||
| <MenuItem value={"A4"}>A4</MenuItem> | |||||
| <MenuItem value={"Label"}>Label</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <TextField | |||||
| fullWidth | |||||
| label="DPI" | |||||
| type="number" | |||||
| value={formData.dpi || ""} | |||||
| onChange={handleChange("dpi")} | |||||
| variant="outlined" | |||||
| disabled={formData.type !== "Label"} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <Stack direction="row" spacing={2}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<ArrowBack />} | |||||
| onClick={() => router.push("/settings/printer")} | |||||
| > | |||||
| {t("Back")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| onClick={handleSubmit} | |||||
| disabled={isSubmitting} | |||||
| > | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default EditPrinter; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./EditPrinter"; | |||||
| @@ -29,7 +29,6 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [isSearching, setIsSearching] = useState(false); | const [isSearching, setIsSearching] = useState(false); | ||||
| // Sync state when printers prop changes | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("Printers prop changed:", printers); | console.log("Printers prop changed:", printers); | ||||
| setFilteredPrinters(printers); | setFilteredPrinters(printers); | ||||
| @@ -43,8 +42,8 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | { | ||||
| label: t("Code"), | |||||
| paramName: "code", | |||||
| label: "IP", | |||||
| paramName: "ip", | |||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -66,10 +65,24 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| const onDeleteClick = useCallback((printer: PrinterResult) => { | const onDeleteClick = useCallback((printer: PrinterResult) => { | ||||
| deleteDialog(async () => { | deleteDialog(async () => { | ||||
| await deletePrinter(printer.id); | |||||
| setFilteredPrinters(prev => prev.filter(p => p.id !== printer.id)); | |||||
| router.refresh(); | |||||
| successDialog(t("Delete Success") || "刪除成功", t); | |||||
| try { | |||||
| console.log("Deleting printer with id:", printer.id); | |||||
| const result = await deletePrinter(printer.id); | |||||
| console.log("Delete result:", result); | |||||
| setFilteredPrinters(prev => prev.filter(p => p.id !== printer.id)); | |||||
| router.refresh(); | |||||
| setTimeout(() => { | |||||
| successDialog(t("Delete Success") || "刪除成功", t); | |||||
| }, 100); | |||||
| } catch (error) { | |||||
| console.error("Failed to delete printer:", error); | |||||
| const errorMessage = error instanceof Error ? error.message : (t("Delete Failed") || "刪除失敗"); | |||||
| alert(errorMessage); | |||||
| router.refresh(); | |||||
| } | |||||
| }, t); | }, t); | ||||
| }, [t, router]); | }, [t, router]); | ||||
| @@ -90,29 +103,36 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| sx: { width: "20%", minWidth: "120px" }, | sx: { width: "20%", minWidth: "120px" }, | ||||
| }, | }, | ||||
| { | { | ||||
| name: "code", | |||||
| label: t("Code"), | |||||
| name: "description", | |||||
| label: t("Description"), | |||||
| align: "left", | align: "left", | ||||
| headerAlign: "left", | headerAlign: "left", | ||||
| sx: { width: "15%", minWidth: "100px" }, | |||||
| sx: { width: "20%", minWidth: "140px" }, | |||||
| }, | }, | ||||
| { | { | ||||
| name: "type", | |||||
| label: t("Type"), | |||||
| name: "ip", | |||||
| label: "IP", | |||||
| align: "left", | align: "left", | ||||
| headerAlign: "left", | headerAlign: "left", | ||||
| sx: { width: "15%", minWidth: "100px" }, | sx: { width: "15%", minWidth: "100px" }, | ||||
| }, | }, | ||||
| { | { | ||||
| name: "ip", | |||||
| label: "IP", | |||||
| name: "port", | |||||
| label: "Port", | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | |||||
| { | |||||
| name: "type", | |||||
| label: t("Type"), | |||||
| align: "left", | align: "left", | ||||
| headerAlign: "left", | headerAlign: "left", | ||||
| sx: { width: "15%", minWidth: "100px" }, | sx: { width: "15%", minWidth: "100px" }, | ||||
| }, | }, | ||||
| { | { | ||||
| name: "port", | |||||
| label: "Port", | |||||
| name: "dpi", | |||||
| label: "DPI", | |||||
| align: "left", | align: "left", | ||||
| headerAlign: "left", | headerAlign: "left", | ||||
| sx: { width: "10%", minWidth: "80px" }, | sx: { width: "10%", minWidth: "80px" }, | ||||
| @@ -136,6 +156,10 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onReset={() => { | |||||
| setFilteredPrinters(printers); | |||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||||
| }} | |||||
| onSearch={async (query) => { | onSearch={async (query) => { | ||||
| setIsSearching(true); | setIsSearching(true); | ||||
| try { | try { | ||||
| @@ -147,9 +171,9 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| ); | ); | ||||
| } | } | ||||
| if (query.code && query.code.trim()) { | |||||
| if (query.ip && query.ip.trim()) { | |||||
| results = results.filter((printer) => | results = results.filter((printer) => | ||||
| printer.code?.toLowerCase().includes(query.code?.toLowerCase() || "") | |||||
| printer.ip?.toLowerCase().includes(query.ip?.toLowerCase() || "") | |||||
| ); | ); | ||||
| } | } | ||||
| @@ -179,4 +203,4 @@ const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default PrinterSearch; | |||||
| export default PrinterSearch; | |||||
| @@ -61,6 +61,18 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "Material", value: "Mat" } | { label: "Material", value: "Mat" } | ||||
| ] }, | ] }, | ||||
| ] | ] | ||||
| }, | |||||
| { | |||||
| id: "rep-004", | |||||
| title: "Stock In Traceability Report", | |||||
| apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-in-traceability`, | |||||
| fields: [ | |||||
| { label: "Stock Category", name: "stockCategory", type: "text", required: false, placeholder: "e.g. Meat" }, | |||||
| { label: "Stock Sub Category", name: "stockSubCategory", type: "text", required: false, placeholder: "e.g. Chicken" }, | |||||
| { label: "Item Code", name: "itemCode", type: "text", required: false, placeholder: "e.g. MT-001" }, | |||||
| { label: "Last In Date Start", name: "lastInDateStart", type: "date", required: false }, | |||||
| { label: "Last In Date End", name: "lastInDateEnd", type: "date", required: false }, | |||||
| ] | |||||
| } | } | ||||
| // Add Report 3 to 10 following the same pattern... | |||||
| // Add more reports following the same pattern... | |||||
| ]; | ]; | ||||
| @@ -422,5 +422,9 @@ | |||||
| "Add Shop to Truck Lane": "新增店鋪至卡車路線", | "Add Shop to Truck Lane": "新增店鋪至卡車路線", | ||||
| "Truck lane code already exists. Please use a different code.": "卡車路線編號已存在,請使用其他編號。", | "Truck lane code already exists. Please use a different code.": "卡車路線編號已存在,請使用其他編號。", | ||||
| "MaintenanceEdit": "編輯維護和保養", | "MaintenanceEdit": "編輯維護和保養", | ||||
| "Printer": "列印機" | |||||
| "Printer": "列印機", | |||||
| "Delete": "刪除", | |||||
| "Delete Success": "刪除成功", | |||||
| "Delete Failed": "刪除失敗", | |||||
| "Create Printer": "新增列印機" | |||||
| } | } | ||||