diff --git a/src/app/(main)/settings/qrCodeHandle/page.tsx b/src/app/(main)/settings/qrCodeHandle/page.tsx new file mode 100644 index 0000000..e0a84c7 --- /dev/null +++ b/src/app/(main)/settings/qrCodeHandle/page.tsx @@ -0,0 +1,44 @@ +import { Suspense } from "react"; +import { Metadata } from "next"; +import Typography from "@mui/material/Typography"; +import { getServerI18n } from "@/i18n"; +import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper"; +import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper"; +import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs"; +import { I18nProvider } from "@/i18n"; +import Box from "@mui/material/Box"; + +export const metadata: Metadata = { title: "QR Code Handle" }; + +const QrCodeHandlePage: React.FC = async () => { + const { t } = await getServerI18n("common"); + + return ( + + + {t("QR Code Handle")} + + + + }> + + + + + } + equipmentTabContent={ + }> + + + + + } + /> + + + ); +}; + +export default QrCodeHandlePage; \ No newline at end of file diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx index d66ba94..b0221c1 100644 --- a/src/app/(main)/settings/user/page.tsx +++ b/src/app/(main)/settings/user/page.tsx @@ -34,7 +34,7 @@ const User: React.FC = async () => { {t("Create User")} - + }> diff --git a/src/app/api/scheduling/actions.ts b/src/app/api/scheduling/actions.ts index 0cc4b05..14c891d 100644 --- a/src/app/api/scheduling/actions.ts +++ b/src/app/api/scheduling/actions.ts @@ -177,6 +177,25 @@ export const releaseProdSchedule = cache(async (data: ReleaseProdScheduleReq) => return response; }) +export const exportProdSchedule = async (token: string | null) => { + if (!token) throw new Error("No access token found"); + + const response = await fetch(`${BASE_API_URL}/productionSchedule/export-prod-schedule`, { + method: "POST", + headers: { + "Accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Authorization": `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error(`Backend error: ${response.status}`); + + const arrayBuffer = await response.arrayBuffer(); + + // Convert to Base64 so Next.js can send it safely over the wire + return Buffer.from(arrayBuffer).toString('base64'); +}; + export const saveProdScheduleLine = cache(async (data: ReleaseProdScheduleInputs) => { const response = serverFetchJson( `${BASE_API_URL}/productionSchedule/detail/detailed/save`, diff --git a/src/app/api/scheduling/index.ts b/src/app/api/scheduling/index.ts index 5ffa663..4171c56 100644 --- a/src/app/api/scheduling/index.ts +++ b/src/app/api/scheduling/index.ts @@ -105,6 +105,8 @@ export interface DetailedProdScheduleLineResult { stockQty: number; // Warehouse stock quantity daysLeft: number; // Days remaining before stockout needNoOfJobOrder: number; + prodQty: number; + outputQty: number; } export interface DetailedProdScheduleLineBomMaterialResult { diff --git a/src/app/api/settings/printer/index.ts b/src/app/api/settings/printer/index.ts index e443ccd..d41dc48 100644 --- a/src/app/api/settings/printer/index.ts +++ b/src/app/api/settings/printer/index.ts @@ -9,6 +9,7 @@ export interface PrinterCombo { label?: string; code?: string; name?: string; + type?: string; description?: string; ip?: string; port?: number; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 95b3404..6ec65d3 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -14,9 +14,11 @@ import { cache } from "react"; export interface UserInputs { username: string; // name: string; + staffNo?: string; addAuthIds?: number[]; removeAuthIds?: number[]; password?: string; + confirmPassword?: string; } export interface PasswordInputs { diff --git a/src/app/api/user/client.ts b/src/app/api/user/client.ts new file mode 100644 index 0000000..1e734b9 --- /dev/null +++ b/src/app/api/user/client.ts @@ -0,0 +1,32 @@ +"use client"; + +import { NEXT_PUBLIC_API_URL } from "@/config/api"; + +export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => { + + const token = localStorage.getItem("accessToken"); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/user/export-qrcode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: JSON.stringify({ userIds }), + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error(`Failed to export QR code: ${response.status} ${response.statusText}`); + } + + const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "user_qrcode.pdf"; + + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + const blobValue = new Uint8Array(arrayBuffer); + + return { blobValue, filename }; +}; \ No newline at end of file diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts index f19439f..a55c6c2 100644 --- a/src/app/api/user/index.ts +++ b/src/app/api/user/index.ts @@ -7,6 +7,7 @@ export interface UserResult { action: any; id: number; username: string; + staffNo: number; // name: string; } @@ -71,3 +72,26 @@ export const fetchEscalationCombo = cache(async () => { next: { tags: ["escalationCombo"]} }) }) + + +export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => { + const response = await fetch(`${BASE_API_URL}/user/export-qrcode`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userIds }), + }); + + if (!response.ok) { + throw new Error("Failed to export QR code"); + } + + const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "user_qrcode.pdf"; + + const blob = await response.blob(); + const arrayBuffer = await blob.arrayBuffer(); + const blobValue = new Uint8Array(arrayBuffer); + + return { blobValue, filename }; +}; \ No newline at end of file diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 1022e26..be6f8c7 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -14,6 +14,7 @@ const pathToLabelMap: { [path: string]: string } = { "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/settings/qcItem": "Qc Item", + "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", "/scheduling/rough": "Demand Forecast", "/scheduling/rough/edit": "FG & Material Demand Forecast Detail", diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index 6da92b5..12e17c3 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -12,6 +12,7 @@ import { SearchProdSchedule, fetchDetailedProdSchedules, fetchProdSchedules, + exportProdSchedule, testDetailedSchedule, } from "@/app/api/scheduling/actions"; import { defaultPagingController } from "../SearchResults/SearchResults"; @@ -21,6 +22,7 @@ import { orderBy, uniqBy, upperFirst } from "lodash"; import { Button, Stack } from "@mui/material"; import isToday from 'dayjs/plugin/isToday'; import useUploadContext from "../UploadProvider/useUploadContext"; +import { FileDownload, CalendarMonth } from "@mui/icons-material"; dayjs.extend(isToday); // may need move to "index" or "actions" @@ -298,21 +300,68 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { } }, [inputs]) + const exportProdScheduleClick = async () => { + try { + const token = localStorage.getItem("accessToken"); + // 1. Get Base64 string from server + const base64String = await exportProdSchedule(token); + + // 2. Convert Base64 back to Blob + const byteCharacters = atob(base64String); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + const blob = new Blob([byteArray], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + }); + + // 3. Trigger download (same as before) + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "production_schedule.xlsx"; + link.click(); + window.URL.revokeObjectURL(url); + + } catch (error) { + console.error(error); + alert("Export failed. Check the console for details."); + } + }; + return ( <> + + = ({ } }, [scheduleId, setIsUploading, t, router]); // -------------------------------------------------------------------- - - + const [tempValue, setTempValue] = useState(null) const onEditClick = useCallback((rowId: number) => { const row = formProps.getValues("prodScheduleLines").find(ele => ele.id == rowId) @@ -298,7 +297,7 @@ const DetailedScheduleDetailView: React.FC = ({ onClick={onGlobalReleaseClick} disabled={!scheduleId} // Disable if we don't have a schedule ID > - {t("放單(自動生成工單)")} + {t("生成工單")} {/* ------------------------------------------- */} diff --git a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx index 80a6472..9f3c6e1 100644 --- a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx +++ b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx @@ -32,6 +32,7 @@ const DetailedScheduleDetailWrapper: React.FC & SubComponents = async ({ stockQty: line.stockQty || 0, daysLeft: line.daysLeft || 0, needNoOfJobOrder: line.needNoOfJobOrder || 0, + outputQty: line.outputQty || 0, })).sort((a, b) => b.priority - a.priority); } diff --git a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx index a4fa909..151da2b 100644 --- a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx @@ -108,9 +108,16 @@ const ViewByFGDetails: React.FC = ({ style: { textAlign: "right" } as any, renderCell: (row) => <>{row.daysLeft ?? 0}, }, + { + field: "outputQty", + label: t("每批次生產數"), + type: "read-only", + style: { textAlign: "right", fontWeight: "bold" } as any, + renderCell: (row) => <>{row.outputQty ?? 0}, + }, { field: "needNoOfJobOrder", - label: t("生產量"), + label: t("生產批次"), type: "read-only", style: { textAlign: "right", fontWeight: "bold" } as any, renderCell: (row) => <>{row.needNoOfJobOrder ?? 0}, @@ -137,7 +144,7 @@ const ViewByFGDetails: React.FC = ({ isEditable={true} isEdit={isEdit} hasCollapse={true} - // Note: onReleaseClick is NOT passed here to hide the row-level "Release" function + onReleaseClick={onReleaseClick} onEditClick={onEditClick} handleEditChange={handleEditChange} onSaveClick={onSaveClick} diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index 23a7ffd..910ab79 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -47,7 +47,7 @@ interface Props { auths: auth[]; } -const EditUser: React.FC = async ({ user, rules, auths }) => { +const EditUser: React.FC = ({ user, rules, auths }) => { console.log(user); const { t } = useTranslation("user"); const formProps = useForm(); @@ -73,28 +73,31 @@ const EditUser: React.FC = async ({ user, rules, auths }) => { const errors = formProps.formState.errors; - const resetForm = React.useCallback(() => { + const resetForm = React.useCallback((e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); console.log("triggerred"); console.log(addAuthIds); try { formProps.reset({ username: user.username, - // name: user.name, - // email: user.email, + staffNo: user.staffNo?.toString() ??"", addAuthIds: addAuthIds, removeAuthIds: [], password: "", + confirmPassword: "", }); + formProps.clearErrors(); console.log(formProps.formState.defaultValues); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }, [auths, user]); + }, [formProps, auths, user, addAuthIds, t]); useEffect(() => { resetForm(); - }, []); + }, [user.id]); const hasErrorsInTab = ( tabIndex: number, @@ -146,6 +149,7 @@ const EditUser: React.FC = async ({ user, rules, auths }) => { } const userData = { username: data.username, + staffNo: data.staffNo, // name: user.name, locked: false, addAuthIds: data.addAuthIds || [], @@ -218,17 +222,25 @@ const EditUser: React.FC = async ({ user, rules, auths }) => { {tabIndex == 0 && } {tabIndex === 1 && } - + + + diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx index 42d1754..9ac1776 100644 --- a/src/components/EditUser/UserDetail.tsx +++ b/src/components/EditUser/UserDetail.tsx @@ -20,8 +20,14 @@ const UserDetail: React.FC = () => { register, formState: { errors }, control, + watch, } = useFormContext(); + const password = watch("password"); + const confirmPassword = watch("confirmPassword"); + const username = watch("username"); + const staffNo = watch("staffNo"); + return ( @@ -33,35 +39,55 @@ const UserDetail: React.FC = () => { + + + - // - 8-20 characters - //
- // - Uppercase letters - //
- // - Lowercase letters - //
- // - Numbers - //
- // - Symbols - // ) - // ) - // } helperText={ Boolean(errors.password) && (errors.password?.message @@ -71,6 +97,36 @@ const UserDetail: React.FC = () => { error={Boolean(errors.password)} />
+ + { + if (password && value !== password) { + return "Passwords do not match"; + } + return true; + }, + })} + error={Boolean(errors.confirmPassword)} + helperText={ + Boolean(errors.confirmPassword) && + (errors.confirmPassword?.message + ? t(errors.confirmPassword.message) + : "") + } + /> + {/* { export default UserDetail; -{ - /* <> - - 8-20 characters -
- - Uppercase letters -
- - Lowercase letters -
- - Numbers -
- - Symbols - */ -} diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index bf56617..812e3e3 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -17,6 +17,7 @@ import Assignment from "@mui/icons-material/Assignment"; import Settings from "@mui/icons-material/Settings"; import Analytics from "@mui/icons-material/Analytics"; import Payments from "@mui/icons-material/Payments"; +import QrCodeIcon from "@mui/icons-material/QrCode"; import { useTranslation } from "react-i18next"; import Typography from "@mui/material/Typography"; import { usePathname } from "next/navigation"; @@ -268,11 +269,11 @@ const NavigationContent: React.FC = () => { label: "Demand Forecast Setting", path: "/settings/rss", }, - { - icon: , - label: "Equipment Type", - path: "/settings/equipmentType", - }, + //{ + // icon: , + // label: "Equipment Type", + // path: "/settings/equipmentType", + //}, { icon: , label: "Equipment", @@ -308,6 +309,11 @@ const NavigationContent: React.FC = () => { label: "QC Check Template", path: "/settings/user", }, + { + icon: , + label: "QR Code Handle", + path: "/settings/qrCodeHandle", + }, // { // icon: , // label: "Mail", diff --git a/src/components/ScheduleTable/ScheduleTable.tsx b/src/components/ScheduleTable/ScheduleTable.tsx index d7d575e..db83752 100644 --- a/src/components/ScheduleTable/ScheduleTable.tsx +++ b/src/components/ScheduleTable/ScheduleTable.tsx @@ -227,7 +227,7 @@ function ScheduleTable({ return ( <> - {/*isDetailedType(type) && ( + {isDetailedType(type) && ( ({ - )*/} + )} {(isEditable || hasCollapse) && ( {editingRowId === row.id ? ( diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index 104c8c8..6797da2 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -8,8 +8,11 @@ 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 useUploadContext from "../UploadProvider/useUploadContext"; +import { downloadFile } from "@/app/utils/commonUtil"; import { UserResult } from "@/app/api/user"; import { deleteUser } from "@/app/api/user/actions"; +import QrCodeIcon from "@mui/icons-material/QrCode"; import UserSearchLoading from "./UserSearchLoading"; interface Props { @@ -22,7 +25,12 @@ type SearchParamNames = keyof SearchQuery; const UserSearch: React.FC = ({ users }) => { const { t } = useTranslation("user"); const [filteredUser, setFilteredUser] = useState(users); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); const router = useRouter(); + const { setIsUploading } = useUploadContext(); const searchCriteria: Criterion[] = useMemo( () => [ @@ -31,6 +39,11 @@ const UserSearch: React.FC = ({ users }) => { paramName: "username", type: "text", }, + { + label: t("staffNo"), + paramName: "staffNo", + type: "text", + }, ], [t], ); @@ -43,6 +56,20 @@ const UserSearch: React.FC = ({ users }) => { [router, t], ); + {/* + const printQrcode = useCallback(async (lotLineId: number) => { + setIsUploading(true); + // const postData = { stockInLineIds: [42,43,44] }; + const postData: LotLineToQrcode = { + inventoryLotLineId: lotLineId + } + const response = await fetchQrCodeByLotLineId(postData); + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!); + } + setIsUploading(false); + }, [setIsUploading]); +*/} const onDeleteClick = useCallback((users: UserResult) => { deleteDialog(async () => { await deleteUser(users.id); @@ -50,6 +77,9 @@ const UserSearch: React.FC = ({ users }) => { }, t); }, []); + + + const columns = useMemo[]>( () => [ { @@ -59,6 +89,7 @@ const UserSearch: React.FC = ({ users }) => { buttonIcon: , }, { name: "username", label: t("Username") }, + { name: "staffNo", label: t("staffNo") }, { name: "action", label: t("Delete"), @@ -75,20 +106,23 @@ const UserSearch: React.FC = ({ users }) => { { - // setFilteredUser( - // users.filter( - // (t) => - // t.name.toLowerCase().includes(query.name.toLowerCase()) && - // t.code.toLowerCase().includes(query.code.toLowerCase()) && - // t.description.toLowerCase().includes(query.description.toLowerCase()) - // ) - // ) + setFilteredUser( + users.filter((user) => { + const usernameMatch = !query.username || + user.username.toLowerCase().includes(query.username.toLowerCase()); + const staffNoMatch = !query.staffNo || + String(user.staffNo).includes(String(query.staffNo)); + return usernameMatch && staffNoMatch; + }) + ); + setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); }} /> items={filteredUser} columns={columns} - pagingController={{ pageNum: 1, pageSize: 10 }} + pagingController={pagingController} + setPagingController={setPagingController} /> ); diff --git a/src/components/UserSearch/index.ts b/src/components/UserSearch/index.ts index c2e98d7..21b052b 100644 --- a/src/components/UserSearch/index.ts +++ b/src/components/UserSearch/index.ts @@ -1 +1,2 @@ export { default } from "./UserSearchWrapper"; + diff --git a/src/components/qrCodeHandles/index.ts b/src/components/qrCodeHandles/index.ts new file mode 100644 index 0000000..122561e --- /dev/null +++ b/src/components/qrCodeHandles/index.ts @@ -0,0 +1,3 @@ +export { default } from "./qrCodeHandleSearchWrapper"; +export { default as QrCodeHandleSearch } from "./qrCodeHandleSearch"; +export { default as QrCodeHandleEquipmentSearch } from "./qrCodeHandleEquipmentSearch"; diff --git a/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx new file mode 100644 index 0000000..87d5df1 --- /dev/null +++ b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearch.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { EquipmentResult } from "@/app/api/settings/equipment"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import { EditNote } from "@mui/icons-material"; +import { useRouter } from "next/navigation"; +import { GridDeleteIcon } from "@mui/x-data-grid"; +import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; + +type Props = { + equipments: EquipmentResult[]; +}; + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const QrCodeHandleEquipmentSearch: React.FC = ({ equipments }) => { + const [filteredEquipments, setFilteredEquipments] = + useState([]); + const { t } = useTranslation("common"); + const router = useRouter(); + const [filterObj, setFilterObj] = useState({}); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + const [totalCount, setTotalCount] = useState(0); + + const searchCriteria: Criterion[] = useMemo(() => { + const searchCriteria: Criterion[] = [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Description"), paramName: "description", type: "text" }, + ]; + return searchCriteria; + }, [t, equipments]); + + const onDetailClick = useCallback( + (equipment: EquipmentResult) => { + router.push(`/settings/equipment/edit?id=${equipment.id}`); + }, + [router], + ); + + const onDeleteClick = useCallback( + (equipment: EquipmentResult) => {}, + [router], + ); + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onDetailClick, + buttonIcon: , + }, + { + name: "code", + label: t("Code"), + }, + { + name: "equipmentTypeId", + label: t("Equipment Type"), + sx: {minWidth: 180}, + }, + { + name: "description", + label: t("Description"), + }, + { + name: "action", + label: t(""), + buttonIcon: , + onClick: onDeleteClick, + }, + ], + [filteredEquipments], + ); + + interface ApiResponse { + records: T[]; + total: number; + } + + const refetchData = useCallback( + async (filterObj: SearchQuery, pageNum: number, pageSize: number) => { + const authHeader = axiosInstance.defaults.headers["Authorization"]; + if (!authHeader) { + setTimeout(() => { + refetchData(filterObj, pageNum, pageSize); + }, 10); + return; + } + const params = { + pageNum: pageNum, + pageSize: pageSize, + ...filterObj, + }; + try { + const response = await axiosInstance.get>( + `${NEXT_PUBLIC_API_URL}/Equipment/getRecordByPage`, + { params }, + ); + console.log(response); + if (response.status == 200) { + setFilteredEquipments(response.data.records); + setTotalCount(response.data.total); + return response; + } else { + throw "400"; + } + } catch (error) { + console.error("Error fetching equipment types:", error); + throw error; + } + }, + [], + ); + + useEffect(() => { + refetchData(filterObj, pagingController.pageNum, pagingController.pageSize); + }, [filterObj, pagingController.pageNum, pagingController.pageSize]); + + const onReset = useCallback(() => { + setFilterObj({}); + setPagingController({ pageNum: 1, pageSize: 10 }); + }, []); + + return ( + <> + { + setFilterObj({ + ...query, + }); + }} + onReset={onReset} + /> + + items={filteredEquipments} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + totalCount={totalCount} + isAutoPaging={false} + /> + + ); +}; + +export default QrCodeHandleEquipmentSearch; \ No newline at end of file diff --git a/src/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper.tsx b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper.tsx new file mode 100644 index 0000000..3393927 --- /dev/null +++ b/src/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import QrCodeHandleEquipmentSearch from "./qrCodeHandleEquipmentSearch"; +import EquipmentSearchLoading from "../EquipmentSearch/EquipmentSearchLoading"; +import { fetchAllEquipments } from "@/app/api/settings/equipment"; + +interface SubComponents { + Loading: typeof EquipmentSearchLoading; +} + +const QrCodeHandleEquipmentSearchWrapper: React.FC & SubComponents = async () => { + const equipments = await fetchAllEquipments(); + return ; +}; + +QrCodeHandleEquipmentSearchWrapper.Loading = EquipmentSearchLoading; + +export default QrCodeHandleEquipmentSearchWrapper; \ No newline at end of file diff --git a/src/components/qrCodeHandles/qrCodeHandleSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx new file mode 100644 index 0000000..70cd478 --- /dev/null +++ b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx @@ -0,0 +1,507 @@ +"use client"; + +import SearchBox, { Criterion } from "../SearchBox"; +import { useCallback, useMemo, useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import { downloadFile } from "@/app/utils/commonUtil"; +import { UserResult } from "@/app/api/user"; +import { deleteUser } from "@/app/api/user/actions"; +import QrCodeIcon from "@mui/icons-material/QrCode"; +import { exportUserQrCode } from "@/app/api/user/client"; +import { + Checkbox, + Box, + Button, + TextField, + Stack, + Autocomplete, + Modal, + Card, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Typography +} from "@mui/material"; +import DownloadIcon from "@mui/icons-material/Download"; +import PrintIcon from "@mui/icons-material/Print"; +import CloseIcon from "@mui/icons-material/Close"; +import { PrinterCombo } from "@/app/api/settings/printer"; + +interface Props { + users: UserResult[]; + printerCombo: PrinterCombo[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { + const { t } = useTranslation("user"); + const [filteredUser, setFilteredUser] = useState(users); + const router = useRouter(); + const { setIsUploading } = useUploadContext(); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + + const [checkboxIds, setCheckboxIds] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [printQty, setPrintQty] = useState(1); + + const [previewOpen, setPreviewOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + + const [selectedUsersModalOpen, setSelectedUsersModalOpen] = useState(false); + + const filteredPrinters = useMemo(() => { + return printerCombo.filter((printer) => { + return printer.type === "A4"; + }); + }, [printerCombo]); + + const [selectedPrinter, setSelectedPrinter] = useState( + filteredPrinters.length > 0 ? filteredPrinters[0] : undefined + ); + + useEffect(() => { + if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) { + setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined); + } + }, [filteredPrinters, selectedPrinter]); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Username"), + paramName: "username", + type: "text", + }, + { + label: t("staffNo"), + paramName: "staffNo", + type: "text", + }, + ], + [t], + ); + + const onDeleteClick = useCallback((user: UserResult) => { + deleteDialog(async () => { + await deleteUser(user.id); + successDialog(t("Delete Success"), t); + }, t); + }, [t]); + + const handleSelectUser = useCallback((userId: number, checked: boolean) => { + if (checked) { + setCheckboxIds(prev => [...prev, userId]); + } else { + setCheckboxIds(prev => prev.filter(id => id !== userId)); + setSelectAll(false); + } + }, []); + + const handleSelectAll = useCallback((checked: boolean) => { + if (checked) { + setCheckboxIds(filteredUser.map(user => user.id)); + setSelectAll(true); + } else { + setCheckboxIds([]); + setSelectAll(false); + } + }, [filteredUser]); + + + const showPdfPreview = useCallback(async (userIds: number[]) => { + if (userIds.length === 0) { + return; + } + try { + setIsUploading(true); + const response = await exportUserQrCode(userIds); + + const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" }); + const url = URL.createObjectURL(blob); + + setPreviewUrl(`${url}#toolbar=0`); + setPreviewOpen(true); + } catch (error) { + console.error("Error exporting QR code:", error); + } finally { + setIsUploading(false); + } + }, [setIsUploading]); + + + const handleClosePreview = useCallback(() => { + setPreviewOpen(false); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + setPreviewUrl(null); + } + }, [previewUrl]); + + const handleDownloadQrCode = useCallback(async (userIds: number[]) => { + if (userIds.length === 0) { + return; + } + try { + setIsUploading(true); + const response = await exportUserQrCode(userIds); + downloadFile(response.blobValue, response.filename); + setSelectedUsersModalOpen(false); + successDialog("二維碼已下載", t); + } catch (error) { + console.error("Error exporting QR code:", error); + } finally { + setIsUploading(false); + } + }, [setIsUploading, t]); + + const handlePrint = useCallback(async () => { + if (checkboxIds.length === 0) { + return; + } + try { + setIsUploading(true); + const response = await exportUserQrCode(checkboxIds); + + const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" }); + const url = URL.createObjectURL(blob); + + const printWindow = window.open(url, '_blank'); + if (printWindow) { + printWindow.onload = () => { + for (let i = 0; i < printQty; i++) { + setTimeout(() => { + printWindow.print(); + }, i * 500); + } + }; + } + + setTimeout(() => { + URL.revokeObjectURL(url); + }, 1000); + setSelectedUsersModalOpen(false); + successDialog("二維碼已列印", t); + } catch (error) { + console.error("Error printing QR code:", error); + } finally { + setIsUploading(false); + } + }, [checkboxIds, printQty, setIsUploading, t]); + + const handleViewSelectedQrCodes = useCallback(() => { + if (checkboxIds.length === 0) { + return; + } + setSelectedUsersModalOpen(true); + }, [checkboxIds]); + + const selectedUsers = useMemo(() => { + return users.filter(user => checkboxIds.includes(user.id)); + }, [users, checkboxIds]); + + const handleCloseSelectedUsersModal = useCallback(() => { + setSelectedUsersModalOpen(false); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: "", + renderCell: (params) => ( + handleSelectUser(params.id, e.target.checked)} + onClick={(e) => e.stopPropagation()} + /> + ), + }, + { + name: "username", + label: t("Username"), + align: "left", + headerAlign: "left", + }, + { + name: "staffNo", + label: t("staffNo"), + align: "left", + headerAlign: "left", + }, + { + name: "staffNo", + label: t("qrcode"), + align: "center", + headerAlign: "center", + onClick: async (user: UserResult) => { + await showPdfPreview([user.id]); + }, + buttonIcon: , + }, + ], + [t, checkboxIds, handleSelectUser, showPdfPreview], + ); + + const onReset = useCallback(() => { + setFilteredUser(users); + }, [users]); + + return ( + <> + { + setFilteredUser( + users.filter((user) => { + const usernameMatch = !query.username || + user.username?.toLowerCase().includes(query.username?.toLowerCase() || ""); + const staffNoMatch = !query.staffNo || + user.staffNo?.toString().includes(query.staffNo?.toString() || ""); + return usernameMatch && staffNoMatch; + }) + ); + setPagingController({ pageNum: 1, pageSize: 10 }); + }} + onReset={onReset} + /> + + items={filteredUser} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={filteredUser.length} + isAutoPaging={true} + /> + + + + + + + + + + 已選擇用戶 ({selectedUsers.length}) + + + + + + + + + + + + + {t("Username")} + + + {t("staffNo")} + + + + + {selectedUsers.length === 0 ? ( + + + 沒有選擇的用戶 + + + ) : ( + selectedUsers.map((user) => ( + + {user.username || '-'} + {user.staffNo || '-'} + + )) + )} + +
+
+
+ + + + + options={filteredPrinters} + value={selectedPrinter ?? null} + onChange={(event, value) => { + setSelectedPrinter(value ?? undefined); + }} + getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)} + renderInput={(params) => ( + + )} + /> + { + const value = parseInt(e.target.value) || 1; + setPrintQty(Math.max(1, value)); + }} + inputProps={{ min: 1 }} + sx={{ width: 120 }} + /> + + + + +
+
+ + + + + + + + + + + {previewUrl && ( +