Procházet zdrojové kódy

create user,. edit user edit auths

create_edit_user
MSI\2Fi před 2 měsíci
rodič
revize
4f8a5c87b7
24 změnil soubory, kde provedl 1621 přidání a 25 odebrání
  1. +29
    -0
      src/app/(main)/settings/user/create/page.tsx
  2. +24
    -0
      src/app/(main)/settings/user/edit/page.tsx
  3. +35
    -9
      src/app/(main)/settings/user/page.tsx
  4. +0
    -3
      src/app/(main)/user/page.tsx
  5. +55
    -0
      src/app/api/group/actions.ts
  6. +34
    -0
      src/app/api/group/index.ts
  7. +72
    -0
      src/app/api/user/actions.ts
  8. +58
    -0
      src/app/api/user/index.ts
  9. +4
    -0
      src/app/utils/fetchUtil.ts
  10. +225
    -0
      src/components/CreateUser/AuthAllocation.tsx
  11. +243
    -0
      src/components/CreateUser/CreateUser.tsx
  12. +40
    -0
      src/components/CreateUser/CreateUserLoading.tsx
  13. +25
    -0
      src/components/CreateUser/CreateUserWrapper.tsx
  14. +104
    -0
      src/components/CreateUser/UserDetail.tsx
  15. +1
    -0
      src/components/CreateUser/index.ts
  16. +225
    -0
      src/components/EditUser/AuthAllocation.tsx
  17. +244
    -0
      src/components/EditUser/EditUser.tsx
  18. +40
    -0
      src/components/EditUser/EditUserLoading.tsx
  19. +32
    -0
      src/components/EditUser/EditUserWrapper.tsx
  20. +104
    -0
      src/components/EditUser/UserDetail.tsx
  21. +1
    -0
      src/components/EditUser/index.ts
  22. +2
    -4
      src/components/Logo/Logo.tsx
  23. +6
    -9
      src/components/UserSearch/UserSearch.tsx
  24. +18
    -0
      src/i18n/zh/user.json

+ 29
- 0
src/app/(main)/settings/user/create/page.tsx Zobrazit soubor

@@ -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 (
<>
<Typography variant="h4">{t("Create User")}</Typography>
<I18nProvider namespaces={["user", "common"]}>
<Suspense fallback={<CreateUser.Loading />}>
<CreateUser/>
</Suspense>
</I18nProvider>
</>
);
};

export default CreateStaffPage;

+ 24
- 0
src/app/(main)/settings/user/edit/page.tsx Zobrazit soubor

@@ -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<searchParamsProps> = async ({ searchParams }) => {
const { t } = await getServerI18n("user");
preloadUser();
return (
<>
<Typography variant="h4">{t("Edit User")}</Typography>
<I18nProvider namespaces={["user", "common"]}>
<Suspense fallback={<EditUser.Loading />}>
<EditUser searchParams={searchParams} />
</Suspense>
</I18nProvider>
</>
);
};
export default User;

+ 35
- 9
src/app/(main)/settings/user/page.tsx Zobrazit soubor

@@ -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 (
<I18nProvider namespaces={["dashboard"]}>
<Typography variant="h4" marginInlineEnd={2}>
User
</Typography>
<></>
</I18nProvider>
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("User")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/user/create"
>
{t("Create User")}
</Button>
</Stack>
<I18nProvider namespaces={["user", "common"]}>
<Suspense fallback={<UserSearch.Loading />}>
<UserSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default User;

+ 0
- 3
src/app/(main)/user/page.tsx Zobrazit soubor

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


+ 55
- 0
src/app/api/group/actions.ts Zobrazit soubor

@@ -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<record>(`${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
};

+ 34
- 0
src/app/api/group/index.ts Zobrazit soubor

@@ -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<Records>(`${BASE_API_URL}/group`, {
next: { tags: ["group"] },
});
});


export const fetchIndivGroup = cache(async (id: number) => {
return serverFetchJson<IndivUserGroup>(`${BASE_API_URL}/group/${id}`, {
next: { tags: ["group"] },
});
});

+ 72
- 0
src/app/api/user/actions.ts Zobrazit soubor

@@ -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<UserDetail>(`${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" },
});
};

+ 58
- 0
src/app/api/user/index.ts Zobrazit soubor

@@ -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<UserResult[]>(`${BASE_API_URL}/user`, {
next: { tags: ["user"] },
});
});

export const fetchUserDetail = cache(async (id: number) => {
return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, {
next: { tags: ["user"] },
});
});

export const fetchPwRules = cache(async () => {
return serverFetchJson<passwordRule>(`${BASE_API_URL}/user/password-rule`, {
next: { tags: ["pwRule"] },
});
});

