diff --git a/public/PP Staff List v.7.xlsx b/public/PP Staff List v.7.xlsx new file mode 100644 index 0000000..e96d535 Binary files /dev/null and b/public/PP Staff List v.7.xlsx differ diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx index b0221c1..6876046 100644 --- a/src/app/(main)/settings/user/page.tsx +++ b/src/app/(main)/settings/user/page.tsx @@ -1,12 +1,13 @@ import { Metadata } from "next"; import { getServerI18n, I18nProvider } from "@/i18n"; import Typography from "@mui/material/Typography"; -import { Suspense } from "react"; import { Stack } from "@mui/material"; import { Button } from "@mui/material"; import Link from "next/link"; -import UserSearch from "@/components/UserSearch"; import Add from "@mui/icons-material/Add"; +import UserExcelSheetView from "../../../../components/UserSearch/UserExcelSheetView"; +import { fetchUser } from "@/app/api/user"; +import { fetchAuthBatchByUserIds } from "@/app/api/group/actions"; export const metadata: Metadata = { title: "User Management", @@ -14,6 +15,18 @@ export const metadata: Metadata = { const User: React.FC = async () => { const { t } = await getServerI18n("user"); + const users = await fetchUser(); + const authBatchMap = await fetchAuthBatchByUserIds(users.map(user => user.id)).catch( + () => ({} as Record), + ); + const usersWithDetails = users.map(user => { + const authRecords = authBatchMap[user.id] ?? []; + return { + ...user, + authIds: authRecords.filter(a => a.v === 1).map(a => a.id), + auths: authRecords, + }; + }); return ( <> { - }> - - + ); diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts index 219804c..604977b 100644 --- a/src/app/api/group/actions.ts +++ b/src/app/api/group/actions.ts @@ -31,6 +31,8 @@ export interface record { records: auth[]; } +export type UserAuthBatchRecord = Record; + export const fetchAuth = cache(async (target: string, id?: number) => { return serverFetchJson( `${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, @@ -40,6 +42,21 @@ export const fetchAuth = cache(async (target: string, id?: number) => { ); }); +export const fetchAuthBatchByUserIds = cache(async (userIds: number[]) => { + if (userIds.length === 0) { + return {} as UserAuthBatchRecord; + } + + return serverFetchJson( + `${BASE_API_URL}/group/auth/user-batch?${new URLSearchParams( + userIds.map(id => ["userIds", String(id)]), + ).toString()}`, + { + next: { tags: ["auth"] }, + }, + ); +}); + export const saveGroup = async (data: CreateGroupInputs) => { const newGroup = serverFetchJson(`${BASE_API_URL}/group/save`, { method: "POST", diff --git a/src/app/api/user/client.ts b/src/app/api/user/client.ts index f07f7d6..2040da7 100644 --- a/src/app/api/user/client.ts +++ b/src/app/api/user/client.ts @@ -134,4 +134,36 @@ export const searchUsers = async (searchParams: { } return response.json(); +}; + +export interface UpdateUserRequest { + username: string; + name: string; + staffNo?: string; + locked?: boolean; + addAuthIds?: number[]; + removeAuthIds?: number[]; +} + +export const updateUser = async ( + id: number, + data: UpdateUserRequest, +): Promise => { + const token = localStorage.getItem("accessToken"); + + const response = await fetch(`${NEXT_PUBLIC_API_URL}/user/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + if (response.status === 401) { + throw new Error("Unauthorized: Please log in again"); + } + throw new Error(`Failed to update user: ${response.status} ${response.statusText}`); + } }; \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 1c1fa6e..67e9ae4 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -97,7 +97,9 @@ export async function serverFetchJson(...args: FetchParams) { const t0 = performance.now(); const response = await serverFetch(...args); const t1 = performance.now(); - console.log(`[serverFetchJson] ${response.status} ${(t1 - t0).toFixed(1)}ms ${url}`); + if (process.env.NEXT_PUBLIC_DEBUG_FETCH_LOG === "true") { + console.log(`[serverFetchJson] ${response.status} ${(t1 - t0).toFixed(1)}ms ${url}`); + } if (response.ok) { if (response.status === 204) { return response.status as T; @@ -124,7 +126,9 @@ export async function serverFetchString(...args: FetchParams) { const t0 = performance.now(); const response = await serverFetch(...args); const t1 = performance.now(); -console.log(`[serverFetchJson] ${response.status} ${(t1 - t0).toFixed(1)}ms ${url}`); +if (process.env.NEXT_PUBLIC_DEBUG_FETCH_LOG === "true") { + console.log(`[serverFetchJson] ${response.status} ${(t1 - t0).toFixed(1)}ms ${url}`); +} if (response.ok) { return response.text() as T; diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index 6da4b62..b55739b 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -10,106 +10,61 @@ import React, { import { useTranslation } from "react-i18next"; import { Button, - Card, - CardContent, - Grid, Stack, - Tab, - Tabs, - TabsProps, - TextField, Typography, } from "@mui/material"; import { - FieldErrors, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, - useFormContext, } from "react-hook-form"; -import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; +import { Check, Close, RestartAlt } from "@mui/icons-material"; import { UserInputs, adminChangePassword, editUser, - fetchUserDetails, } from "@/app/api/user/actions"; import UserDetail from "./UserDetail"; import { UserResult, passwordRule } from "@/app/api/user"; -import { auth } from "@/app/api/group/actions"; -import AuthAllocation from "./AuthAllocation"; interface Props { - user: UserResult; + user: UserResult & { authIds?: number[] }; rules: passwordRule; - auths: auth[]; } -const EditUser: React.FC = ({ user, rules, auths }) => { +const EditUser: React.FC = ({ user, rules }) => { console.log(user); const { t } = useTranslation("user"); const formProps = useForm(); const searchParams = useSearchParams(); const id = parseInt(searchParams.get("id") || "0"); - const [tabIndex, setTabIndex] = useState(0); const router = useRouter(); const [serverError, setServerError] = useState(""); - const addAuthIds = - auths && auths.length > 0 - ? auths - .filter((item) => item.v === 1) - .map((item) => item.id) - .sort((a, b) => a - b) - : []; - - const handleTabChange = useCallback>( - (_e, newValue) => { - setTabIndex(newValue); - }, - [], - ); - - const errors = formProps.formState.errors; 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, staffNo: user.staffNo?.toString() ?? "", - addAuthIds: addAuthIds, + addAuthIds: user.authIds ?? [], removeAuthIds: [], password: "", }); formProps.clearErrors(); - console.log(formProps.formState.defaultValues); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }, [formProps, auths, user, addAuthIds, t]); + }, [formProps, user, t]); useEffect(() => { resetForm(); }, [user.id]); - const hasErrorsInTab = ( - tabIndex: number, - errors: FieldErrors, - ) => { - switch (tabIndex) { - case 0: - return Object.keys(errors).length > 0; - default: - false; - } - }; - const handleCancel = () => { router.back(); }; @@ -195,31 +150,7 @@ const EditUser: React.FC = ({ user, rules, auths }) => { component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - - - - ) : undefined - } - iconPosition="end" - /> - - - - {tabIndex == 0 && } - {tabIndex === 1 && } +