@@ -0,0 +1,26 @@ | |||||
import EditPosition from "@/components/EditPosition"; | |||||
import EditUserGroup from "@/components/EditUserGroup"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { Metadata } from "next"; | |||||
export const metadata: Metadata = { | |||||
title: "Edit User Group", | |||||
}; | |||||
const Positions: React.FC = async () => { | |||||
const { t } = await getServerI18n("group"); | |||||
// Preload necessary dependencies | |||||
return ( | |||||
<> | |||||
{/* <Typography variant="h4">{t("Edit User Group")}</Typography> */} | |||||
<I18nProvider namespaces={["group"]}> | |||||
<EditUserGroup /> | |||||
</I18nProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default Positions; |
@@ -29,11 +29,11 @@ export interface record { | |||||
records: auth[]; | records: auth[]; | ||||
} | } | ||||
export const fetchAuth = cache(async () => { | |||||
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/combo`, { | |||||
next: { tags: ["auth"] }, | |||||
}); | |||||
export const fetchAuth = cache(async (id?: number) => { | |||||
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/combo/${id ?? 0}`, { | |||||
next: { tags: ["auth"] }, | |||||
}); | }); | ||||
}); | |||||
export const saveGroup = async (data: CreateGroupInputs) => { | export const saveGroup = async (data: CreateGroupInputs) => { | ||||
return serverFetchJson(`${BASE_API_URL}/group/save`, { | return serverFetchJson(`${BASE_API_URL}/group/save`, { | ||||
@@ -41,4 +41,11 @@ export const saveGroup = async (data: CreateGroupInputs) => { | |||||
body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
}); | }); | ||||
}; | |||||
}; | |||||
export const deleteGroup = async (id: number) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/group/${id}`, { | |||||
method: "DELETE", | |||||
headers: { "Content-Type": "application/json" }, | |||||
}); | |||||
}; |
@@ -50,6 +50,7 @@ const AuthorityAllocation: React.FC<Props> = ({ auth }) => { | |||||
); | ); | ||||
} | } | ||||
); | ); | ||||
// Adding / Removing Auth | // Adding / Removing Auth | ||||
const addAuth = useCallback((auth: auth) => { | const addAuth = useCallback((auth: auth) => { | ||||
setSelectedAuths((a) => [...a, auth]); | setSelectedAuths((a) => [...a, auth]); | ||||
@@ -126,10 +127,6 @@ const AuthorityAllocation: React.FC<Props> = ({ auth }) => { | |||||
// ); | // ); | ||||
}, [auth, query]); | }, [auth, query]); | ||||
useEffect(() => { | |||||
// console.log(getValues("addStaffIds")) | |||||
}, [initialAuths]); | |||||
const resetAuth = React.useCallback(() => { | const resetAuth = React.useCallback(() => { | ||||
clearQueryInput(); | clearQueryInput(); | ||||
clearAuth(); | clearAuth(); | ||||
@@ -0,0 +1,210 @@ | |||||
"use client"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
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 { CreateGroupInputs, auth } from "@/app/api/group/actions"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { Add, Clear, Remove, Search } from "@mui/icons-material"; | |||||
export interface Props { | |||||
auth: auth[]; | |||||
} | |||||
const AuthorityAllocation: React.FC<Props> = ({ auth }) => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
setValue, | |||||
getValues, | |||||
formState: { defaultValues }, | |||||
reset, | |||||
resetField, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
console.log(auth) | |||||
const initialAuths = auth.map((a) => ({ ...a })).sort((a, b) => a.id - b.id); | |||||
const [filteredAuths, setFilteredAuths] = useState(initialAuths); | |||||
const [selectedAuths, setSelectedAuths] = useState<typeof filteredAuths>( | |||||
() => initialAuths.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("Auth Name"), name: "name" }, | |||||
// { label: t("Current Position"), name: "currentPosition" }, | |||||
], | |||||
[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("Auth Name"), 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(() => { | |||||
// 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)) | |||||
// }) | |||||
// ); | |||||
}, [auth, 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 staff ID, name 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 AuthorityAllocation; |
@@ -0,0 +1,165 @@ | |||||
"use client"; | |||||
import { useRouter, useSearchParams } from "next/navigation"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
// import { TeamResult } from "@/app/api/team"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | |||||
import { CreateTeamInputs, saveTeam } from "@/app/api/team/actions"; | |||||
import { | |||||
FieldErrors, | |||||
FormProvider, | |||||
SubmitHandler, | |||||
useForm, | |||||
useFormContext, | |||||
} from "react-hook-form"; | |||||
import { Check, Close, Error } from "@mui/icons-material"; | |||||
import { StaffResult } from "@/app/api/staff"; | |||||
import { CreateGroupInputs, auth, fetchAuth, saveGroup } from "@/app/api/group/actions"; | |||||
import { UserGroupResult } from "@/app/api/group"; | |||||
import { UserResult } from "@/app/api/user"; | |||||
import GroupInfo from "./GroupInfo"; | |||||
import AuthorityAllocation from "./AuthorityAllocation"; | |||||
import UserAllocation from "./UserAllocation"; | |||||
interface Props { | |||||
groups: UserGroupResult[]; | |||||
// auths: auth[]; | |||||
users: UserResult[]; | |||||
} | |||||
const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||||
// console.log(users) | |||||
const { t } = useTranslation(); | |||||
const [serverError, setServerError] = useState(""); | |||||
const formProps = useForm<CreateGroupInputs>(); | |||||
const searchParams = useSearchParams(); | |||||
const id = parseInt(searchParams.get("id") || "0"); | |||||
const router = useRouter(); | |||||
const [tabIndex, setTabIndex] = useState(0); | |||||
const [auths, setAuths] = useState<auth[]>(); | |||||
const errors = formProps.formState.errors; | |||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[] | |||||
); | |||||
const hasErrorsInTab = ( | |||||
tabIndex: number, | |||||
errors: FieldErrors<CreateGroupInputs> | |||||
) => { | |||||
switch (tabIndex) { | |||||
case 0: | |||||
return Object.keys(errors).length > 0; | |||||
default: | |||||
false; | |||||
} | |||||
}; | |||||
const onSubmit = useCallback<SubmitHandler<CreateGroupInputs>>( | |||||
async (data) => { | |||||
try { | |||||
console.log(data); | |||||
const tempData = { | |||||
...data, | |||||
removeUserIds: data.removeUserIds ?? [], | |||||
removeAuthIds: data.removeAuthIds ?? [], | |||||
id: id | |||||
} | |||||
console.log(tempData) | |||||
await saveGroup(tempData); | |||||
router.replace("/settings/group"); | |||||
} catch (e) { | |||||
console.log(e); | |||||
setServerError(t("An error has occurred. Please try again later.")); | |||||
} | |||||
}, | |||||
[router] | |||||
); | |||||
useEffect(() => { | |||||
const thisGroup = groups.filter((item) => item.id === id)[0]; | |||||
const addUserIds = users.filter((item) => item.groupId === id).map((data) => data.id) | |||||
let addAuthIds: number[] = [] | |||||
fetchAuth(id).then((data) => { | |||||
setAuths(data.records) | |||||
addAuthIds = data.records.filter((data) => data.v === 1).map((data) => data.id).sort((a, b) => a - b); | |||||
formProps.reset({ | |||||
name: thisGroup.name, | |||||
description: thisGroup.description, | |||||
addAuthIds: addAuthIds, | |||||
addUserIds: addUserIds, | |||||
}); | |||||
}); | |||||
// console.log(auths) | |||||
}, [groups, users]); | |||||
return ( | |||||
<> | |||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("Edit User Group")} | |||||
</Typography> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
<Tabs | |||||
value={tabIndex} | |||||
onChange={handleTabChange} | |||||
variant="scrollable" | |||||
> | |||||
<Tab | |||||
label={t("Group Info")} | |||||
icon={ | |||||
hasErrorsInTab(0, errors) ? ( | |||||
<Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
) : undefined | |||||
} | |||||
iconPosition="end" | |||||
/> | |||||
<Tab label={t("Authority Allocation")} iconPosition="end" /> | |||||
<Tab label={t("User Allocation")} iconPosition="end" /> | |||||
</Tabs> | |||||
</Stack> | |||||
{serverError && ( | |||||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
{serverError} | |||||
</Typography> | |||||
)} | |||||
{tabIndex === 0 && <GroupInfo />} | |||||
{tabIndex === 1 && <AuthorityAllocation auth={auths!!}/>} | |||||
{tabIndex === 2 && <UserAllocation users={users!!} />} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
// onClick={handleCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
// disabled={Boolean(formProps.watch("isGridEditing"))} | |||||
> | |||||
{t("Confirm")} | |||||
</Button> | |||||
</Stack> | |||||
</Stack> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default EditUserGroup; |
@@ -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 EditUserGroupLoading: 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>EditUserGroup | |||||
<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 EditUserGroupLoading; |
@@ -0,0 +1,31 @@ | |||||
import React from "react"; | |||||
import EditUserGroup from "./EditUserGroup"; | |||||
import EditUserGroupLoading from "./EditUserGroupLoading"; | |||||
import { fetchGroup } from "@/app/api/group"; | |||||
import { fetchAuth } from "@/app/api/group/actions"; | |||||
import { fetchUser } from "@/app/api/user"; | |||||
import { useSearchParams } from "next/navigation"; | |||||
interface SubComponents { | |||||
Loading: typeof EditUserGroupLoading; | |||||
} | |||||
const EditUserGroupWrapper: React.FC & SubComponents = async () => { | |||||
const [ | |||||
groups, | |||||
// auths, | |||||
users, | |||||
] = await Promise.all([ | |||||
fetchGroup(), | |||||
// fetchAuth(), | |||||
fetchUser(), | |||||
]); | |||||
console.log(users) | |||||
return <EditUserGroup groups={groups.records} users={users}/>; | |||||
}; | |||||
EditUserGroupWrapper.Loading = EditUserGroupLoading; | |||||
export default EditUserGroupWrapper; |
@@ -0,0 +1,81 @@ | |||||
"use client"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Box from "@mui/material/Box"; | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import TextField from "@mui/material/TextField"; | |||||
import Typography from "@mui/material/Typography"; | |||||
import { CreateGroupInputs } from "@/app/api/group/actions"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { useCallback } from "react"; | |||||
const GroupInfo: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
register, | |||||
formState: { errors, defaultValues }, | |||||
control, | |||||
reset, | |||||
resetField, | |||||
setValue, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
const resetGroup = useCallback(() => { | |||||
console.log(defaultValues); | |||||
if (defaultValues !== undefined) { | |||||
resetField("description"); | |||||
} | |||||
}, [defaultValues]); | |||||
return ( | |||||
<Card sx={{ display: "block" }}> | |||||
<CardContent component={Stack} spacing={4}> | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Group Info")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<TextField | |||||
label={t("Group Name")} | |||||
fullWidth | |||||
{...register("name", { | |||||
required: true, | |||||
})} | |||||
error={Boolean(errors.name)} | |||||
helperText={ | |||||
Boolean(errors.name) && | |||||
(errors.name?.message | |||||
? t(errors.name.message) | |||||
: t("Please input correct name")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
<Grid item xs={12}> | |||||
<TextField | |||||
label={t("Group Description")} | |||||
fullWidth | |||||
multiline | |||||
rows={4} | |||||
{...register("description")} | |||||
error={Boolean(errors.description)} | |||||
helperText={ | |||||
Boolean(errors.description) && | |||||
(errors.description?.message | |||||
? t(errors.description.message) | |||||
: t("Please input correct description")) | |||||
} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
</CardContent> | |||||
</Card> | |||||
); | |||||
}; | |||||
export default GroupInfo; |
@@ -0,0 +1,216 @@ | |||||
"use client"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
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 { CreateGroupInputs, auth } from "@/app/api/group/actions"; | |||||
import SearchResults, { Column } from "../SearchResults"; | |||||
import { Add, Clear, Remove, Search } from "@mui/icons-material"; | |||||
import { UserResult } from "@/app/api/user"; | |||||
export interface Props { | |||||
users: UserResult[]; | |||||
} | |||||
const UserAllocation: React.FC<Props> = ({ users }) => { | |||||
const { t } = useTranslation(); | |||||
const { | |||||
setValue, | |||||
getValues, | |||||
formState: { defaultValues }, | |||||
reset, | |||||
resetField, | |||||
} = useFormContext<CreateGroupInputs>(); | |||||
const initialUsers = users.map((u) => ({ ...u })).sort((a, b) => a.id - b.id); | |||||
const [filteredUsers, setFilteredUsers] = useState(initialUsers); | |||||
const [selectedUsers, setSelectedUsers] = useState<typeof filteredUsers>( | |||||
() => { | |||||
return filteredUsers.filter( | |||||
(s) => getValues("addUserIds")?.includes(s.id) | |||||
); | |||||
} | |||||
); | |||||
const [deletedUserIds, setDeletedUserIds] = useState<number[]>([]); | |||||
// Adding / Removing Auth | |||||
const addUser = useCallback((users: UserResult) => { | |||||
setSelectedUsers((a) => [...a, users]); | |||||
}, []); | |||||
const removeUser = useCallback((users: UserResult) => { | |||||
setSelectedUsers((a) => a.filter((a) => a.id !== users.id)); | |||||
setDeletedUserIds((prevIds) => [...prevIds, users.id]); | |||||
}, []); | |||||
const clearUser = useCallback(() => { | |||||
if (defaultValues !== undefined) { | |||||
resetField("addUserIds"); | |||||
setSelectedUsers( | |||||
initialUsers.filter((s) => defaultValues.addUserIds?.includes(s.id)) | |||||
); | |||||
} | |||||
}, [defaultValues]); | |||||
// Sync with form | |||||
useEffect(() => { | |||||
setValue( | |||||
"addUserIds", | |||||
selectedUsers.map((u) => u.id) | |||||
); | |||||
setValue( | |||||
"removeUserIds", | |||||
deletedUserIds | |||||
); | |||||
}, [selectedUsers, deletedUserIds, setValue]); | |||||
const UserPoolColumns = useMemo<Column<UserResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Add"), | |||||
name: "id", | |||||
onClick: addUser, | |||||
buttonIcon: <Add />, | |||||
}, | |||||
{ label: t("User Name"), name: "username" }, | |||||
{ label: t("name"), name: "name" }, | |||||
], | |||||
[addUser, t] | |||||
); | |||||
const allocatedUserColumns = useMemo<Column<UserResult>[]>( | |||||
() => [ | |||||
{ | |||||
label: t("Remove"), | |||||
name: "id", | |||||
onClick: removeUser, | |||||
buttonIcon: <Remove color="warning" />, | |||||
}, | |||||
{ label: t("User Name"), name: "username" }, | |||||
{ label: t("name"), name: "name" }, | |||||
], | |||||
[removeUser, selectedUsers, 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(() => { | |||||
// 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)) | |||||
// }) | |||||
// ); | |||||
}, [users, query]); | |||||
const resetUser = React.useCallback(() => { | |||||
clearQueryInput(); | |||||
clearUser(); | |||||
}, [clearQueryInput, clearUser]); | |||||
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("User")} | |||||
</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 staff ID, name or position.")} | |||||
InputProps={{ | |||||
endAdornment: query && ( | |||||
<InputAdornment position="end"> | |||||
<IconButton onClick={clearQueryInput}> | |||||
<Clear /> | |||||
</IconButton> | |||||
</InputAdornment> | |||||
), | |||||
}} | |||||
/> | |||||
</Grid> | |||||
</Grid> | |||||
<Tabs value={tabIndex} onChange={handleTabChange}> | |||||
<Tab label={t("User Pool")} /> | |||||
<Tab | |||||
label={`${t("Allocated Users")} (${selectedUsers.length})`} | |||||
/> | |||||
</Tabs> | |||||
<Box sx={{ marginInline: -3 }}> | |||||
{tabIndex === 0 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={differenceBy(filteredUsers, selectedUsers, "id")} | |||||
columns={UserPoolColumns} | |||||
/> | |||||
)} | |||||
{tabIndex === 1 && ( | |||||
<SearchResults | |||||
noWrapper | |||||
items={selectedUsers} | |||||
columns={allocatedUserColumns} | |||||
/> | |||||
)} | |||||
</Box> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</FormProvider> | |||||
</> | |||||
); | |||||
}; | |||||
export default UserAllocation; |
@@ -0,0 +1 @@ | |||||
export { default } from "./EditUserGroupWrapper"; |
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation"; | |||||
import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | ||||
import { UserGroupResult } from "@/app/api/group"; | import { UserGroupResult } from "@/app/api/group"; | ||||
import { deleteUser } from "@/app/api/user/actions"; | import { deleteUser } from "@/app/api/user/actions"; | ||||
import { deleteGroup } from "@/app/api/group/actions"; | |||||
interface Props { | interface Props { | ||||
users: UserGroupResult[]; | users: UserGroupResult[]; | ||||
@@ -34,20 +35,20 @@ const UserGroupSearch: React.FC<Props> = ({ users }) => { | |||||
); | ); | ||||
const onUserClick = useCallback( | const onUserClick = useCallback( | ||||
(users: UserGroupResult) => { | |||||
console.log(users); | |||||
// router.push(`/settings/user/edit?id=${users.id}`) | |||||
(group: UserGroupResult) => { | |||||
console.log(group); | |||||
router.push(`/settings/group/edit?id=${group.id}`) | |||||
}, | }, | ||||
[router, t] | [router, t] | ||||
); | ); | ||||
const onDeleteClick = useCallback((users: UserGroupResult) => { | |||||
const onDeleteClick = useCallback((group: UserGroupResult) => { | |||||
deleteDialog(async () => { | deleteDialog(async () => { | ||||
await deleteUser(users.id); | |||||
await deleteGroup(group.id); | |||||
successDialog(t("Delete Success"), t); | successDialog(t("Delete Success"), t); | ||||
setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); | |||||
setFilteredUser((prev) => prev.filter((obj) => obj.id !== group.id)); | |||||
}, t); | }, t); | ||||
}, []); | }, []); | ||||