+ 4
- 0
src/app/utils/fetchUtil.ts Zobrazit soubor

@@ -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) {


+ 225
- 0
src/components/CreateUser/AuthAllocation.tsx Zobrazit soubor

@@ -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<Props> = ({ auths }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<UserInputs>();
const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
const [filteredAuths, setFilteredAuths] = useState(initialAuths);
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>(
() => {
return filteredAuths.filter(
(s) => getValues("addAuthIds")?.includes(s.id)
);
}
);
const [removeAuthIds, setRemoveAuthIds] = useState<number[]>([]);

// 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<Column<auth>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addAuth,
buttonIcon: <Add />,
},
{ label: t("authority"), name: "authority" },
{ label: t("description"), name: "name" },
],
[addAuth, t]
);

const allocatedAuthColumns = useMemo<Column<auth>[]>(
() => [
{
label: t("Remove"),
name: "id",
onClick: removeAuth,
buttonIcon: <Remove color="warning" />,
},
{ label: t("authority"), name: "authority" },
{ label: t("description"), name: "name" },
],
[removeAuth, selectedAuths, t]
);

const [query, setQuery] = React.useState("");
const onQueryInputChange = React.useCallback<
React.ChangeEventHandler<HTMLInputElement>
>((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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
<CardContent
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("Authority")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t(
"Search by Authority or description or position."
)}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Authority Pool")} />
<Tab
label={`${t("Allocated Authority")} (${
selectedAuths.length
})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredAuths, selectedAuths, "id")}
columns={AuthPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedAuths}
columns={allocatedAuthColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>
</FormProvider>
</>
);
};
export default AuthAllocation;

+ 243
- 0
src/components/CreateUser/CreateUser.tsx Zobrazit soubor

@@ -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<Props> = async ({ rules, auths }) => {
console.log(auths)
const { t } = useTranslation("user");
const formProps = useForm<UserInputs>();
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<NonNullable<TabsProps["onChange"]>>(
(_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<UserResult>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

const handleCancel = () => {
router.back();
};

const onSubmit = useCallback<SubmitHandler<UserInputs>>(
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<SubmitErrorHandler<UserInputs>>(
(errors) => {
console.log(errors);
},
[]
);

return (
<>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("User Detail")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("User Authority")} iconPosition="end" />
</Tabs>
</Stack>
{tabIndex == 0 && <UserDetail />}
{tabIndex === 1 && <AuthAllocation auths={auths!} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={resetForm}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};
export default CreateUser;

+ 40
- 0
src/components/CreateUser/CreateUserLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>CreateUser
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default CreateUserLoading;

+ 25
- 0
src/components/CreateUser/CreateUserWrapper.tsx Zobrazit soubor

@@ -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 <CreateUser rules={pwRule} auths={auths.records} />;
};

CreateUserWrapper.Loading = CreateUserLoading;

export default CreateUserWrapper;

+ 104
- 0
src/components/CreateUser/UserDetail.tsx Zobrazit soubor

@@ -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<UserInputs>();

return (
<Card>
<CardContent component={Stack} spacing={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("User Detail")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("username")}
fullWidth
{...register("username", {
required: "username required!",
})}
error={Boolean(errors.username)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("password")}
fullWidth
{...register("password")}
// helperText={
// Boolean(errors.password) &&
// (errors.password?.message
// ? t(errors.password.message)
// :
// (<>
// - 8-20 characters
// <br/>
// - Uppercase letters
// <br/>
// - Lowercase letters
// <br/>
// - Numbers
// <br/>
// - Symbols
// </>)
// )
// }
helperText={
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;


{/* <>
- 8-20 characters
<br/>
- Uppercase letters
<br/>
- Lowercase letters
<br/>
- Numbers
<br/>
- Symbols
</> */}

+ 1
- 0
src/components/CreateUser/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./CreateUserWrapper";

+ 225
- 0
src/components/EditUser/AuthAllocation.tsx Zobrazit soubor

@@ -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<Props> = ({ auths }) => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const {
setValue,
getValues,
formState: { defaultValues },
reset,
resetField,
} = useFormContext<UserInputs>();
const initialAuths = auths.map((u) => ({ ...u })).sort((a, b) => a.id - b.id);
const [filteredAuths, setFilteredAuths] = useState(initialAuths);
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>(
() => {
return filteredAuths.filter(
(s) => getValues("addAuthIds")?.includes(s.id)
);
}
);
const [removeAuthIds, setRemoveAuthIds] = useState<number[]>([]);

// 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<Column<auth>[]>(
() => [
{
label: t("Add"),
name: "id",
onClick: addAuth,
buttonIcon: <Add />,
},
{ label: t("authority"), name: "authority" },
{ label: t("description"), name: "name" },
],
[addAuth, t]
);

const allocatedAuthColumns = useMemo<Column<auth>[]>(
() => [
{
label: t("Remove"),
name: "id",
onClick: removeAuth,
buttonIcon: <Remove color="warning" />,
},
{ label: t("authority"), name: "authority" },
{ label: t("description"), name: "name" },
],
[removeAuth, selectedAuths, t]
);

const [query, setQuery] = React.useState("");
const onQueryInputChange = React.useCallback<
React.ChangeEventHandler<HTMLInputElement>
>((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<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

return (
<>
<FormProvider {...formProps}>
<Card sx={{ display: "block" }}>
<CardContent
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("Authority")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
<TextField
variant="standard"
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t(
"Search by Authority or description or position."
)}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">
<IconButton onClick={clearQueryInput}>
<Clear />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Authority Pool")} />
<Tab
label={`${t("Allocated Authority")} (${
selectedAuths.length
})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredAuths, selectedAuths, "id")}
columns={AuthPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedAuths}
columns={allocatedAuthColumns}
/>
)}
</Box>
</Stack>
</CardContent>
</Card>
</FormProvider>
</>
);
};
export default AuthAllocation;

