| @@ -1,7 +1,5 @@ | |||||
| "use server"; | "use server"; | ||||
| // import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
| // import { BASE_API_URL } from "@/config/api"; | |||||
| import { | import { | ||||
| serverFetchJson, | serverFetchJson, | ||||
| serverFetchWithNoContent, | serverFetchWithNoContent, | ||||
| @@ -13,7 +11,7 @@ import { cache } from "react"; | |||||
| export interface UserInputs { | export interface UserInputs { | ||||
| username: string; | username: string; | ||||
| // name: string; | |||||
| name: string; | |||||
| staffNo?: string; | staffNo?: string; | ||||
| addAuthIds?: number[]; | addAuthIds?: number[]; | ||||
| removeAuthIds?: number[]; | removeAuthIds?: number[]; | ||||
| @@ -58,7 +56,7 @@ export const fetchNewNameList = cache(async () => { | |||||
| }); | }); | ||||
| export const editUser = async (id: number, data: UserInputs) => { | 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", | method: "PUT", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| @@ -1,6 +1,7 @@ | |||||
| "use client"; | "use client"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { UserResult } from "./index"; | |||||
| export const exportUserQrCode = async (userIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => { | 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); | const blobValue = new Uint8Array(arrayBuffer); | ||||
| return { blobValue, filename }; | 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, | useMemo, | ||||
| useState, | useState, | ||||
| } from "react"; | } from "react"; | ||||
| // import { TeamResult } from "@/app/api/team"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| Button, | Button, | ||||
| @@ -81,11 +80,11 @@ const EditUser: React.FC<Props> = ({ user, rules, auths }) => { | |||||
| try { | try { | ||||
| formProps.reset({ | formProps.reset({ | ||||
| username: user.username, | username: user.username, | ||||
| staffNo: user.staffNo?.toString() ??"", | |||||
| name: user.name, | |||||
| staffNo: user.staffNo?.toString() ?? "", | |||||
| addAuthIds: addAuthIds, | addAuthIds: addAuthIds, | ||||
| removeAuthIds: [], | removeAuthIds: [], | ||||
| password: "", | password: "", | ||||
| confirmPassword: "", | |||||
| }); | }); | ||||
| formProps.clearErrors(); | formProps.clearErrors(); | ||||
| console.log(formProps.formState.defaultValues); | console.log(formProps.formState.defaultValues); | ||||
| @@ -149,8 +148,8 @@ const EditUser: React.FC<Props> = ({ user, rules, auths }) => { | |||||
| } | } | ||||
| const userData = { | const userData = { | ||||
| username: data.username, | username: data.username, | ||||
| name: data.name, | |||||
| staffNo: data.staffNo, | staffNo: data.staffNo, | ||||
| // name: user.name, | |||||
| locked: false, | locked: false, | ||||
| addAuthIds: data.addAuthIds || [], | addAuthIds: data.addAuthIds || [], | ||||
| removeAuthIds: data.removeAuthIds || [], | 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>(); | } = useFormContext<UserInputs>(); | ||||
| const password = watch("password"); | const password = watch("password"); | ||||
| const confirmPassword = watch("confirmPassword"); | |||||
| const username = watch("username"); | const username = watch("username"); | ||||
| const staffNo = watch("staffNo"); | const staffNo = watch("staffNo"); | ||||
| const name = watch("name"); | |||||
| return ( | return ( | ||||
| <Card> | <Card> | ||||
| @@ -76,72 +76,54 @@ const UserDetail: React.FC = () => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("password")} | |||||
| label={t("name")} | |||||
| fullWidth | fullWidth | ||||
| type="password" | |||||
| variant="filled" | variant="filled" | ||||
| InputLabelProps={{ | InputLabelProps={{ | ||||
| shrink: !!password, | |||||
| shrink: !!name, | |||||
| sx: { fontSize: "0.9375rem" }, | sx: { fontSize: "0.9375rem" }, | ||||
| }} | }} | ||||
| InputProps={{ | InputProps={{ | ||||
| sx: { paddingTop: "8px" }, | sx: { paddingTop: "8px" }, | ||||
| }} | }} | ||||
| {...register("password")} | |||||
| {...register("name", { | |||||
| required: "name required!", | |||||
| })} | |||||
| error={Boolean(errors.name)} | |||||
| helperText={ | 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> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| label={t("Confirm Password")} | |||||
| label={t("password")} | |||||
| fullWidth | fullWidth | ||||
| type="password" | type="password" | ||||
| variant="filled" | variant="filled" | ||||
| InputLabelProps={{ | InputLabelProps={{ | ||||
| shrink: !!confirmPassword, | |||||
| shrink: !!password, | |||||
| sx: { fontSize: "0.9375rem" }, | sx: { fontSize: "0.9375rem" }, | ||||
| }} | }} | ||||
| InputProps={{ | InputProps={{ | ||||
| sx: { paddingTop: "8px" }, | 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={ | 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> | ||||
| {/* <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("name")} | |||||
| fullWidth | |||||
| {...register("name", { | |||||
| required: "name required!", | |||||
| })} | |||||
| error={Boolean(errors.name)} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | </Grid> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </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 { deleteUser } from "@/app/api/user/actions"; | ||||
| import QrCodeIcon from "@mui/icons-material/QrCode"; | import QrCodeIcon from "@mui/icons-material/QrCode"; | ||||
| import UserSearchLoading from "./UserSearchLoading"; | import UserSearchLoading from "./UserSearchLoading"; | ||||
| import { searchUsersByUsernameOrName } from "@/app/api/user/client"; | |||||
| interface Props { | interface Props { | ||||
| users: UserResult[]; | users: UserResult[]; | ||||
| @@ -31,11 +32,12 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||||
| }); | }); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| const [isSearching, setIsSearching] = useState(false); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("Username"), | |||||
| label: "用戶/姓名", | |||||
| paramName: "username", | paramName: "username", | ||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| @@ -56,29 +58,12 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||||
| [router, t], | [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) => { | const onDeleteClick = useCallback((users: UserResult) => { | ||||
| deleteDialog(async () => { | deleteDialog(async () => { | ||||
| await deleteUser(users.id); | await deleteUser(users.id); | ||||
| successDialog(t("Delete Success"), t); | successDialog(t("Delete Success"), t); | ||||
| }, t); | }, t); | ||||
| }, []); | |||||
| }, [t]); | |||||
| const columns = useMemo<Column<UserResult>[]>( | const columns = useMemo<Column<UserResult>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -87,35 +72,78 @@ const UserSearch: React.FC<Props> = ({ users }) => { | |||||
| label: t("Edit"), | label: t("Edit"), | ||||
| onClick: onUserClick, | onClick: onUserClick, | ||||
| buttonIcon: <EditNote />, | 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", | name: "action", | ||||
| label: t("Delete"), | label: t("Delete"), | ||||
| onClick: onDeleteClick, | onClick: onDeleteClick, | ||||
| buttonIcon: <DeleteIcon />, | buttonIcon: <DeleteIcon />, | ||||
| color: "error", | color: "error", | ||||
| sx: { width: "10%", minWidth: "80px" }, | |||||
| }, | }, | ||||
| ], | ], | ||||
| [t], | |||||
| [t, onUserClick, onDeleteClick], | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | 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> | <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 { UserResult } from "@/app/api/user"; | ||||
| import { deleteUser } from "@/app/api/user/actions"; | import { deleteUser } from "@/app/api/user/actions"; | ||||
| import QrCodeIcon from "@mui/icons-material/QrCode"; | import QrCodeIcon from "@mui/icons-material/QrCode"; | ||||
| import { exportUserQrCode } from "@/app/api/user/client"; | |||||
| import { exportUserQrCode, searchUsersByUsernameOrName } from "@/app/api/user/client"; | |||||
| import { | import { | ||||
| Checkbox, | Checkbox, | ||||
| Box, | Box, | ||||
| @@ -58,6 +58,7 @@ const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => { | |||||
| const [checkboxIds, setCheckboxIds] = useState<number[]>([]); | const [checkboxIds, setCheckboxIds] = useState<number[]>([]); | ||||
| const [selectAll, setSelectAll] = useState(false); | const [selectAll, setSelectAll] = useState(false); | ||||
| const [printQty, setPrintQty] = useState(1); | const [printQty, setPrintQty] = useState(1); | ||||
| const [isSearching, setIsSearching] = useState(false); | |||||
| const [previewOpen, setPreviewOpen] = useState(false); | const [previewOpen, setPreviewOpen] = useState(false); | ||||
| const [previewUrl, setPreviewUrl] = useState<string | null>(null); | const [previewUrl, setPreviewUrl] = useState<string | null>(null); | ||||
| @@ -83,7 +84,7 @@ const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => { | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("User"), | |||||
| label: "用戶/姓名", | |||||
| paramName: "username", | paramName: "username", | ||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| @@ -265,17 +266,44 @@ const QrCodeHandleSearch: React.FC<Props> = ({ users, printerCombo }) => { | |||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | 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} | onReset={onReset} | ||||
| /> | /> | ||||