Bladeren bron

Supporting function: Printer Handle

MergeProblem1
B.E.N.S.O.N 2 weken geleden
bovenliggende
commit
321927854e
12 gewijzigde bestanden met toevoegingen van 504 en 25 verwijderingen
  1. +22
    -0
      src/app/(main)/settings/printer/create/page.tsx
  2. +38
    -0
      src/app/(main)/settings/printer/edit/page.tsx
  3. +7
    -0
      src/app/api/settings/printer/actions.ts
  4. +7
    -0
      src/app/api/settings/printer/index.ts
  5. +27
    -5
      src/app/utils/fetchUtil.ts
  6. +1
    -0
      src/components/Breadcrumb/Breadcrumb.tsx
  7. +197
    -0
      src/components/CreatePrinter/CreatePrinter.tsx
  8. +2
    -0
      src/components/CreatePrinter/index.ts
  9. +154
    -0
      src/components/EditPrinter/EditPrinter.tsx
  10. +1
    -0
      src/components/EditPrinter/index.ts
  11. +43
    -19
      src/components/PrinterSearch/PrinterSearch.tsx
  12. +5
    -1
      src/i18n/zh/common.json

+ 22
- 0
src/app/(main)/settings/printer/create/page.tsx Bestand weergeven

@@ -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;


+ 38
- 0
src/app/(main)/settings/printer/edit/page.tsx Bestand weergeven

@@ -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;

+ 7
- 0
src/app/api/settings/printer/actions.ts Bestand weergeven

@@ -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<string[]>(`${BASE_API_URL}/printers/descriptions`, {
next: { tags: ["printers"] },
});
};

+ 7
- 0
src/app/api/settings/printer/index.ts Bestand weergeven

@@ -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<PrinterResult[]>(`${BASE_API_URL}/printers`, {
next: { tags: ["printers"] },
});
});

export const fetchPrinterDescriptions = cache(async () => {
return serverFetchJson<string[]>(`${BASE_API_URL}/printers/descriptions`, {
next: { tags: ["printers"] },
});
});

+ 27
- 5
src/app/utils/fetchUtil.ts Bestand weergeven

@@ -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<typeof fetch>;

export async function serverFetchJson<T>(...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<T>(...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,
);
}


+ 1
- 0
src/components/Breadcrumb/Breadcrumb.tsx Bestand weergeven

@@ -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",


+ 197
- 0
src/components/CreatePrinter/CreatePrinter.tsx Bestand weergeven

@@ -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<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();
}, []);

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: 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 (
<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"
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>{t("Description")}</InputLabel>
<Select
label={t("Description")}
value={formData.description ?? ""}
onChange={handleDescriptionChange}
>
<MenuItem value="">
<em>-- {t("Select") || "選擇"} --</em>
</MenuItem>
{descriptions.map((desc) => (
<MenuItem key={desc} value={desc}>
{desc}
</MenuItem>
))}
</Select>
</FormControl>
</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 });


+ 2
- 0
src/components/CreatePrinter/index.ts Bestand weergeven

@@ -0,0 +1,2 @@
export { default } from "./CreatePrinter";


+ 154
- 0
src/components/EditPrinter/EditPrinter.tsx Bestand weergeven

@@ -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<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,
});

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"
/>
</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;

+ 1
- 0
src/components/EditPrinter/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./EditPrinter";

+ 43
- 19
src/components/PrinterSearch/PrinterSearch.tsx Bestand weergeven

@@ -29,7 +29,6 @@ const PrinterSearch: React.FC<Props> = ({ 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<Props> = ({ printers }) => {
type: "text",
},
{
label: t("Code"),
paramName: "code",
label: "IP",
paramName: "ip",
type: "text",
},
{
@@ -66,10 +65,24 @@ const PrinterSearch: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ printers }) => {
<>
<SearchBox
criteria={searchCriteria}
onReset={() => {
setFilteredPrinters(printers);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
}}
onSearch={async (query) => {
setIsSearching(true);
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) =>
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;

+ 5
- 1
src/i18n/zh/common.json Bestand weergeven

@@ -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": "新增列印機"
}

Laden…
Annuleren
Opslaan