| @@ -60,24 +60,20 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => { | |||
| return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)) | |||
| } | |||
| ); | |||
| const [seletedTeamLead, setSeletedTeamLead] = useState<number>(); | |||
| const [deletedStaffIds, setDeletedStaffIds] = useState<number[]>([]); | |||
| // Adding / Removing staff | |||
| const addStaff = useCallback((staff: StaffResult) => { | |||
| setSelectedStaff((s) => [...s, staff]); | |||
| // setDeletedStaffIds((s) => s.filter((s) => s === selectedStaff.id)) | |||
| }, []); | |||
| const removeStaff = useCallback((staff: StaffResult) => { | |||
| setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); | |||
| // setDeletedStaffIds((s) => s) | |||
| setDeletedStaffIds((prevIds) => [...prevIds, staff.id]); | |||
| }, []); | |||
| const setTeamLead = useCallback( | |||
| (staff: StaffResult) => { | |||
| setSeletedTeamLead(staff.id); | |||
| const rearrangedList = getValues("addStaffIds").reduce<number[]>( | |||
| (acc, num, index) => { | |||
| if (num === staff.id && index !== 0) { | |||
| @@ -171,16 +167,16 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => { | |||
| }, []); | |||
| React.useEffect(() => { | |||
| // setFilteredStaff( | |||
| // initialStaffs.filter((s) => { | |||
| // const q = query.toLowerCase(); | |||
| // // s.staffId.toLowerCase().includes(q) | |||
| // // const q = query.toLowerCase(); | |||
| // // return s.name.toLowerCase().includes(q); | |||
| // // s.code.toString().includes(q) || | |||
| // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) | |||
| // }) | |||
| // ); | |||
| setFilteredStaff( | |||
| initialStaffs.filter((i) => { | |||
| const q = query.toLowerCase(); | |||
| return ( | |||
| i.staffId.toLowerCase().includes(q) || | |||
| i.name.toLowerCase().includes(q) || | |||
| i.currentPosition.toLowerCase().includes(q) | |||
| ); | |||
| }) | |||
| ); | |||
| }, [staff, query]); | |||
| useEffect(() => { | |||
| @@ -1,93 +1,97 @@ | |||
| "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 { | |||
| 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"; | |||
| 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"; | |||
| 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[] | |||
| } | |||
| 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 { 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[]>([]); | |||
| } | |||
| ); | |||
| 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]); | |||
| }, []); | |||
| // 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]); | |||
| const clearAuth = useCallback(() => { | |||
| if (defaultValues !== undefined) { | |||
| resetField("addAuthIds"); | |||
| setSelectedAuths( | |||
| initialAuths.filter( | |||
| (auth) => defaultValues.addAuthIds?.includes(auth.id) | |||
| ) | |||
| ); | |||
| } | |||
| }, [defaultValues]); | |||
| // Sync with form | |||
| // Sync with form | |||
| useEffect(() => { | |||
| setValue( | |||
| "addAuthIds", | |||
| selectedAuths.map((a) => a.id) | |||
| ); | |||
| setValue( | |||
| "removeAuthIds", | |||
| removeAuthIds | |||
| ); | |||
| setValue("removeAuthIds", removeAuthIds); | |||
| }, [selectedAuths, removeAuthIds, setValue]); | |||
| const AuthPoolColumns = useMemo<Column<auth>[]>( | |||
| () => [ | |||
| { | |||
| @@ -97,8 +101,7 @@ const AuthAllocation: React.FC<Props> = ({ auths }) => { | |||
| buttonIcon: <Add />, | |||
| }, | |||
| { label: t("authority"), name: "authority" }, | |||
| { label: t("Auth Name"), name: "name" }, | |||
| // { label: t("Current Position"), name: "currentPosition" }, | |||
| { label: t("description"), name: "name" }, | |||
| ], | |||
| [addAuth, t] | |||
| ); | |||
| @@ -109,10 +112,10 @@ const AuthAllocation: React.FC<Props> = ({ auths }) => { | |||
| label: t("Remove"), | |||
| name: "id", | |||
| onClick: removeAuth, | |||
| buttonIcon: <Remove color="warning"/>, | |||
| buttonIcon: <Remove color="warning" />, | |||
| }, | |||
| { label: t("authority"), name: "authority" }, | |||
| { label: t("Auth Name"), name: "name" }, | |||
| { label: t("description"), name: "name" }, | |||
| ], | |||
| [removeAuth, selectedAuths, t] | |||
| ); | |||
| @@ -128,16 +131,14 @@ const AuthAllocation: React.FC<Props> = ({ auths }) => { | |||
| }, []); | |||
| React.useEffect(() => { | |||
| // setFilteredStaff( | |||
| // initialStaffs.filter((s) => { | |||
| // const q = query.toLowerCase(); | |||
| // // s.staffId.toLowerCase().includes(q) | |||
| // // const q = query.toLowerCase(); | |||
| // // return s.name.toLowerCase().includes(q); | |||
| // // s.code.toString().includes(q) || | |||
| // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) | |||
| // }) | |||
| // ); | |||
| setFilteredAuths( | |||
| initialAuths.filter((a) => | |||
| ( | |||
| a.authority.toLowerCase().includes(query.toLowerCase()) || | |||
| a.name?.toLowerCase().includes(query.toLowerCase()) | |||
| ) | |||
| ) | |||
| ); | |||
| }, [auths, query]); | |||
| const resetAuth = React.useCallback(() => { | |||
| @@ -147,16 +148,16 @@ const AuthAllocation: React.FC<Props> = ({ auths }) => { | |||
| const formProps = useForm({}); | |||
| // Tab related | |||
| const [tabIndex, setTabIndex] = React.useState(0); | |||
| const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [] | |||
| ); | |||
| // Tab related | |||
| const [tabIndex, setTabIndex] = React.useState(0); | |||
| const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [] | |||
| ); | |||
| return ( | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Card sx={{ display: "block" }}> | |||
| @@ -175,7 +176,9 @@ return ( | |||
| fullWidth | |||
| onChange={onQueryInputChange} | |||
| value={query} | |||
| placeholder={t("Search by staff ID, name or position.")} | |||
| placeholder={t( | |||
| "Search by Authority or description or position." | |||
| )} | |||
| InputProps={{ | |||
| endAdornment: query && ( | |||
| <InputAdornment position="end"> | |||
| @@ -191,18 +194,20 @@ return ( | |||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||
| <Tab label={t("Authority Pool")} /> | |||
| <Tab | |||
| label={`${t("Allocated Authority")} (${selectedAuths.length})`} | |||
| label={`${t("Allocated Authority")} (${ | |||
| selectedAuths.length | |||
| })`} | |||
| /> | |||
| </Tabs> | |||
| <Box sx={{ marginInline: -3 }}> | |||
| {tabIndex === 0 && ( | |||
| {tabIndex === 0 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={differenceBy(filteredAuths, selectedAuths, "id")} | |||
| columns={AuthPoolColumns} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| {tabIndex === 1 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={selectedAuths} | |||
| @@ -216,6 +221,5 @@ return ( | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| } | |||
| export default AuthAllocation | |||
| }; | |||
| export default AuthAllocation; | |||
| @@ -1,6 +1,12 @@ | |||
| "use client"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; | |||
| import React, { | |||
| useCallback, | |||
| useEffect, | |||
| useLayoutEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| // import { TeamResult } from "@/app/api/team"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -26,23 +32,24 @@ import { | |||
| } from "react-hook-form"; | |||
| import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; | |||
| import { StaffResult } from "@/app/api/staff"; | |||
| import { UserInputs, adminChangePassword, editUser, fetchUserDetails } from "@/app/api/user/actions"; | |||
| import { | |||
| UserInputs, | |||
| adminChangePassword, | |||
| editUser, | |||
| fetchUserDetails, | |||
| } from "@/app/api/user/actions"; | |||
| import UserDetail from "./UserDetail"; | |||
| import { UserResult, passwordRule } from "@/app/api/user"; | |||
| import { auth, fetchAuth } from "@/app/api/group/actions"; | |||
| import AuthAllocation from "./AuthAllocation"; | |||
| interface Props { | |||
| user: UserResult, | |||
| rules: passwordRule, | |||
| auths: auth[] | |||
| } | |||
| user: UserResult; | |||
| rules: passwordRule; | |||
| auths: auth[]; | |||
| } | |||
| const EditUser: React.FC<Props> = async ({ | |||
| user, | |||
| rules, | |||
| auths | |||
| }) => { | |||
| const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||
| const { t } = useTranslation(); | |||
| const formProps = useForm<UserInputs>(); | |||
| const searchParams = useSearchParams(); | |||
| @@ -50,6 +57,13 @@ const EditUser: React.FC<Props> = async ({ | |||
| 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) => { | |||
| @@ -60,22 +74,27 @@ const EditUser: React.FC<Props> = async ({ | |||
| const errors = formProps.formState.errors; | |||
| useEffect(() => { | |||
| const resetForm = React.useCallback(() => { | |||
| console.log("triggerred"); | |||
| console.log(addAuthIds); | |||
| try { | |||
| const addAuthIds = auths && auths.length > 0 | |||
| ? auths.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b) | |||
| : [] | |||
| formProps.reset({ | |||
| name: user.username, | |||
| email: user.email, | |||
| addAuthIds: addAuthIds | |||
| addAuthIds: addAuthIds, | |||
| removeAuthIds: [], | |||
| password: "", | |||
| }); | |||
| console.log(formProps.formState.defaultValues); | |||
| } catch (error) { | |||
| console.log(error); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| }, [user, auths]); | |||
| }, [auths, user]); | |||
| useEffect(() => { | |||
| resetForm(); | |||
| }, []); | |||
| const hasErrorsInTab = ( | |||
| tabIndex: number, | |||
| @@ -96,22 +115,33 @@ const EditUser: React.FC<Props> = async ({ | |||
| 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 = '' | |||
| 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 | |||
| pw = data.password; | |||
| if (pw.length < rules.min) { | |||
| haveError = true | |||
| formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" }) | |||
| 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" }) | |||
| 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" }) | |||
| haveError = true; | |||
| formProps.setError("password", { | |||
| message: | |||
| "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", | |||
| type: "required", | |||
| }); | |||
| } | |||
| } | |||
| const userData = { | |||
| @@ -119,16 +149,16 @@ const EditUser: React.FC<Props> = async ({ | |||
| locked: false, | |||
| addAuthIds: data.addAuthIds || [], | |||
| removeAuthIds: data.removeAuthIds || [], | |||
| } | |||
| }; | |||
| const pwData = { | |||
| id: id, | |||
| password: "", | |||
| newPassword: pw | |||
| } | |||
| newPassword: pw, | |||
| }; | |||
| if (haveError) { | |||
| return | |||
| return; | |||
| } | |||
| console.log("passed") | |||
| console.log("passed"); | |||
| await editUser(id, userData); | |||
| if (data.password && data.password.length > 0) { | |||
| await adminChangePassword(pwData); | |||
| @@ -185,12 +215,12 @@ const EditUser: React.FC<Props> = async ({ | |||
| </Tabs> | |||
| </Stack> | |||
| {tabIndex == 0 && <UserDetail />} | |||
| {tabIndex === 1 && <AuthAllocation auths={auths!}/>} | |||
| {tabIndex === 1 && <AuthAllocation auths={auths!} />} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| variant="text" | |||
| startIcon={<RestartAlt />} | |||
| // onClick={() => console.log("asdasd")} | |||
| onClick={resetForm} | |||
| > | |||
| {t("Reset")} | |||
| </Button> | |||
| @@ -9,13 +9,13 @@ import { | |||
| 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, | |||
| @@ -45,6 +45,30 @@ const UserDetail: React.FC = () => { | |||
| 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> | |||
| @@ -55,3 +79,16 @@ const UserDetail: React.FC = () => { | |||
| }; | |||
| export default UserDetail; | |||
| {/* <> | |||
| - 8-20 characters | |||
| <br/> | |||
| - Uppercase letters | |||
| <br/> | |||
| - Lowercase letters | |||
| <br/> | |||
| - Numbers | |||
| <br/> | |||
| - Symbols | |||
| </> */} | |||