diff --git a/src/app/(main)/settings/user/create/page.tsx b/src/app/(main)/settings/user/create/page.tsx
new file mode 100644
index 0000000..a48bdec
--- /dev/null
+++ b/src/app/(main)/settings/user/create/page.tsx
@@ -0,0 +1,29 @@
+// 'use client';
+import { I18nProvider, getServerI18n } from "@/i18n";
+import React, { Suspense, useCallback, useState } from "react";
+import { Typography } from "@mui/material";
+import CreateUser from "@/components/CreateUser";
+
+interface CreateCustomInputs {
+ projectCode: string;
+ projectName: string;
+}
+
+// const Title = ["title1", "title2"];
+
+const CreateStaffPage: React.FC = async () => {
+ const { t } = await getServerI18n("user");
+
+ return (
+ <>
+ {t("Create User")}
+
+ }>
+
+
+
+ >
+ );
+};
+
+export default CreateStaffPage;
diff --git a/src/app/(main)/settings/user/edit/page.tsx b/src/app/(main)/settings/user/edit/page.tsx
new file mode 100644
index 0000000..b45dc04
--- /dev/null
+++ b/src/app/(main)/settings/user/edit/page.tsx
@@ -0,0 +1,24 @@
+import { Edit } from "@mui/icons-material";
+import { Metadata } from "next";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import EditUser from "@/components/EditUser";
+import { Typography } from "@mui/material";
+import { Suspense } from "react";
+import { preloadUser } from "@/app/api/user";
+import { searchParamsProps } from "@/app/utils/fetchUtil";
+
+const User: React.FC = async ({ searchParams }) => {
+ const { t } = await getServerI18n("user");
+ preloadUser();
+ return (
+ <>
+ {t("Edit User")}
+
+ }>
+
+
+
+ >
+ );
+};
+export default User;
diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx
index a7a9ef1..81424a1 100644
--- a/src/app/(main)/settings/user/page.tsx
+++ b/src/app/(main)/settings/user/page.tsx
@@ -1,19 +1,45 @@
import { Metadata } from "next";
-import { I18nProvider } from "@/i18n";
+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";
export const metadata: Metadata = {
- title: "Project Status by Client",
+ title: "User Management",
};
-const User: React.FC = () => {
+const User: React.FC = async() => {
+ const { t } = await getServerI18n("user");
return (
-
-
- User
-
- <>>
-
+ <>
+
+
+ {t("User")}
+
+ }
+ LinkComponent={Link}
+ href="/settings/user/create"
+ >
+ {t("Create User")}
+
+
+
+ }>
+
+
+
+ >
);
};
export default User;
diff --git a/src/app/(main)/user/page.tsx b/src/app/(main)/user/page.tsx
index 788fd9b..272d5a1 100644
--- a/src/app/(main)/user/page.tsx
+++ b/src/app/(main)/user/page.tsx
@@ -1,5 +1,3 @@
-import { preloadTaskTemplates } from "@/app/api/tasks";
-import TaskTemplateSearch from "@/components/TaskTemplateSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
@@ -15,7 +13,6 @@ export const metadata: Metadata = {
const TaskTemplates: React.FC = async () => {
const { t } = await getServerI18n("user");
- preloadTaskTemplates();
return (
<>
diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts
new file mode 100644
index 0000000..eadfe7d
--- /dev/null
+++ b/src/app/api/group/actions.ts
@@ -0,0 +1,55 @@
+"use server";
+
+import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { revalidateTag } from "next/cache";
+import { cache } from "react";
+
+
+export interface CreateGroupInputs {
+ id?: number;
+ name: string;
+ description: string;
+ addUserIds?: number[];
+ removeUserIds?: number[];
+ addAuthIds?: number[];
+ removeAuthIds?: number[];
+ }
+
+export interface auth {
+ id: number;
+ module?: any | null;
+ authority: string;
+ name: string;
+ description: string | null;
+ v: number;
+ }
+
+export interface record {
+ records: auth[];
+ }
+
+export const fetchAuth = cache(async (target: string, id?: number ) => {
+ return serverFetchJson(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, {
+ next: { tags: ["auth"] },
+ });
+});
+
+export const saveGroup = async (data: CreateGroupInputs) => {
+ const newGroup = serverFetchJson(`${BASE_API_URL}/group/save`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+ revalidateTag("group")
+ return newGroup
+ };
+
+export const deleteGroup = async (id: number) => {
+ const newGroup = serverFetchWithNoContent(`${BASE_API_URL}/group/${id}`, {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ });
+ revalidateTag("group")
+ return newGroup
+};
\ No newline at end of file
diff --git a/src/app/api/group/index.ts b/src/app/api/group/index.ts
new file mode 100644
index 0000000..082b0f3
--- /dev/null
+++ b/src/app/api/group/index.ts
@@ -0,0 +1,34 @@
+import { serverFetchJson } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { cache } from "react";
+import "server-only";
+
+export interface Records {
+ records: UserGroupResult[]
+}
+
+export interface UserGroupResult {
+ id: number;
+ action: () => void;
+ name: string;
+ description: string;
+}
+
+export type IndivUserGroup = {
+ authIds: number[];
+ data: any;
+ userIds: number[];
+}
+
+export const fetchGroup = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/group`, {
+ next: { tags: ["group"] },
+ });
+ });
+
+
+export const fetchIndivGroup = cache(async (id: number) => {
+ return serverFetchJson(`${BASE_API_URL}/group/${id}`, {
+ next: { tags: ["group"] },
+ });
+ });
diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts
new file mode 100644
index 0000000..759bef5
--- /dev/null
+++ b/src/app/api/user/actions.ts
@@ -0,0 +1,72 @@
+"use server";
+
+import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { revalidateTag } from "next/cache";
+import { UserDetail, UserResult } from ".";
+import { cache } from "react";
+
+export interface UserInputs {
+ username: string;
+ // name: string;
+ addAuthIds?: number[];
+ removeAuthIds?: number[];
+ password?: string;
+}
+
+export interface PasswordInputs {
+ password: string;
+ newPassword: string;
+ newPasswordCheck: string;
+}
+
+export const fetchUserDetails = cache(async (id: number) => {
+ return serverFetchJson(`${BASE_API_URL}/user/${id}`, {
+ next: { tags: ["user"] },
+ });
+ });
+
+export const editUser = async (id: number, data: UserInputs) => {
+ const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
+ method: "PUT",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+ revalidateTag("user")
+ return newUser
+};
+
+export const createUser = async (data: UserInputs) => {
+ const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/save`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+ revalidateTag("user")
+ return newUser
+};
+
+export const deleteUser = async (id: number) => {
+ const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ });
+ revalidateTag("user")
+ return newUser
+ };
+
+export const changePassword = async (data: any) => {
+ return serverFetchWithNoContent(`${BASE_API_URL}/user/change-password`, {
+ method: "PATCH",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+ };
+
+export const adminChangePassword = async (data: any) => {
+ return serverFetchWithNoContent(`${BASE_API_URL}/user/admin-change-password`, {
+ method: "PATCH",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ });
+ };
\ No newline at end of file
diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts
new file mode 100644
index 0000000..79fc62b
--- /dev/null
+++ b/src/app/api/user/index.ts
@@ -0,0 +1,58 @@
+import { serverFetchJson } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { cache } from "react";
+import "server-only";
+
+export interface UserResult {
+ action: any;
+ id: number;
+ username: string;
+ // name: string;
+ }
+
+// export interface DetailedUser extends UserResult {
+// username: string;
+// password: string
+// }
+
+export interface UserDetail {
+ data: UserResult;
+ authIds: number[];
+ groupIds: number[];
+ auths: any[]
+ }
+
+ export type passwordRule = {
+ min: number;
+ max: number;
+ number: boolean;
+ upperEng: boolean;
+ lowerEng: boolean;
+ specialChar: boolean;
+ }
+
+ export const preloadUser = () => {
+ fetchUser();
+ };
+
+ export const preloadUserDetail = (id: number) => {
+ fetchUserDetail(id);
+ };
+
+ export const fetchUser = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/user`, {
+ next: { tags: ["user"] },
+ });
+ });
+
+ export const fetchUserDetail = cache(async (id: number) => {
+ return serverFetchJson(`${BASE_API_URL}/user/${id}`, {
+ next: { tags: ["user"] },
+ });
+ });
+
+ export const fetchPwRules = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/user/password-rule`, {
+ next: { tags: ["pwRule"] },
+ });
+ });
\ No newline at end of file
diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts
index 92a2127..053957a 100644
--- a/src/app/utils/fetchUtil.ts
+++ b/src/app/utils/fetchUtil.ts
@@ -7,6 +7,10 @@ export type SearchParams = {
searchParams: { [key: string]: string | string[] | undefined };
}
+export interface searchParamsProps {
+ searchParams: { [key: string]: string | string[] | undefined };
+}
+
export class ServerFetchError extends Error {
public readonly response: Response | undefined;
constructor(message?: string, response?: Response) {
diff --git a/src/components/CreateUser/AuthAllocation.tsx b/src/components/CreateUser/AuthAllocation.tsx
new file mode 100644
index 0000000..fe6c5a9
--- /dev/null
+++ b/src/components/CreateUser/AuthAllocation.tsx
@@ -0,0 +1,225 @@
+"use client";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import {
+ Add,
+ Clear,
+ PersonAdd,
+ PersonRemove,
+ Remove,
+ Search,
+} from "@mui/icons-material";
+import { useTranslation } from "react-i18next";
+import {
+ FieldErrors,
+ FormProvider,
+ SubmitErrorHandler,
+ SubmitHandler,
+ useForm,
+ useFormContext,
+} from "react-hook-form";
+import {
+ Box,
+ Card,
+ CardContent,
+ Grid,
+ IconButton,
+ InputAdornment,
+ Stack,
+ Tab,
+ Tabs,
+ TabsProps,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { differenceBy } from "lodash";
+import { UserInputs } from "@/app/api/user/actions";
+import { auth } from "@/app/api/group/actions";
+import SearchResults, { Column } from "../SearchResults";
+
+export interface Props {
+ auths: auth[];
+}
+
+const AuthAllocation: React.FC = ({ auths }) => {
+ const { t } = useTranslation();
+ const searchParams = useSearchParams();
+ const id = parseInt(searchParams.get("id") || "0");
+ const {
+ setValue,
+ getValues,
+ formState: { defaultValues },
+ reset,
+ resetField,
+ } = useFormContext();
+ const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
+ const [filteredAuths, setFilteredAuths] = useState(initialAuths);
+ const [selectedAuths, setSelectedAuths] = useState(
+ () => {
+ return filteredAuths.filter(
+ (s) => getValues("addAuthIds")?.includes(s.id)
+ );
+ }
+ );
+ const [removeAuthIds, setRemoveAuthIds] = useState([]);
+
+ // Adding / Removing Auth
+ const addAuth = useCallback((auth: auth) => {
+ setSelectedAuths((a) => [...a, auth]);
+ }, []);
+ const removeAuth = useCallback((auth: auth) => {
+ setSelectedAuths((a) => a.filter((a) => a.id !== auth.id));
+ setRemoveAuthIds((prevIds) => [...prevIds, auth.id]);
+ }, []);
+
+ const clearAuth = useCallback(() => {
+ if (defaultValues !== undefined) {
+ resetField("addAuthIds");
+ setSelectedAuths(
+ initialAuths.filter(
+ (auth) => defaultValues.addAuthIds?.includes(auth.id)
+ )
+ );
+ }
+ }, [defaultValues]);
+
+ // Sync with form
+ useEffect(() => {
+ setValue(
+ "addAuthIds",
+ selectedAuths.map((a) => a.id)
+ );
+ setValue("removeAuthIds", removeAuthIds);
+ }, [selectedAuths, removeAuthIds, setValue]);
+
+ const AuthPoolColumns = useMemo[]>(
+ () => [
+ {
+ label: t("Add"),
+ name: "id",
+ onClick: addAuth,
+ buttonIcon: ,
+ },
+ { label: t("authority"), name: "authority" },
+ { label: t("description"), name: "name" },
+ ],
+ [addAuth, t]
+ );
+
+ const allocatedAuthColumns = useMemo[]>(
+ () => [
+ {
+ label: t("Remove"),
+ name: "id",
+ onClick: removeAuth,
+ buttonIcon: ,
+ },
+ { label: t("authority"), name: "authority" },
+ { label: t("description"), name: "name" },
+ ],
+ [removeAuth, selectedAuths, t]
+ );
+
+ const [query, setQuery] = React.useState("");
+ const onQueryInputChange = React.useCallback<
+ React.ChangeEventHandler
+ >((e) => {
+ setQuery(e.target.value);
+ }, []);
+ const clearQueryInput = React.useCallback(() => {
+ setQuery("");
+ }, []);
+
+ React.useEffect(() => {
+ setFilteredAuths(
+ initialAuths.filter((a) =>
+ (
+ a.authority.toLowerCase().includes(query.toLowerCase()) ||
+ a.name?.toLowerCase().includes(query.toLowerCase())
+ )
+ )
+ );
+ }, [auths, query]);
+
+ const resetAuth = React.useCallback(() => {
+ clearQueryInput();
+ clearAuth();
+ }, [clearQueryInput, clearAuth]);
+
+ const formProps = useForm({});
+
+ // Tab related
+ const [tabIndex, setTabIndex] = React.useState(0);
+ const handleTabChange = React.useCallback>(
+ (_e, newValue) => {
+ setTabIndex(newValue);
+ },
+ []
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {t("Authority")}
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+ {tabIndex === 0 && (
+
+ )}
+ {tabIndex === 1 && (
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+export default AuthAllocation;
diff --git a/src/components/CreateUser/CreateUser.tsx b/src/components/CreateUser/CreateUser.tsx
new file mode 100644
index 0000000..8ec54dd
--- /dev/null
+++ b/src/components/CreateUser/CreateUser.tsx
@@ -0,0 +1,243 @@
+"use client";
+import { useRouter, useSearchParams } from "next/navigation";
+import React, {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useState,
+} from "react";
+// import { TeamResult } from "@/app/api/team";
+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 {
+ UserInputs,
+ adminChangePassword,
+ fetchUserDetails,
+ createUser,
+} 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 {
+ rules: passwordRule;
+ auths: auth[];
+}
+
+const CreateUser: React.FC = async ({ rules, auths }) => {
+ console.log(auths)
+ 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(() => {
+ console.log("triggerred");
+ console.log(addAuthIds);
+ try {
+ formProps.reset({
+ username: "",
+ addAuthIds: addAuthIds,
+ removeAuthIds: [],
+ password: "",
+ });
+ console.log(formProps.formState.defaultValues);
+ } catch (error) {
+ console.log(error);
+ setServerError(t("An error has occurred. Please try again later."));
+ }
+ }, [auths]);
+
+ useEffect(() => {
+ resetForm();
+ }, []);
+
+ const hasErrorsInTab = (
+ tabIndex: number,
+ errors: FieldErrors
+ ) => {
+ switch (tabIndex) {
+ case 0:
+ return Object.keys(errors).length > 0;
+ default:
+ false;
+ }
+ };
+
+ const handleCancel = () => {
+ router.back();
+ };
+
+ const onSubmit = useCallback>(
+ async (data) => {
+ try {
+ let haveError = false;
+ let regex_pw =
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/;
+ let pw = "";
+ if (data.password && data.password.length > 0) {
+ pw = data.password;
+ if (pw.length < rules.min) {
+ haveError = true;
+ formProps.setError("password", {
+ message: t("The password requires 8-20 characters."),
+ type: "required",
+ });
+ }
+ if (pw.length > rules.max) {
+ haveError = true;
+ formProps.setError("password", {
+ message: t("The password requires 8-20 characters."),
+ type: "required",
+ });
+ }
+ if (!regex_pw.test(pw)) {
+ haveError = true;
+ formProps.setError("password", {
+ message:
+ "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.",
+ type: "required",
+ });
+ }
+ }
+ const userData = {
+ username: data.username,
+ // name: data.name,
+ locked: false,
+ addAuthIds: data.addAuthIds || [],
+ removeAuthIds: data.removeAuthIds || [],
+ password: pw,
+ };
+ const pwData = {
+ id: id,
+ password: pw,
+ newPassword: "",
+ };
+ if (haveError) {
+ return;
+ }
+ console.log("passed");
+ console.log(userData)
+ await createUser(userData);
+ // if (data.password && data.password.length > 0) {
+ // await adminChangePassword(pwData);
+ // }
+ router.replace("/settings/user");
+ } catch (e) {
+ console.log(e);
+ setServerError(t("An error has occurred. Please try again later."));
+ }
+ },
+ [router]
+ );
+ const onSubmitError = useCallback>(
+ (errors) => {
+ console.log(errors);
+ },
+ []
+ );
+
+ return (
+ <>
+ {serverError && (
+
+ {serverError}
+
+ )}
+
+
+
+
+
+ ) : undefined
+ }
+ iconPosition="end"
+ />
+
+
+
+ {tabIndex == 0 && }
+ {tabIndex === 1 && }
+
+ }
+ onClick={resetForm}
+ >
+ {t("Reset")}
+
+ }
+ onClick={handleCancel}
+ >
+ {t("Cancel")}
+
+ } type="submit">
+ {t("Confirm")}
+
+
+
+
+ >
+ );
+};
+export default CreateUser;
diff --git a/src/components/CreateUser/CreateUserLoading.tsx b/src/components/CreateUser/CreateUserLoading.tsx
new file mode 100644
index 0000000..6ff6088
--- /dev/null
+++ b/src/components/CreateUser/CreateUserLoading.tsx
@@ -0,0 +1,40 @@
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Skeleton from "@mui/material/Skeleton";
+import Stack from "@mui/material/Stack";
+import React from "react";
+
+// Can make this nicer
+export const CreateUserLoading: React.FC = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ CreateUser
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default CreateUserLoading;
diff --git a/src/components/CreateUser/CreateUserWrapper.tsx b/src/components/CreateUser/CreateUserWrapper.tsx
new file mode 100644
index 0000000..76d341d
--- /dev/null
+++ b/src/components/CreateUser/CreateUserWrapper.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+import CreateUser from "./CreateUser";
+import CreateUserLoading from "./CreateUserLoading";
+import { searchParamsProps } from "@/app/utils/fetchUtil";
+import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user";
+import { fetchUserDetails } from "@/app/api/user/actions";
+import { fetchAuth } from "@/app/api/group/actions";
+
+interface SubComponents {
+ Loading: typeof CreateUserLoading;
+}
+
+const CreateUserWrapper: React.FC & SubComponents = async ({
+}) => {
+ const [pwRule, auths] = await Promise.all([
+ fetchPwRules(),
+ fetchAuth("user"),
+ ]);
+
+ return ;
+};
+
+CreateUserWrapper.Loading = CreateUserLoading;
+
+export default CreateUserWrapper;
diff --git a/src/components/CreateUser/UserDetail.tsx b/src/components/CreateUser/UserDetail.tsx
new file mode 100644
index 0000000..df1581a
--- /dev/null
+++ b/src/components/CreateUser/UserDetail.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { UserResult } from "@/app/api/user";
+import { UserInputs } from "@/app/api/user/actions";
+import {
+ Card,
+ CardContent,
+ Grid,
+ Stack,
+ TextField,
+ Typography,
+ makeStyles,
+} from "@mui/material";
+import { useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+
+const UserDetail: React.FC = () => {
+
+ const { t } = useTranslation();
+ const {
+ register,
+ formState: { errors },
+ control,
+ } = useFormContext();
+
+ return (
+
+
+
+ {t("User Detail")}
+
+
+
+
+
+
+
+ // - 8-20 characters
+ //
+ // - Uppercase letters
+ //
+ // - Lowercase letters
+ //
+ // - Numbers
+ //
+ // - Symbols
+ // >)
+ // )
+ // }
+ helperText={
+ Boolean(errors.password) &&
+ (errors.password?.message
+ ? t(errors.password.message)
+ : t("Please input correct password"))
+ }
+ error={Boolean(errors.password)}
+ />
+
+ {/*
+
+ */}
+
+
+
+ );
+};
+
+export default UserDetail;
+
+
+{/* <>
+ - 8-20 characters
+
+ - Uppercase letters
+
+ - Lowercase letters
+
+ - Numbers
+
+ - Symbols
+ > */}
\ No newline at end of file
diff --git a/src/components/CreateUser/index.ts b/src/components/CreateUser/index.ts
new file mode 100644
index 0000000..502645d
--- /dev/null
+++ b/src/components/CreateUser/index.ts
@@ -0,0 +1 @@
+export { default } from "./CreateUserWrapper";
diff --git a/src/components/EditUser/AuthAllocation.tsx b/src/components/EditUser/AuthAllocation.tsx
new file mode 100644
index 0000000..fe6c5a9
--- /dev/null
+++ b/src/components/EditUser/AuthAllocation.tsx
@@ -0,0 +1,225 @@
+"use client";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import {
+ Add,
+ Clear,
+ PersonAdd,
+ PersonRemove,
+ Remove,
+ Search,
+} from "@mui/icons-material";
+import { useTranslation } from "react-i18next";
+import {
+ FieldErrors,
+ FormProvider,
+ SubmitErrorHandler,
+ SubmitHandler,
+ useForm,
+ useFormContext,
+} from "react-hook-form";
+import {
+ Box,
+ Card,
+ CardContent,
+ Grid,
+ IconButton,
+ InputAdornment,
+ Stack,
+ Tab,
+ Tabs,
+ TabsProps,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { differenceBy } from "lodash";
+import { UserInputs } from "@/app/api/user/actions";
+import { auth } from "@/app/api/group/actions";
+import SearchResults, { Column } from "../SearchResults";
+
+export interface Props {
+ auths: auth[];
+}
+
+const AuthAllocation: React.FC = ({ auths }) => {
+ const { t } = useTranslation();
+ const searchParams = useSearchParams();
+ const id = parseInt(searchParams.get("id") || "0");
+ const {
+ setValue,
+ getValues,
+ formState: { defaultValues },
+ reset,
+ resetField,
+ } = useFormContext();
+ const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
+ const [filteredAuths, setFilteredAuths] = useState(initialAuths);
+ const [selectedAuths, setSelectedAuths] = useState(
+ () => {
+ return filteredAuths.filter(
+ (s) => getValues("addAuthIds")?.includes(s.id)
+ );
+ }
+ );
+ const [removeAuthIds, setRemoveAuthIds] = useState([]);
+
+ // Adding / Removing Auth
+ const addAuth = useCallback((auth: auth) => {
+ setSelectedAuths((a) => [...a, auth]);
+ }, []);
+ const removeAuth = useCallback((auth: auth) => {
+ setSelectedAuths((a) => a.filter((a) => a.id !== auth.id));
+ setRemoveAuthIds((prevIds) => [...prevIds, auth.id]);
+ }, []);
+
+ const clearAuth = useCallback(() => {
+ if (defaultValues !== undefined) {
+ resetField("addAuthIds");
+ setSelectedAuths(
+ initialAuths.filter(
+ (auth) => defaultValues.addAuthIds?.includes(auth.id)
+ )
+ );
+ }
+ }, [defaultValues]);
+
+ // Sync with form
+ useEffect(() => {
+ setValue(
+ "addAuthIds",
+ selectedAuths.map((a) => a.id)
+ );
+ setValue("removeAuthIds", removeAuthIds);
+ }, [selectedAuths, removeAuthIds, setValue]);
+
+ const AuthPoolColumns = useMemo[]>(
+ () => [
+ {
+ label: t("Add"),
+ name: "id",
+ onClick: addAuth,
+ buttonIcon: ,
+ },
+ { label: t("authority"), name: "authority" },
+ { label: t("description"), name: "name" },
+ ],
+ [addAuth, t]
+ );
+
+ const allocatedAuthColumns = useMemo[]>(
+ () => [
+ {
+ label: t("Remove"),
+ name: "id",
+ onClick: removeAuth,
+ buttonIcon: ,
+ },
+ { label: t("authority"), name: "authority" },
+ { label: t("description"), name: "name" },
+ ],
+ [removeAuth, selectedAuths, t]
+ );
+
+ const [query, setQuery] = React.useState("");
+ const onQueryInputChange = React.useCallback<
+ React.ChangeEventHandler
+ >((e) => {
+ setQuery(e.target.value);
+ }, []);
+ const clearQueryInput = React.useCallback(() => {
+ setQuery("");
+ }, []);
+
+ React.useEffect(() => {
+ setFilteredAuths(
+ initialAuths.filter((a) =>
+ (
+ a.authority.toLowerCase().includes(query.toLowerCase()) ||
+ a.name?.toLowerCase().includes(query.toLowerCase())
+ )
+ )
+ );
+ }, [auths, query]);
+
+ const resetAuth = React.useCallback(() => {
+ clearQueryInput();
+ clearAuth();
+ }, [clearQueryInput, clearAuth]);
+
+ const formProps = useForm({});
+
+ // Tab related
+ const [tabIndex, setTabIndex] = React.useState(0);
+ const handleTabChange = React.useCallback>(
+ (_e, newValue) => {
+ setTabIndex(newValue);
+ },
+ []
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {t("Authority")}
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+ {tabIndex === 0 && (
+
+ )}
+ {tabIndex === 1 && (
+
+ )}
+
+
+
+
+
+ >
+ );
+};
+export default AuthAllocation;
diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx
new file mode 100644
index 0000000..27f6609
--- /dev/null
+++ b/src/components/EditUser/EditUser.tsx
@@ -0,0 +1,244 @@
+"use client";
+import { useRouter, useSearchParams } from "next/navigation";
+import React, {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useState,
+} from "react";
+// import { TeamResult } from "@/app/api/team";
+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 {
+ 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;
+ rules: passwordRule;
+ auths: auth[];
+}
+
+const EditUser: React.FC = async ({ user, rules, auths }) => {
+ 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(() => {
+ console.log("triggerred");
+ console.log(addAuthIds);
+ try {
+ formProps.reset({
+ username: user.username,
+ // name: user.name,
+ // email: user.email,
+ addAuthIds: addAuthIds,
+ removeAuthIds: [],
+ password: "",
+ });
+ console.log(formProps.formState.defaultValues);
+ } catch (error) {
+ console.log(error);
+ setServerError(t("An error has occurred. Please try again later."));
+ }
+ }, [auths, user]);
+
+ useEffect(() => {
+ resetForm();
+ }, []);
+
+ const hasErrorsInTab = (
+ tabIndex: number,
+ errors: FieldErrors
+ ) => {
+ switch (tabIndex) {
+ case 0:
+ return Object.keys(errors).length > 0;
+ default:
+ false;
+ }
+ };
+
+ const handleCancel = () => {
+ router.back();
+ };
+
+ const onSubmit = useCallback>(
+ async (data) => {
+ try {
+ let haveError = false;
+ let regex_pw =
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,20}$/;
+ let pw = "";
+ if (data.password && data.password.length > 0) {
+ pw = data.password;
+ if (pw.length < rules.min) {
+ haveError = true;
+ formProps.setError("password", {
+ message: t("The password requires 8-20 characters."),
+ type: "required",
+ });
+ }
+ if (pw.length > rules.max) {
+ haveError = true;
+ formProps.setError("password", {
+ message: t("The password requires 8-20 characters."),
+ type: "required",
+ });
+ }
+ if (!regex_pw.test(pw)) {
+ haveError = true;
+ formProps.setError("password", {
+ message:
+ "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.",
+ type: "required",
+ });
+ }
+ }
+ const userData = {
+ username: data.username,
+ // name: user.name,
+ locked: false,
+ addAuthIds: data.addAuthIds || [],
+ removeAuthIds: data.removeAuthIds || [],
+ };
+ const pwData = {
+ id: id,
+ password: "",
+ newPassword: pw,
+ };
+ if (haveError) {
+ return;
+ }
+ console.log("passed");
+ await editUser(id, userData);
+ if (data.password && data.password.length > 0) {
+ await adminChangePassword(pwData);
+ }
+ router.replace("/settings/user");
+ } catch (e) {
+ console.log(e);
+ setServerError(t("An error has occurred. Please try again later."));
+ }
+ },
+ [router]
+ );
+ const onSubmitError = useCallback>(
+ (errors) => {
+ console.log(errors);
+ },
+ []
+ );
+
+ return (
+ <>
+ {serverError && (
+
+ {serverError}
+
+ )}
+
+
+
+
+
+ ) : undefined
+ }
+ iconPosition="end"
+ />
+
+
+
+ {tabIndex == 0 && }
+ {tabIndex === 1 && }
+
+ }
+ onClick={resetForm}
+ >
+ {t("Reset")}
+
+ }
+ onClick={handleCancel}
+ >
+ {t("Cancel")}
+
+ } type="submit">
+ {t("Confirm")}
+
+
+
+
+ >
+ );
+};
+export default EditUser;
diff --git a/src/components/EditUser/EditUserLoading.tsx b/src/components/EditUser/EditUserLoading.tsx
new file mode 100644
index 0000000..971c9e4
--- /dev/null
+++ b/src/components/EditUser/EditUserLoading.tsx
@@ -0,0 +1,40 @@
+import Card from "@mui/material/Card";
+import CardContent from "@mui/material/CardContent";
+import Skeleton from "@mui/material/Skeleton";
+import Stack from "@mui/material/Stack";
+import React from "react";
+
+// Can make this nicer
+export const EditUserLoading: React.FC = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ EditUser
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default EditUserLoading;
diff --git a/src/components/EditUser/EditUserWrapper.tsx b/src/components/EditUser/EditUserWrapper.tsx
new file mode 100644
index 0000000..dc0743b
--- /dev/null
+++ b/src/components/EditUser/EditUserWrapper.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import EditUser from "./EditUser";
+import EditUserLoading from "./EditUserLoading";
+// import { fetchTeam, fetchTeamLeads } from "@/app/api/Team";
+import { useSearchParams } from "next/navigation";
+import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user";
+import { searchParamsProps } from "@/app/utils/fetchUtil";
+import { fetchUserDetails } from "@/app/api/user/actions";
+import { fetchAuth } from "@/app/api/group/actions";
+
+interface SubComponents {
+ Loading: typeof EditUserLoading;
+}
+
+const EditUserWrapper: React.FC & SubComponents = async ({
+ searchParams,
+}) => {
+ const id = parseInt(searchParams.id as string);
+ const [pwRule, user, auths] = await Promise.all([
+ fetchPwRules(),
+ fetchUserDetails(id),
+ fetchAuth("user", id),
+ ]);
+ console.log(user.data)
+ console.log(auths.records)
+
+ return ;
+};
+
+EditUserWrapper.Loading = EditUserLoading;
+
+export default EditUserWrapper;
diff --git a/src/components/EditUser/UserDetail.tsx b/src/components/EditUser/UserDetail.tsx
new file mode 100644
index 0000000..df1581a
--- /dev/null
+++ b/src/components/EditUser/UserDetail.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { UserResult } from "@/app/api/user";
+import { UserInputs } from "@/app/api/user/actions";
+import {
+ Card,
+ CardContent,
+ Grid,
+ Stack,
+ TextField,
+ Typography,
+ makeStyles,
+} from "@mui/material";
+import { useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+
+const UserDetail: React.FC = () => {
+
+ const { t } = useTranslation();
+ const {
+ register,
+ formState: { errors },
+ control,
+ } = useFormContext();
+
+ return (
+
+
+
+ {t("User Detail")}
+
+
+
+
+
+
+
+ // - 8-20 characters
+ //
+ // - Uppercase letters
+ //
+ // - Lowercase letters
+ //
+ // - Numbers
+ //
+ // - Symbols
+ // >)
+ // )
+ // }
+ helperText={
+ Boolean(errors.password) &&
+ (errors.password?.message
+ ? t(errors.password.message)
+ : t("Please input correct password"))
+ }
+ error={Boolean(errors.password)}
+ />
+
+ {/*
+
+ */}
+
+
+
+ );
+};
+
+export default UserDetail;
+
+
+{/* <>
+ - 8-20 characters
+
+ - Uppercase letters
+
+ - Lowercase letters
+
+ - Numbers
+
+ - Symbols
+ > */}
\ No newline at end of file
diff --git a/src/components/EditUser/index.ts b/src/components/EditUser/index.ts
new file mode 100644
index 0000000..c12dc8e
--- /dev/null
+++ b/src/components/EditUser/index.ts
@@ -0,0 +1 @@
+export { default } from "./EditUserWrapper";
diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx
index 59b48d9..28b00fd 100644
--- a/src/components/Logo/Logo.tsx
+++ b/src/components/Logo/Logo.tsx
@@ -21,10 +21,8 @@ const Logo: React.FC = ({ width, height }) => {
fill="#000"
// style="stroke:#000;stroke-width:0.25mm;fill:#000"
>
-
+
);
diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx
index b7ac669..60e1976 100644
--- a/src/components/UserSearch/UserSearch.tsx
+++ b/src/components/UserSearch/UserSearch.tsx
@@ -10,10 +10,12 @@ import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { UserResult } from "@/app/api/user";
import { deleteUser } from "@/app/api/user/actions";
+import UserSearchLoading from "./UserSearchLoading";
interface Props {
users: UserResult[];
}
+
type SearchQuery = Partial>;
type SearchParamNames = keyof SearchQuery;
@@ -25,8 +27,8 @@ const UserSearch: React.FC = ({ users }) => {
const searchCriteria: Criterion[] = useMemo(
() => [
{
- label: t("User Name"),
- paramName: "title",
+ label: t("Username"),
+ paramName: "username",
type: "text",
},
],
@@ -56,12 +58,7 @@ const UserSearch: React.FC = ({ users }) => {
onClick: onUserClick,
buttonIcon: ,
},
- { name: "name", label: t("UserName") },
- { name: "fullName", label: t("FullName") },
- { name: "title", label: t("Title") },
- { name: "department", label: t("Department") },
- { name: "email", label: t("Email") },
- { name: "phone1", label: t("Phone") },
+ { name: "username", label: t("Username") },
{
name: "action",
label: t("Delete"),
@@ -88,7 +85,7 @@ const UserSearch: React.FC = ({ users }) => {
// )
}}
/>
- items={filteredUser} columns={columns} />
+ items={filteredUser} columns={columns} pagingController={{ pageNum: 1, pageSize: 10, totalCount: 100 }}/>
>
);
};
diff --git a/src/i18n/zh/user.json b/src/i18n/zh/user.json
new file mode 100644
index 0000000..2548c90
--- /dev/null
+++ b/src/i18n/zh/user.json
@@ -0,0 +1,18 @@
+{
+ "Create User": "新增用戶",
+ "User Detail": "用戶詳細資料",
+ "User Authority": "用戶權限",
+ "Authority Pool": "權限池",
+ "Allocated Authority": "已分配權限",
+ "username": "用戶名稱",
+ "password": "密碼",
+ "Confirm Password": "確認密碼",
+ "Reset": "重置",
+ "Cancel": "取消",
+ "Confirm": "確認",
+ "name": "姓名",
+ "User ID": "用戶ID",
+ "User Name": "用戶名稱",
+ "User Group": "用戶群組",
+ "Authority": "權限"
+}
\ No newline at end of file