From 1d92556988587dc9dc0375fcc23f4703161474c9 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Wed, 31 Dec 2025 12:19:44 +0800 Subject: [PATCH] Update for the user page --- src/app/api/user/actions.ts | 6 +- src/app/api/user/client.ts | 79 +++++++++++++++ src/components/EditUser/EditUser.tsx | 9 +- src/components/EditUser/UserDetail.tsx | 56 ++++------- src/components/UserSearch/UserSearch.tsx | 96 ++++++++++++------- .../qrCodeHandles/qrCodeHandleSearch.tsx | 54 ++++++++--- 6 files changed, 207 insertions(+), 93 deletions(-) diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 6ec65d3..5341269 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -1,7 +1,5 @@ "use server"; -// import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; -// import { BASE_API_URL } from "@/config/api"; import { serverFetchJson, serverFetchWithNoContent, @@ -13,7 +11,7 @@ import { cache } from "react"; export interface UserInputs { username: string; - // name: string; + name: string; staffNo?: string; addAuthIds?: number[]; removeAuthIds?: number[]; @@ -58,7 +56,7 @@ export const fetchNewNameList = cache(async () => { }); export const editUser = async (id: number, data: UserInputs) => { - const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { + const newUser = await serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { method: "PUT", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/user/client.ts b/src/app/api/user/client.ts index 1e734b9..ecfe62e 100644 --- a/src/app/api/user/client.ts +++ b/src/app/api/user/client.ts @@ -1,6 +1,7 @@ "use client"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { UserResult } from "./index"; export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => { @@ -29,4 +30,82 @@ export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: const blobValue = new Uint8Array(arrayBuffer); return { blobValue, filename }; +}; + +export const searchUsersByUsernameOrName = async (searchTerm: string): Promise => { + if (!searchTerm.trim()) { + return []; + } + + const token = localStorage.getItem("accessToken"); + + const [usernameResults, nameResults] = await Promise.all([ + fetch(`${NEXT_PUBLIC_API_URL}/user?username=${encodeURIComponent(searchTerm)}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + }).then(res => { + if (!res.ok) { + if (res.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error("Failed to search by username"); + } + return res.json(); + }), + fetch(`${NEXT_PUBLIC_API_URL}/user?name=${encodeURIComponent(searchTerm)}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + }).then(res => { + if (!res.ok) { + if (res.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error("Failed to search by name"); + } + return res.json(); + }), + ]); + + const mergedResults = [...usernameResults, ...nameResults]; + const uniqueResults = mergedResults.filter( + (user, index, self) => index === self.findIndex((u) => u.id === user.id) + ); + + return uniqueResults; +}; + +export const searchUsers = async (searchParams: { + username?: string; + name?: string; + staffNo?: string; +}): Promise => { + const token = localStorage.getItem("accessToken"); + + const params = new URLSearchParams(); + if (searchParams.username) params.append("username", searchParams.username); + if (searchParams.name) params.append("name", searchParams.name); + if (searchParams.staffNo) params.append("staffNo", searchParams.staffNo); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/user?${params.toString()}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error(`Failed to search users: ${response.status} ${response.statusText}`); + } + + return response.json(); }; \ No newline at end of file diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index 910ab79..6da4b62 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -7,7 +7,6 @@ import React, { useMemo, useState, } from "react"; -// import { TeamResult } from "@/app/api/team"; import { useTranslation } from "react-i18next"; import { Button, @@ -81,11 +80,11 @@ const EditUser: React.FC = ({ user, rules, auths }) => { try { formProps.reset({ username: user.username, - staffNo: user.staffNo?.toString() ??"", + name: user.name, + staffNo: user.staffNo?.toString() ?? "", addAuthIds: addAuthIds, removeAuthIds: [], password: "", - confirmPassword: "", }); formProps.clearErrors(); console.log(formProps.formState.defaultValues); @@ -149,8 +148,8 @@ const EditUser: React.FC = ({ user, rules, auths }) => { } const userData = { username: data.username, + name: data.name, staffNo: data.staffNo, - // name: user.name, locked: false, addAuthIds: data.addAuthIds || [], removeAuthIds: data.removeAuthIds || [], @@ -253,4 +252,4 @@ const EditUser: React.FC = ({ user, rules, auths }) => { ); }; -export default EditUser; +export default EditUser; \ No newline at end of file diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx index 9ac1776..fec50f7 100644 --- a/src/components/EditUser/UserDetail.tsx +++ b/src/components/EditUser/UserDetail.tsx @@ -24,9 +24,9 @@ const UserDetail: React.FC = () => { } = useFormContext(); const password = watch("password"); - const confirmPassword = watch("confirmPassword"); const username = watch("username"); const staffNo = watch("staffNo"); + const name = watch("name"); return ( @@ -76,72 +76,54 @@ const UserDetail: React.FC = () => { { - if (password && value !== password) { - return "Passwords do not match"; - } - return true; - }, - })} - error={Boolean(errors.confirmPassword)} + {...register("password")} helperText={ - Boolean(errors.confirmPassword) && - (errors.confirmPassword?.message - ? t(errors.confirmPassword.message) - : "") + Boolean(errors.password) && + (errors.password?.message + ? t(errors.password.message) + : t("Please input correct password")) } + error={Boolean(errors.password)} /> - {/* - - */} ); }; -export default UserDetail; - +export default UserDetail; \ No newline at end of file diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index 6797da2..30af9cb 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -14,6 +14,7 @@ import { UserResult } from "@/app/api/user"; import { deleteUser } from "@/app/api/user/actions"; import QrCodeIcon from "@mui/icons-material/QrCode"; import UserSearchLoading from "./UserSearchLoading"; +import { searchUsersByUsernameOrName } from "@/app/api/user/client"; interface Props { users: UserResult[]; @@ -31,11 +32,12 @@ const UserSearch: React.FC = ({ users }) => { }); const router = useRouter(); const { setIsUploading } = useUploadContext(); + const [isSearching, setIsSearching] = useState(false); const searchCriteria: Criterion[] = useMemo( () => [ { - label: t("Username"), + label: "用戶/姓名", paramName: "username", type: "text", }, @@ -56,29 +58,12 @@ 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); successDialog(t("Delete Success"), t); }, t); - }, []); - - - + }, [t]); const columns = useMemo[]>( () => [ @@ -87,35 +72,78 @@ const UserSearch: React.FC = ({ users }) => { label: t("Edit"), onClick: onUserClick, buttonIcon: , + sx: { width: "10%", minWidth: "80px" }, + }, + { + name: "username", + label: t("Username"), + align: "left", + headerAlign: "left", + sx: { width: "22.5%", minWidth: "120px" }, + }, + { + name: "name", + label: t("name"), + align: "left", + headerAlign: "left", + sx: { width: "22.5%", minWidth: "120px" }, + }, + { + name: "staffNo", + label: t("staffNo"), + align: "left", + headerAlign: "left", + sx: { width: "22.5%", minWidth: "120px" }, }, - { name: "username", label: t("Username") }, - { name: "staffNo", label: t("staffNo") }, { name: "action", label: t("Delete"), onClick: onDeleteClick, buttonIcon: , color: "error", + sx: { width: "10%", minWidth: "80px" }, }, ], - [t], + [t, onUserClick, onDeleteClick], ); return ( <> { - 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 }); + onSearch={async (query) => { + setIsSearching(true); + try { + let results: UserResult[] = []; + + if (query.username && query.username.trim()) { + results = await searchUsersByUsernameOrName(query.username); + } else { + results = users; + } + if (query.staffNo && query.staffNo.trim()) { + results = results.filter((user) => + user.staffNo?.toString().includes(query.staffNo?.toString() || "") + ); + } + + setFilteredUser(results); + setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); + } catch (error) { + console.error("Error searching users:", error); + setFilteredUser( + users.filter((user) => { + const userMatch = !query.username || + user.username?.toLowerCase().includes(query.username?.toLowerCase() || "") || + user.name?.toLowerCase().includes(query.username?.toLowerCase() || ""); + const staffNoMatch = !query.staffNo || + user.staffNo?.toString().includes(query.staffNo?.toString() || ""); + return userMatch && staffNoMatch; + }) + ); + } finally { + setIsSearching(false); + } }} /> @@ -127,4 +155,4 @@ const UserSearch: React.FC = ({ users }) => { ); }; -export default UserSearch; +export default UserSearch; \ No newline at end of file diff --git a/src/components/qrCodeHandles/qrCodeHandleSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx index 977cabe..0032ca3 100644 --- a/src/components/qrCodeHandles/qrCodeHandleSearch.tsx +++ b/src/components/qrCodeHandles/qrCodeHandleSearch.tsx @@ -12,7 +12,7 @@ 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 { exportUserQrCode, searchUsersByUsernameOrName } from "@/app/api/user/client"; import { Checkbox, Box, @@ -58,6 +58,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { const [checkboxIds, setCheckboxIds] = useState([]); const [selectAll, setSelectAll] = useState(false); const [printQty, setPrintQty] = useState(1); + const [isSearching, setIsSearching] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const [previewUrl, setPreviewUrl] = useState(null); @@ -83,7 +84,7 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { const searchCriteria: Criterion[] = useMemo( () => [ { - label: t("User"), + label: "用戶/姓名", paramName: "username", type: "text", }, @@ -265,17 +266,44 @@ const QrCodeHandleSearch: React.FC = ({ users, printerCombo }) => { <> { - 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 }); + onSearch={async (query) => { + setIsSearching(true); + try { + let results: UserResult[] = []; + + if (query.username && query.username.trim()) { + // Search by username OR name from database + results = await searchUsersByUsernameOrName(query.username); + } else { + // If no username search, start with all users + results = users; + } + + // Then filter by staffNo if provided (client-side filtering) + if (query.staffNo && query.staffNo.trim()) { + results = results.filter((user) => + user.staffNo?.toString().includes(query.staffNo?.toString() || "") + ); + } + + setFilteredUser(results); + setPagingController({ pageNum: 1, pageSize: 10 }); + } catch (error) { + console.error("Error searching users:", error); + // Fallback to client-side filtering on error + setFilteredUser( + users.filter((user) => { + const userMatch = !query.username || + user.username?.toLowerCase().includes(query.username?.toLowerCase() || "") || + user.name?.toLowerCase().includes(query.username?.toLowerCase() || ""); + const staffNoMatch = !query.staffNo || + user.staffNo?.toString().includes(query.staffNo?.toString() || ""); + return userMatch && staffNoMatch; + }) + ); + } finally { + setIsSearching(false); + } }} onReset={onReset} />