| @@ -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; | |||
| 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"] }, | |||
| }); | |||
| }; | |||
| @@ -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"] }, | |||
| }); | |||
| }); | |||
| @@ -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, | |||
| ); | |||
| } | |||
| @@ -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", | |||
| @@ -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 [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; | |||
| @@ -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... | |||
| ]; | |||
| @@ -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": "新增列印機" | |||
| } | |||