| @@ -0,0 +1,47 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n, I18nProvider } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Suspense } from "react"; | |||||
| import { Stack } from "@mui/material"; | |||||
| import { Button } from "@mui/material"; | |||||
| import Link from "next/link"; | |||||
| import PrinterSearch from "@/components/PrinterSearch"; | |||||
| import Add from "@mui/icons-material/Add"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Printer Management", | |||||
| }; | |||||
| const Printer: React.FC = async () => { | |||||
| const { t } = await getServerI18n("common"); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Printer")} | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/settings/printer/create" | |||||
| > | |||||
| {t("Create Printer") || "新增列印機"} | |||||
| </Button> | |||||
| </Stack> | |||||
| <I18nProvider namespaces={["common", "dashboard"]}> | |||||
| <Suspense fallback={<PrinterSearch.Loading />}> | |||||
| <PrinterSearch /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Printer; | |||||
| @@ -0,0 +1,53 @@ | |||||
| "use server"; | |||||
| import { | |||||
| serverFetchJson, | |||||
| serverFetchWithNoContent, | |||||
| } from "../../../utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "../../../../config/api"; | |||||
| import { revalidateTag } from "next/cache"; | |||||
| import { PrinterResult } from "."; | |||||
| export interface PrinterInputs { | |||||
| name?: string; | |||||
| code?: string; | |||||
| type?: string; | |||||
| description?: string; | |||||
| ip?: string; | |||||
| port?: number; | |||||
| } | |||||
| export const fetchPrinterDetails = async (id: number) => { | |||||
| return serverFetchJson<PrinterResult>(`${BASE_API_URL}/printers/${id}`, { | |||||
| next: { tags: ["printers"] }, | |||||
| }); | |||||
| }; | |||||
| export const editPrinter = async (id: number, data: PrinterInputs) => { | |||||
| const result = await serverFetchWithNoContent(`${BASE_API_URL}/printers/${id}`, { | |||||
| method: "PUT", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("printers"); | |||||
| return result; | |||||
| }; | |||||
| export const createPrinter = async (data: PrinterInputs) => { | |||||
| const result = await serverFetchWithNoContent(`${BASE_API_URL}/printers`, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("printers"); | |||||
| return result; | |||||
| }; | |||||
| export const deletePrinter = async (id: number) => { | |||||
| const result = await serverFetchWithNoContent(`${BASE_API_URL}/printers/${id}`, { | |||||
| method: "DELETE", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| revalidateTag("printers"); | |||||
| return result; | |||||
| }; | |||||
| @@ -15,8 +15,25 @@ export interface PrinterCombo { | |||||
| port?: number; | port?: number; | ||||
| } | } | ||||
| export interface PrinterResult { | |||||
| action: any; | |||||
| id: number; | |||||
| name?: string; | |||||
| code?: string; | |||||
| type?: string; | |||||
| description?: string; | |||||
| ip?: string; | |||||
| port?: number; | |||||
| } | |||||
| export const fetchPrinterCombo = cache(async () => { | export const fetchPrinterCombo = cache(async () => { | ||||
| return serverFetchJson<PrinterCombo[]>(`${BASE_API_URL}/printers/combo`, { | return serverFetchJson<PrinterCombo[]>(`${BASE_API_URL}/printers/combo`, { | ||||
| next: { tags: ["qcItems"] }, | |||||
| next: { tags: ["printers"] }, | |||||
| }) | }) | ||||
| }) | |||||
| }) | |||||
| export const fetchPrinters = cache(async () => { | |||||
| return serverFetchJson<PrinterResult[]>(`${BASE_API_URL}/printers`, { | |||||
| next: { tags: ["printers"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -13,7 +13,7 @@ export interface WarehouseResult { | |||||
| warehouse?: string; | warehouse?: string; | ||||
| area?: string; | area?: string; | ||||
| slot?: string; | slot?: string; | ||||
| order?: number; | |||||
| order?: string; | |||||
| stockTakeSection?: string; | stockTakeSection?: string; | ||||
| } | } | ||||
| @@ -35,7 +35,7 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||||
| const response = await serverFetch(...args); | const response = await serverFetch(...args); | ||||
| if (response.ok) { | if (response.ok) { | ||||
| return response.status; // 204 No Content, e.g. for delete data | |||||
| return response.status; | |||||
| } else { | } else { | ||||
| switch (response.status) { | switch (response.status) { | ||||
| case 401: | case 401: | ||||
| @@ -52,7 +52,6 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||||
| } | } | ||||
| export const serverFetch: typeof fetch = async (input, init) => { | export const serverFetch: typeof fetch = async (input, init) => { | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||||
| const session = await getServerSession<any, SessionWithTokens>(authOptions); | const session = await getServerSession<any, SessionWithTokens>(authOptions); | ||||
| const accessToken = session?.accessToken; | const accessToken = session?.accessToken; | ||||
| @@ -129,7 +128,6 @@ export async function serverFetchBlob<T extends BlobResponse>(...args: FetchPara | |||||
| while (!done) { | while (!done) { | ||||
| const read = await reader?.read(); | const read = await reader?.read(); | ||||
| // version 1 | |||||
| if (read?.done) { | if (read?.done) { | ||||
| done = true; | done = true; | ||||
| } else { | } else { | ||||
| @@ -362,6 +362,11 @@ const NavigationContent: React.FC = () => { | |||||
| label: "QC Item All", | label: "QC Item All", | ||||
| path: "/settings/qcItemAll", | path: "/settings/qcItemAll", | ||||
| }, | }, | ||||
| { | |||||
| icon: <QrCodeIcon/>, | |||||
| label: "QR Code Handle", | |||||
| path: "/settings/qrCodeHandle", | |||||
| }, | |||||
| // { | // { | ||||
| // icon: <RequestQuote />, | // icon: <RequestQuote />, | ||||
| // label: "Mail", | // label: "Mail", | ||||
| @@ -0,0 +1,182 @@ | |||||
| "use client"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { useCallback, useMemo, useState, useEffect } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import SearchResults, { Column } from "../SearchResults/index"; | |||||
| import EditNote from "@mui/icons-material/EditNote"; | |||||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||||
| import { PrinterResult } from "@/app/api/settings/printer"; | |||||
| import { deletePrinter } from "@/app/api/settings/printer/actions"; | |||||
| import PrinterSearchLoading from "./PrinterSearchLoading"; | |||||
| interface Props { | |||||
| printers: PrinterResult[]; | |||||
| } | |||||
| type SearchQuery = Partial<Omit<PrinterResult, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const PrinterSearch: React.FC<Props> = ({ printers }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const [filteredPrinters, setFilteredPrinters] = useState(printers); | |||||
| const [pagingController, setPagingController] = useState({ | |||||
| pageNum: 1, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const router = useRouter(); | |||||
| const [isSearching, setIsSearching] = useState(false); | |||||
| // Sync state when printers prop changes | |||||
| useEffect(() => { | |||||
| console.log("Printers prop changed:", printers); | |||||
| setFilteredPrinters(printers); | |||||
| }, [printers]); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { | |||||
| label: t("Name"), | |||||
| paramName: "name", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Code"), | |||||
| paramName: "code", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Type"), | |||||
| paramName: "type", | |||||
| type: "text", | |||||
| }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| const onPrinterClick = useCallback( | |||||
| (printer: PrinterResult) => { | |||||
| console.log(printer); | |||||
| router.push(`/settings/printer/edit?id=${printer.id}`); | |||||
| }, | |||||
| [router], | |||||
| ); | |||||
| 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); | |||||
| }, t); | |||||
| }, [t, router]); | |||||
| const columns = useMemo<Column<PrinterResult>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "action", | |||||
| label: t("Edit"), | |||||
| onClick: onPrinterClick, | |||||
| buttonIcon: <EditNote />, | |||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | |||||
| { | |||||
| name: "name", | |||||
| label: t("Name"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "20%", minWidth: "120px" }, | |||||
| }, | |||||
| { | |||||
| name: "code", | |||||
| label: t("Code"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "100px" }, | |||||
| }, | |||||
| { | |||||
| name: "type", | |||||
| label: t("Type"), | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "100px" }, | |||||
| }, | |||||
| { | |||||
| name: "ip", | |||||
| label: "IP", | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "15%", minWidth: "100px" }, | |||||
| }, | |||||
| { | |||||
| name: "port", | |||||
| label: "Port", | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | |||||
| { | |||||
| name: "action", | |||||
| label: t("Delete"), | |||||
| onClick: onDeleteClick, | |||||
| buttonIcon: <DeleteIcon />, | |||||
| color: "error", | |||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | |||||
| ], | |||||
| [t, onPrinterClick, onDeleteClick], | |||||
| ); | |||||
| console.log("PrinterSearch render - filteredPrinters:", filteredPrinters); | |||||
| console.log("PrinterSearch render - printers prop:", printers); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={async (query) => { | |||||
| setIsSearching(true); | |||||
| try { | |||||
| let results: PrinterResult[] = printers; | |||||
| if (query.name && query.name.trim()) { | |||||
| results = results.filter((printer) => | |||||
| printer.name?.toLowerCase().includes(query.name?.toLowerCase() || "") | |||||
| ); | |||||
| } | |||||
| if (query.code && query.code.trim()) { | |||||
| results = results.filter((printer) => | |||||
| printer.code?.toLowerCase().includes(query.code?.toLowerCase() || "") | |||||
| ); | |||||
| } | |||||
| if (query.type && query.type.trim()) { | |||||
| results = results.filter((printer) => | |||||
| printer.type?.toLowerCase().includes(query.type?.toLowerCase() || "") | |||||
| ); | |||||
| } | |||||
| setFilteredPrinters(results); | |||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | |||||
| } catch (error) { | |||||
| console.error("Error searching printers:", error); | |||||
| setFilteredPrinters(printers); | |||||
| } finally { | |||||
| setIsSearching(false); | |||||
| } | |||||
| }} | |||||
| /> | |||||
| <SearchResults<PrinterResult> | |||||
| items={filteredPrinters} | |||||
| columns={columns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default PrinterSearch; | |||||
| @@ -0,0 +1,39 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| export const PrinterSearchLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default PrinterSearchLoading; | |||||
| @@ -0,0 +1,25 @@ | |||||
| import React from "react"; | |||||
| import PrinterSearch from "./PrinterSearch"; | |||||
| import PrinterSearchLoading from "./PrinterSearchLoading"; | |||||
| import { PrinterResult, fetchPrinters } from "@/app/api/settings/printer"; | |||||
| interface SubComponents { | |||||
| Loading: typeof PrinterSearchLoading; | |||||
| } | |||||
| const PrinterSearchWrapper: React.FC & SubComponents = async () => { | |||||
| let printers: PrinterResult[] = []; | |||||
| try { | |||||
| printers = await fetchPrinters(); | |||||
| console.log("Printers fetched:", printers); | |||||
| } catch (error) { | |||||
| console.error("Error fetching printers:", error); | |||||
| printers = []; | |||||
| } | |||||
| return <PrinterSearch printers={printers} />; | |||||
| }; | |||||
| PrinterSearchWrapper.Loading = PrinterSearchLoading; | |||||
| export default PrinterSearchWrapper; | |||||
| @@ -0,0 +1,2 @@ | |||||
| export { default } from "./PrinterSearchWrapper"; | |||||
| @@ -84,6 +84,10 @@ const QrCodeHandleWarehouseSearch: React.FC<Props> = ({ warehouses, printerCombo | |||||
| } | } | ||||
| }, [filteredPrinters, selectedPrinter]); | }, [filteredPrinters, selectedPrinter]); | ||||
| useEffect(() => { | |||||
| setFilteredWarehouses(warehouses); | |||||
| }, [warehouses]); | |||||
| const handleReset = useCallback(() => { | const handleReset = useCallback(() => { | ||||
| setSearchInputs({ | setSearchInputs({ | ||||
| store_id: "", | store_id: "", | ||||
| @@ -1,21 +1,32 @@ | |||||
| import React from "react"; | import React from "react"; | ||||
| import QrCodeHandleWarehouseSearch from "./qrCodeHandleWarehouseSearch"; | import QrCodeHandleWarehouseSearch from "./qrCodeHandleWarehouseSearch"; | ||||
| import QrCodeHandleSearchLoading from "./qrCodeHandleSearchLoading"; | import QrCodeHandleSearchLoading from "./qrCodeHandleSearchLoading"; | ||||
| import { fetchWarehouseList } from "@/app/api/warehouse"; | |||||
| import { fetchPrinterCombo } from "@/app/api/settings/printer"; | |||||
| import { fetchWarehouseList, WarehouseResult } from "@/app/api/warehouse"; | |||||
| import { fetchPrinterCombo, PrinterCombo } from "@/app/api/settings/printer"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof QrCodeHandleSearchLoading; | Loading: typeof QrCodeHandleSearchLoading; | ||||
| } | } | ||||
| const QrCodeHandleWarehouseSearchWrapper: React.FC & SubComponents = async () => { | const QrCodeHandleWarehouseSearchWrapper: React.FC & SubComponents = async () => { | ||||
| const [warehouses, printerCombo] = await Promise.all([ | |||||
| fetchWarehouseList(), | |||||
| fetchPrinterCombo(), | |||||
| ]); | |||||
| let warehouses: WarehouseResult[] = []; | |||||
| let printerCombo: PrinterCombo[] = []; | |||||
| try { | |||||
| warehouses = await fetchWarehouseList(); | |||||
| } catch (error) { | |||||
| console.error("Error fetching warehouse list:", error); | |||||
| } | |||||
| try { | |||||
| printerCombo = await fetchPrinterCombo(); | |||||
| } catch (error) { | |||||
| console.error("Error fetching printer combo:", error); | |||||
| } | |||||
| return <QrCodeHandleWarehouseSearch warehouses={warehouses} printerCombo={printerCombo} />; | return <QrCodeHandleWarehouseSearch warehouses={warehouses} printerCombo={printerCombo} />; | ||||
| }; | }; | ||||
| QrCodeHandleWarehouseSearchWrapper.Loading = QrCodeHandleSearchLoading; | QrCodeHandleWarehouseSearchWrapper.Loading = QrCodeHandleSearchLoading; | ||||
| export default QrCodeHandleWarehouseSearchWrapper; | |||||
| export default QrCodeHandleWarehouseSearchWrapper; | |||||
| @@ -421,5 +421,6 @@ | |||||
| "Edit shop details": "編輯店鋪詳情", | "Edit shop details": "編輯店鋪詳情", | ||||
| "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": "列印機" | |||||
| } | } | ||||