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..b72de16
--- /dev/null
+++ b/src/components/CreatePrinter/CreatePrinter.tsx
@@ -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([]);
+ 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();
+ }, []);
+
+ useEffect(() => {
+ if (formData.type !== "Label") {
+ setFormData((prev) => ({ ...prev, dpi: undefined }));
+ }
+ }, [formData.type]);
+
+ 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: 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("Type")}
+
+
+
+
+
+
+
+ {
+ setFormData((prev) => ({
+ ...prev,
+ description: newInputValue,
+ }));
+ }}
+ renderInput={(params) => (
+
+ )}
+ />
+
+
+
+ }
+ onClick={() => router.push("/settings/printer")}
+ >
+ {t("Back")}
+
+ }
+ onClick={handleSubmit}
+ disabled={isSubmitting}
+ >
+ {t("Save")}
+
+
+
+
+
+ );
+};
+
+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..e8512ce
--- /dev/null
+++ b/src/components/EditPrinter/EditPrinter.tsx
@@ -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 = ({ 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,
+ });
+
+ useEffect(() => {
+ if (formData.type !== "Label") {
+ setFormData((prev) => ({ ...prev, dpi: undefined }));
+ }
+ }, [formData.type]);
+
+ 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")}
+
+
+
+
+
+
+
+
+ }
+ onClick={() => router.push("/settings/printer")}
+ >
+ {t("Back")}
+
+ }
+ onClick={handleSubmit}
+ disabled={isSubmitting}
+ >
+ {t("Save")}
+
+
+
+
+
+ );
+};
+
+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/config/reportConfig.ts b/src/config/reportConfig.ts
index f4a242e..50d15e2 100644
--- a/src/config/reportConfig.ts
+++ b/src/config/reportConfig.ts
@@ -61,6 +61,18 @@ export const REPORTS: ReportDefinition[] = [
{ 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...
];
\ 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