Преглед изворни кода

Update for the user page

master
B.E.N.S.O.N пре 14 часа
родитељ
комит
1d92556988
6 измењених фајлова са 207 додато и 93 уклоњено
  1. +2
    -4
      src/app/api/user/actions.ts
  2. +79
    -0
      src/app/api/user/client.ts
  3. +4
    -5
      src/components/EditUser/EditUser.tsx
  4. +19
    -37
      src/components/EditUser/UserDetail.tsx
  5. +62
    -34
      src/components/UserSearch/UserSearch.tsx
  6. +41
    -13
      src/components/qrCodeHandles/qrCodeHandleSearch.tsx

+ 2
- 4
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" },


+ 79
- 0
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<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();
};

+ 4
- 5
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<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;

+ 19
- 37
src/components/EditUser/UserDetail.tsx Прегледај датотеку

@@ -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;

+ 62
- 34
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<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;

+ 41
- 13
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<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}
/>


Loading…
Откажи
Сачувај