| @@ -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; | |||
| } | |||
| export interface PrinterResult { | |||
| action: any; | |||
| id: number; | |||
| name?: string; | |||
| code?: string; | |||
| type?: string; | |||
| description?: string; | |||
| ip?: string; | |||
| port?: number; | |||
| } | |||
| export const fetchPrinterCombo = cache(async () => { | |||
| 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; | |||
| area?: string; | |||
| slot?: string; | |||
| order?: number; | |||
| order?: string; | |||
| stockTakeSection?: string; | |||
| } | |||
| @@ -35,7 +35,7 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||
| const response = await serverFetch(...args); | |||
| if (response.ok) { | |||
| return response.status; // 204 No Content, e.g. for delete data | |||
| return response.status; | |||
| } else { | |||
| switch (response.status) { | |||
| case 401: | |||
| @@ -52,7 +52,6 @@ export async function serverFetchWithNoContent(...args: FetchParams) { | |||
| } | |||
| 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 accessToken = session?.accessToken; | |||
| @@ -129,7 +128,6 @@ export async function serverFetchBlob<T extends BlobResponse>(...args: FetchPara | |||
| while (!done) { | |||
| const read = await reader?.read(); | |||
| // version 1 | |||
| if (read?.done) { | |||
| done = true; | |||
| } else { | |||
| @@ -362,6 +362,11 @@ const NavigationContent: React.FC = () => { | |||
| label: "QC Item All", | |||
| path: "/settings/qcItemAll", | |||
| }, | |||
| { | |||
| icon: <QrCodeIcon/>, | |||
| label: "QR Code Handle", | |||
| path: "/settings/qrCodeHandle", | |||
| }, | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| // 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]); | |||
| useEffect(() => { | |||
| setFilteredWarehouses(warehouses); | |||
| }, [warehouses]); | |||
| const handleReset = useCallback(() => { | |||
| setSearchInputs({ | |||
| store_id: "", | |||
| @@ -1,21 +1,32 @@ | |||
| import React from "react"; | |||
| import QrCodeHandleWarehouseSearch from "./qrCodeHandleWarehouseSearch"; | |||
| 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 { | |||
| Loading: typeof QrCodeHandleSearchLoading; | |||
| } | |||
| 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} />; | |||
| }; | |||
| QrCodeHandleWarehouseSearchWrapper.Loading = QrCodeHandleSearchLoading; | |||
| export default QrCodeHandleWarehouseSearchWrapper; | |||
| export default QrCodeHandleWarehouseSearchWrapper; | |||
| @@ -421,5 +421,6 @@ | |||
| "Edit shop details": "編輯店鋪詳情", | |||
| "Add Shop to Truck Lane": "新增店鋪至卡車路線", | |||
| "Truck lane code already exists. Please use a different code.": "卡車路線編號已存在,請使用其他編號。", | |||
| "MaintenanceEdit": "編輯維護和保養" | |||
| "MaintenanceEdit": "編輯維護和保養", | |||
| "Printer": "列印機" | |||
| } | |||