| @@ -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" }, | |||
| @@ -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<UserResult[]> => { | |||
| 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<UserResult[]> => { | |||
| 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(); | |||
| }; | |||
| @@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ user, rules, auths }) => { | |||
| </> | |||
| ); | |||
| }; | |||
| export default EditUser; | |||
| export default EditUser; | |||
| @@ -24,9 +24,9 @@ const UserDetail: React.FC = () => { | |||
| } = useFormContext<UserInputs>(); | |||
| const password = watch("password"); | |||
| const confirmPassword = watch("confirmPassword"); | |||
| const username = watch("username"); | |||
| const staffNo = watch("staffNo"); | |||
| const name = watch("name"); | |||
| return ( | |||
| <Card> | |||
| @@ -76,72 +76,54 @@ const UserDetail: React.FC = () => { | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("password")} | |||
| label={t("name")} | |||
| fullWidth | |||
| type="password" | |||
| variant="filled" | |||
| InputLabelProps={{ | |||
| shrink: !!password, | |||
| shrink: !!name, | |||
| sx: { fontSize: "0.9375rem" }, | |||
| }} | |||
| InputProps={{ | |||
| sx: { paddingTop: "8px" }, | |||
| }} | |||
| {...register("password")} | |||
| {...register("name", { | |||
| required: "name required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| helperText={ | |||
| Boolean(errors.password) && | |||
| (errors.password?.message | |||
| ? t(errors.password.message) | |||
| : t("Please input correct password")) | |||
| Boolean(errors.name) && errors.name?.message | |||
| ? t(errors.name.message) | |||
| : "" | |||
| } | |||
| error={Boolean(errors.password)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Confirm Password")} | |||
| label={t("password")} | |||
| fullWidth | |||
| type="password" | |||
| variant="filled" | |||
| InputLabelProps={{ | |||
| shrink: !!confirmPassword, | |||
| shrink: !!password, | |||
| sx: { fontSize: "0.9375rem" }, | |||
| }} | |||
| InputProps={{ | |||
| sx: { paddingTop: "8px" }, | |||
| }} | |||
| {...register("confirmPassword", { | |||
| validate: (value) => { | |||
| 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)} | |||
| /> | |||
| </Grid> | |||
| {/* <Grid item xs={6}> | |||
| <TextField | |||
| label={t("name")} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: "name required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| /> | |||
| </Grid> */} | |||
| </Grid> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default UserDetail; | |||
| export default UserDetail; | |||
| @@ -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<Props> = ({ users }) => { | |||
| }); | |||
| const router = useRouter(); | |||
| const { setIsUploading } = useUploadContext(); | |||
| const [isSearching, setIsSearching] = useState(false); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| { | |||
| label: t("Username"), | |||
| label: "用戶/姓名", | |||
| paramName: "username", | |||
| type: "text", | |||
| }, | |||
| @@ -56,29 +58,12 @@ const UserSearch: React.FC<Props> = ({ 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<Column<UserResult>[]>( | |||
| () => [ | |||
| @@ -87,35 +72,78 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||
| label: t("Edit"), | |||
| onClick: onUserClick, | |||
| buttonIcon: <EditNote />, | |||
| 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: <DeleteIcon />, | |||
| color: "error", | |||
| sx: { width: "10%", minWidth: "80px" }, | |||
| }, | |||
| ], | |||
| [t], | |||
| [t, onUserClick, onDeleteClick], | |||
| ); | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| 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); | |||
| } | |||
| }} | |||
| /> | |||
| <SearchResults<UserResult> | |||
| @@ -127,4 +155,4 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||
| </> | |||
| ); | |||
| }; | |||
| export default UserSearch; | |||
| export default UserSearch; | |||
| @@ -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<Props> = ({ users, printerCombo }) => { | |||
| const [checkboxIds, setCheckboxIds] = useState<number[]>([]); | |||
| const [selectAll, setSelectAll] = useState(false); | |||
| const [printQty, setPrintQty] = useState(1); | |||
| const [isSearching, setIsSearching] = useState(false); | |||
| const [previewOpen, setPreviewOpen] = useState(false); | |||
| const [previewUrl, setPreviewUrl] = useState<string | null>(null); | |||
| @@ -83,7 +84,7 @@ const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => { | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| { | |||
| label: t("User"), | |||
| label: "用戶/姓名", | |||
| paramName: "username", | |||
| type: "text", | |||
| }, | |||
| @@ -265,17 +266,44 @@ const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => { | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| 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} | |||
| /> | |||