+ 244
- 0
src/components/EditUser/EditUser.tsx Zobrazit soubor

@@ -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<Props> = async ({ user, rules, auths }) => {
console.log(user)
const { t } = useTranslation("user");
const formProps = useForm<UserInputs>();
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<NonNullable<TabsProps["onChange"]>>(
(_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<UserResult>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

const handleCancel = () => {
router.back();
};

const onSubmit = useCallback<SubmitHandler<UserInputs>>(
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<SubmitErrorHandler<UserInputs>>(
(errors) => {
console.log(errors);
},
[]
);

return (
<>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("User Detail")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
) : undefined
}
iconPosition="end"
/>
<Tab label={t("User Authority")} iconPosition="end" />
</Tabs>
</Stack>
{tabIndex == 0 && <UserDetail />}
{tabIndex === 1 && <AuthAllocation auths={auths!} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={resetForm}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};
export default EditUser;

+ 40
- 0
src/components/EditUser/EditUserLoading.tsx Zobrazit soubor

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>EditUser
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default EditUserLoading;

+ 32
- 0
src/components/EditUser/EditUserWrapper.tsx Zobrazit soubor

@@ -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<searchParamsProps> & 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 <EditUser user={user.data} rules={pwRule} auths={auths.records} />;
};

EditUserWrapper.Loading = EditUserLoading;

export default EditUserWrapper;

+ 104
- 0
src/components/EditUser/UserDetail.tsx Zobrazit soubor

@@ -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<UserInputs>();

return (
<Card>
<CardContent component={Stack} spacing={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("User Detail")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("username")}
fullWidth
{...register("username", {
required: "username required!",
})}
error={Boolean(errors.username)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("password")}
fullWidth
{...register("password")}
// helperText={
// Boolean(errors.password) &&
// (errors.password?.message
// ? t(errors.password.message)
// :
// (<>
// - 8-20 characters
// <br/>
// - Uppercase letters
// <br/>
// - Lowercase letters
// <br/>
// - Numbers
// <br/>
// - Symbols
// </>)
// )
// }
helperText={
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;


{/* <>
- 8-20 characters
<br/>
- Uppercase letters
<br/>
- Lowercase letters
<br/>
- Numbers
<br/>
- Symbols
</> */}

+ 1
- 0
src/components/EditUser/index.ts Zobrazit soubor

@@ -0,0 +1 @@
export { default } from "./EditUserWrapper";

+ 2
- 4
src/components/Logo/Logo.tsx
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 6
- 9
src/components/UserSearch/UserSearch.tsx Zobrazit soubor

@@ -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<Omit<UserResult, "id">>;
type SearchParamNames = keyof SearchQuery;

@@ -25,8 +27,8 @@ const UserSearch: React.FC<Props> = ({ users }) => {
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("User Name"),
paramName: "title",
label: t("Username"),
paramName: "username",
type: "text",
},
],
@@ -56,12 +58,7 @@ const UserSearch: React.FC<Props> = ({ users }) => {
onClick: onUserClick,
buttonIcon: <EditNote />,
},
{ 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<Props> = ({ users }) => {
// )
}}
/>
<SearchResults<UserResult> items={filteredUser} columns={columns} />
<SearchResults<UserResult> items={filteredUser} columns={columns} pagingController={{ pageNum: 1, pageSize: 10, totalCount: 100 }}/>
</>
);
};


+ 18
- 0
src/i18n/zh/user.json Zobrazit soubor

@@ -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": "權限"
}

Načítá se…
Zrušit
Uložit