diff --git a/src/app/(main)/settings/printer/create/page.tsx b/src/app/(main)/settings/printer/create/page.tsx new file mode 100644 index 0000000..8a5e509 --- /dev/null +++ b/src/app/(main)/settings/printer/create/page.tsx @@ -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 ( + <> + {t("Create Printer") || "新增列印機"} + + }> + + + + + ); +}; + +export default CreatePrinterPage; + diff --git a/src/app/(main)/settings/printer/edit/page.tsx b/src/app/(main)/settings/printer/edit/page.tsx new file mode 100644 index 0000000..c2c02c9 --- /dev/null +++ b/src/app/(main)/settings/printer/edit/page.tsx @@ -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 = 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 ( + <> + {t("Edit")} {t("Printer")} + + Loading...}> + + + + + ); +}; + +export default EditPrinterPage; diff --git a/src/app/api/settings/printer/actions.ts b/src/app/api/settings/printer/actions.ts index 9f6b64b..21ceb3a 100644 --- a/src/app/api/settings/printer/actions.ts +++ b/src/app/api/settings/printer/actions.ts @@ -15,6 +15,7 @@ export interface PrinterInputs { description?: string; ip?: string; port?: number; + dpi?: number; } export const fetchPrinterDetails = async (id: number) => { @@ -51,3 +52,9 @@ export const deletePrinter = async (id: number) => { revalidateTag("printers"); return result; }; + +export const fetchPrinterDescriptions = async () => { + return serverFetchJson(`${BASE_API_URL}/printers/descriptions`, { + next: { tags: ["printers"] }, + }); +}; diff --git a/src/app/api/settings/printer/index.ts b/src/app/api/settings/printer/index.ts index 20bd6c7..a4c4117 100644 --- a/src/app/api/settings/printer/index.ts +++ b/src/app/api/settings/printer/index.ts @@ -24,6 +24,7 @@ export interface PrinterResult { description?: string; ip?: string; port?: number; + dpi?: number; } export const fetchPrinterCombo = cache(async () => { @@ -36,4 +37,10 @@ export const fetchPrinters = cache(async () => { return serverFetchJson(`${BASE_API_URL}/printers`, { next: { tags: ["printers"] }, }); +}); + +export const fetchPrinterDescriptions = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/printers/descriptions`, { + next: { tags: ["printers"] }, + }); }); \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 357dbdf..6ec388f 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -41,10 +41,30 @@ export async function serverFetchWithNoContent(...args: FetchParams) { case 401: signOutUser(); 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( - `Server error: ${response.status} ${response.statusText}. ${errorText || "Something went wrong fetching data in server."}`, + `Server error: ${response.status} ${response.statusText}. ${errorMessage}`, response ); } @@ -74,7 +94,7 @@ type FetchParams = Parameters; export async function serverFetchJson(...args: FetchParams) { const response = await serverFetch(...args); - console.log(response.status); + console.log("serverFetchJson - Status:", response.status, "URL:", args[0]); if (response.ok) { if (response.status === 204) { return response.status as T; @@ -82,12 +102,14 @@ export async function serverFetchJson(...args: FetchParams) { return response.json() as T; } else { + const errorText = await response.text().catch(() => "Unable to read error response"); + console.error("serverFetchJson - Error response:", response.status, errorText); switch (response.status) { case 401: signOutUser(); default: throw new ServerFetchError( - "Something went wrong fetching data in server.", + `Server error: ${response.status} ${response.statusText}. ${errorText}`, response, ); } diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 114f98c..d5c20d3 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -21,6 +21,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/shop": "ShopAndTruck", "/settings/shop/detail": "Shop Detail", "/settings/shop/truckdetail": "Truck Lane Detail", + "/settings/printer": "Printer", "/scheduling/rough": "Demand Forecast", "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", "/scheduling/detailed": "Detail Scheduling", diff --git a/src/components/CreatePrinter/CreatePrinter.tsx b/src/components/CreatePrinter/CreatePrinter.tsx new file mode 100644 index 0000000..c2e7644 --- /dev/null +++ b/src/components/CreatePrinter/CreatePrinter.tsx @@ -0,0 +1,197 @@ +"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 { + 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([]); + const [formData, setFormData] = useState({ + 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(); + }, []); + + const handleChange = useCallback((field: keyof PrinterInputs) => { + return (e: React.ChangeEvent) => { + 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: SelectChangeEvent) => { + setFormData((prev) => ({ + ...prev, + description: e.target.value, + })); + }, []); + + const handleSubmit = useCallback(async () => { + setIsSubmitting(true); + try { + 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 ( + + + + + + + + + + + + + + {t("Type")} + + + + + + + + + {t("Description")} + + + + + + + + + + + + ); +}; + +const CreatePrinterLoading: React.FC = () => { + return null; +}; + +export default Object.assign(CreatePrinter, { Loading: CreatePrinterLoading }); + diff --git a/src/components/CreatePrinter/index.ts b/src/components/CreatePrinter/index.ts new file mode 100644 index 0000000..eb7e890 --- /dev/null +++ b/src/components/CreatePrinter/index.ts @@ -0,0 +1,2 @@ +export { default } from "./CreatePrinter"; + diff --git a/src/components/EditPrinter/EditPrinter.tsx b/src/components/EditPrinter/EditPrinter.tsx new file mode 100644 index 0000000..d29ceec --- /dev/null +++ b/src/components/EditPrinter/EditPrinter.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useCallback, 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 = ({ printer }) => { + const { t } = useTranslation("common"); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: printer.name || "", + ip: printer.ip || "", + port: printer.port || undefined, + type: printer.type || "", + dpi: printer.dpi || undefined, + }); + + const handleChange = useCallback((field: keyof PrinterInputs) => { + return (e: React.ChangeEvent) => { + 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 ( + + + + + + + + + + + + + + {t("Type")} + + + + + + + + + + + + + + + ); +}; + +export default EditPrinter; diff --git a/src/components/EditPrinter/index.ts b/src/components/EditPrinter/index.ts new file mode 100644 index 0000000..d974c1b --- /dev/null +++ b/src/components/EditPrinter/index.ts @@ -0,0 +1 @@ +export { default } from "./EditPrinter"; diff --git a/src/components/PrinterSearch/PrinterSearch.tsx b/src/components/PrinterSearch/PrinterSearch.tsx index ff0fe1c..d2de7e2 100644 --- a/src/components/PrinterSearch/PrinterSearch.tsx +++ b/src/components/PrinterSearch/PrinterSearch.tsx @@ -29,7 +29,6 @@ const PrinterSearch: React.FC = ({ printers }) => { const router = useRouter(); const [isSearching, setIsSearching] = useState(false); - // Sync state when printers prop changes useEffect(() => { console.log("Printers prop changed:", printers); setFilteredPrinters(printers); @@ -43,8 +42,8 @@ const PrinterSearch: React.FC = ({ printers }) => { type: "text", }, { - label: t("Code"), - paramName: "code", + label: "IP", + paramName: "ip", type: "text", }, { @@ -66,10 +65,24 @@ const PrinterSearch: React.FC = ({ printers }) => { const onDeleteClick = useCallback((printer: PrinterResult) => { 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, router]); @@ -90,29 +103,36 @@ const PrinterSearch: React.FC = ({ printers }) => { sx: { width: "20%", minWidth: "120px" }, }, { - name: "code", - label: t("Code"), + name: "description", + label: t("Description"), align: "left", headerAlign: "left", - sx: { width: "15%", minWidth: "100px" }, + sx: { width: "20%", minWidth: "140px" }, }, { - name: "type", - label: t("Type"), + name: "ip", + label: "IP", align: "left", headerAlign: "left", 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", headerAlign: "left", sx: { width: "15%", minWidth: "100px" }, }, { - name: "port", - label: "Port", + name: "dpi", + label: "DPI", align: "left", headerAlign: "left", sx: { width: "10%", minWidth: "80px" }, @@ -136,6 +156,10 @@ const PrinterSearch: React.FC = ({ printers }) => { <> { + setFilteredPrinters(printers); + setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); + }} onSearch={async (query) => { setIsSearching(true); try { @@ -147,9 +171,9 @@ const PrinterSearch: React.FC = ({ printers }) => { ); } - if (query.code && query.code.trim()) { + if (query.ip && query.ip.trim()) { 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 = ({ printers }) => { ); }; -export default PrinterSearch; +export default PrinterSearch; \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 8214b33..ee1e539 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -422,5 +422,9 @@ "Add Shop to Truck Lane": "新增店鋪至卡車路線", "Truck lane code already exists. Please use a different code.": "卡車路線編號已存在,請使用其他編號。", "MaintenanceEdit": "編輯維護和保養", - "Printer": "列印機" + "Printer": "列印機", + "Delete": "刪除", + "Delete Success": "刪除成功", + "Delete Failed": "刪除失敗", + "Create Printer": "新增列印機" } \ No newline at end of file