diff --git a/src/app/(main)/settings/skill/edit/page.tsx b/src/app/(main)/settings/skill/edit/page.tsx new file mode 100644 index 0000000..a2d9863 --- /dev/null +++ b/src/app/(main)/settings/skill/edit/page.tsx @@ -0,0 +1,28 @@ +import { Edit } from "@mui/icons-material"; +import { useSearchParams } from "next/navigation"; +// import EditStaff from "@/components/EditStaff"; +import { Suspense } from "react"; +import { I18nProvider, getServerI18n } from "@/i18n"; +// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; +import { Metadata } from "next"; +import EditSkill from "@/components/EditSkill"; +import { Typography } from "@mui/material"; + + +const EditSkillPage: React.FC = async () => { + const { t } = await getServerI18n("staff"); + + return ( + <> + {t("Edit Skill")} + + }> + + + + {/* */} + + ); +}; + +export default EditSkillPage; diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts index 204c1d9..6800b29 100644 --- a/src/app/api/group/actions.ts +++ b/src/app/api/group/actions.ts @@ -29,8 +29,8 @@ export interface record { records: auth[]; } -export const fetchAuth = cache(async (id?: number) => { - return serverFetchJson(`${BASE_API_URL}/group/auth/combo/${id ?? 0}`, { +export const fetchAuth = cache(async (target: string, id?: number) => { + return serverFetchJson(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, { next: { tags: ["auth"] }, }); }); diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index fff3da4..77b58b5 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -7,8 +7,10 @@ import { UserDetail, UserResult } from "."; import { cache } from "react"; export interface UserInputs { - username: string; - email: string; + name: string; + email?: string; + addAuthIds?: number[]; + removeAuthIds?: number[]; } export interface PasswordInputs { @@ -40,7 +42,7 @@ export const deleteUser = async (id: number) => { }; export const changePassword = async (data: any) => { - return serverFetchJson(`${BASE_API_URL}/user/change-password`, { + return serverFetchWithNoContent(`${BASE_API_URL}/user/change-password`, { method: "PATCH", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts index 3151b64..f34292f 100644 --- a/src/app/api/user/index.ts +++ b/src/app/api/user/index.ts @@ -3,7 +3,6 @@ import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; - export interface UserResult { action: any; id: number; @@ -20,6 +19,7 @@ export interface UserResult { phone2: string; remarks: string; groupId: number; + auths: any } // export interface DetailedUser extends UserResult { @@ -28,9 +28,10 @@ export interface UserResult { // } export interface UserDetail { - authIds: number[]; data: UserResult; + authIds: number[]; groupIds: number[]; + auths: any[] } export const preloadUser = () => { diff --git a/src/components/EditSkill/EditSkill.tsx b/src/components/EditSkill/EditSkill.tsx new file mode 100644 index 0000000..a34b6e9 --- /dev/null +++ b/src/components/EditSkill/EditSkill.tsx @@ -0,0 +1,151 @@ +"use client"; +import { SkillResult } from "@/app/api/skill"; +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 { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; +import EditSkillForm from "./EditSkillForm"; +import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions"; +import AuthAllocation from "../EditUser/AuthAllocation"; + +interface Props { + skills: SkillResult[]; +} + +const EditSkill: React.FC = async ({ skills }) => { + const { t } = useTranslation(); + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const searchParams = useSearchParams(); + const id = parseInt(searchParams.get("id") || "0"); + const [tabIndex, setTabIndex] = useState(0); + const [filteredSkill, setFilteredSkill] = useState(() => + skills.filter((s) => s.id === id)[0] as SkillResult + ); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + const postData = { + ...data, + id: id + } + await saveSkill(postData) + router.replace(`/settings/skill`) + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + useEffect(() => { + formProps.reset({ + name: filteredSkill.name, + code: filteredSkill.code, + description: filteredSkill.description + }); + }, [skills]); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + + return ( + <> + {serverError && ( + + {serverError} + + )} + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {tabIndex === 0 && } + + + + + + + + + ); +}; +export default EditSkill; diff --git a/src/components/EditSkill/EditSkillForm.tsx b/src/components/EditSkill/EditSkillForm.tsx new file mode 100644 index 0000000..120d2e5 --- /dev/null +++ b/src/components/EditSkill/EditSkillForm.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { CreateSkillInputs } from "@/app/api/skill/actions"; +import { + Box, + Button, + Card, + CardContent, + Grid, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { useSearchParams } from "next/navigation"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +interface Props { + // users: UserResult[] +} + +const EditSkillForm: React.FC = async ({}) => { + const { t } = useTranslation(); + const searchParams = useSearchParams(); + const idString = searchParams.get("id"); + const { + register, + setValue, + getValues, + formState: { errors, defaultValues }, + reset, + resetField, + } = useFormContext(); + // const formProps = useForm({}); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default EditSkillForm; diff --git a/src/components/EditSkill/EditSkillLoading.tsx b/src/components/EditSkill/EditSkillLoading.tsx new file mode 100644 index 0000000..74e08af --- /dev/null +++ b/src/components/EditSkill/EditSkillLoading.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 EditSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + Edit Skill + + + + + + + + + + + ); +}; + +export default EditSkillLoading; diff --git a/src/components/EditSkill/EditSkillWrapper.tsx b/src/components/EditSkill/EditSkillWrapper.tsx new file mode 100644 index 0000000..12d7a12 --- /dev/null +++ b/src/components/EditSkill/EditSkillWrapper.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import EditSkill from "./EditSkill"; +import EditSkillLoading from "./EditSkillLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; +import { fetchSkill } from "@/app/api/skill"; + +interface SubComponents { + Loading: typeof EditSkillLoading; +} + +const EditSkillWrapper: React.FC & SubComponents = async () => { + const skills = await fetchSkill() + console.log(skills) + + return ; +}; + +EditSkillWrapper.Loading = EditSkillLoading; + +export default EditSkillWrapper; diff --git a/src/components/EditSkill/index.ts b/src/components/EditSkill/index.ts new file mode 100644 index 0000000..ba42dd8 --- /dev/null +++ b/src/components/EditSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./EditSkillWrapper"; diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index 61e9e8f..f1386fe 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -16,8 +16,8 @@ import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; import { StaffResult, StaffTeamTable } from "@/app/api/staff"; import SearchResults, { Column } from "../SearchResults"; import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; -import { Card } from "reactstrap"; import { + Card, Box, CardContent, Grid, @@ -49,8 +49,6 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { reset, resetField, } = useFormContext(); - - // let firstFilter: StaffResult[] = [] const initialStaffs = staff.map((s) => ({ ...s })); const [filteredStaff, setFilteredStaff] = useState(initialStaffs); diff --git a/src/components/EditUser/AuthAllocation.tsx b/src/components/EditUser/AuthAllocation.tsx new file mode 100644 index 0000000..afb44d5 --- /dev/null +++ b/src/components/EditUser/AuthAllocation.tsx @@ -0,0 +1,221 @@ +"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("Auth Name"), name: "name" }, + // { label: t("Current Position"), name: "currentPosition" }, + ], + [addAuth, t] + ); + + const allocatedAuthColumns = useMemo[]>( + () => [ + { + label: t("Remove"), + name: "id", + onClick: removeAuth, + buttonIcon: , + }, + { label: t("authority"), name: "authority" }, + { label: t("Auth Name"), 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(() => { + // 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)) + // }) + // ); + }, [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 \ No newline at end of file diff --git a/src/components/EditUser/EditUser.tsx b/src/components/EditUser/EditUser.tsx index db52fac..853de6a 100644 --- a/src/components/EditUser/EditUser.tsx +++ b/src/components/EditUser/EditUser.tsx @@ -1,6 +1,6 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; import SearchResults, { Column } from "../SearchResults"; // import { TeamResult } from "@/app/api/team"; import { useTranslation } from "react-i18next"; @@ -26,9 +26,11 @@ import { } from "react-hook-form"; import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; import { StaffResult } from "@/app/api/staff"; -import { editUser, fetchUserDetails } from "@/app/api/user/actions"; +import { UserInputs, editUser, fetchUserDetails } from "@/app/api/user/actions"; import UserDetail from "./UserDetail"; import { UserResult } from "@/app/api/user"; +import { auth, fetchAuth } from "@/app/api/group/actions"; +import AuthAllocation from "./AuthAllocation"; interface Props { // users: UserResult[] @@ -36,11 +38,14 @@ interface Props { const EditUser: React.FC = async ({ }) => { const { t } = useTranslation(); - const formProps = useForm(); + 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 [data, setData] = useState(); + const [auths, setAuths] = useState(); const handleTabChange = useCallback>( (_e, newValue) => { @@ -49,38 +54,45 @@ const EditUser: React.FC = async ({ }) => { [] ); - const [serverError, setServerError] = useState(""); - const [data, setData] = useState(); + const errors = formProps.formState.errors; const fetchUserDetail = async () => { console.log(id); try { + // fetch user info const userDetail = await fetchUserDetails(id); console.log(userDetail); const _data = userDetail.data as UserResult; console.log(_data); setData(_data); + //fetch user auths + const authDetail = await fetchAuth("user", id); + setAuths(authDetail.records) + const addAuthIds = authDetail.records.filter((item) => item.v === 1).map((item) => item.id).sort((a, b) => a - b); + formProps.reset({ - username: _data.username, - firstname: _data.firstname, - lastname: _data.lastname, - title: _data.title, - department: _data.department, + name: _data.username, email: _data.email, - phone1: _data.phone1, - phone2: _data.phone2, - remarks: _data.remarks, + addAuthIds: addAuthIds || [] }); } catch (error) { console.log(error); setServerError(t("An error has occurred. Please try again later.")); } - }; + } useEffect(() => { fetchUserDetail(); }, []); + // useEffect(() => { + // const thisUser = users.filter((item) => item.id === id) + // formProps.reset({ + // username: thisUser[0].username, + // email: thisUser[0].email, + // }); + // }, []); + const hasErrorsInTab = ( tabIndex: number, errors: FieldErrors @@ -97,14 +109,16 @@ const EditUser: React.FC = async ({ }) => { router.back(); }; - const onSubmit = useCallback>( + const onSubmit = useCallback>( async (data) => { try { console.log(data); const tempData = { - username: data.username, + name: data.name, email: data.email, - locked: false + locked: false, + addAuthIds: data.addAuthIds || [], + removeAuthIds: data.removeAuthIds || [], } console.log(tempData); await editUser(id, tempData); @@ -116,7 +130,7 @@ const EditUser: React.FC = async ({ }) => { }, [router] ); - const onSubmitError = useCallback>( + const onSubmitError = useCallback>( (errors) => { console.log(errors); }, @@ -136,7 +150,31 @@ const EditUser: React.FC = async ({ }) => { component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - + + + + ) : undefined + } + iconPosition="end" + /> + + + + {tabIndex == 0 && } + {tabIndex === 1 && }