Ver código fonte

update

tags/Baseline_30082024_FRONTEND_UAT
MSI\derek 1 ano atrás
pai
commit
c217d880a3
24 arquivos alterados com 1031 adições e 11 exclusões
  1. +22
    -0
      src/app/(main)/settings/group/create/page.tsx
  2. +0
    -0
      src/app/(main)/settings/group/edit/page.tsx
  3. +55
    -0
      src/app/(main)/settings/group/page.tsx
  4. +0
    -4
      src/app/(main)/settings/team/create/page.tsx
  5. +24
    -0
      src/app/(main)/settings/user/edit/page.tsx
  6. +2
    -2
      src/app/(main)/settings/user/page.tsx
  7. +44
    -0
      src/app/api/group/actions.ts
  8. +21
    -0
      src/app/api/group/index.ts
  9. +9
    -2
      src/app/api/user/actions.ts
  10. +1
    -0
      src/app/api/user/index.ts
  11. +211
    -0
      src/components/CreateGroup/AuthorityAllocation.tsx
  12. +130
    -0
      src/components/CreateGroup/CreateGroup.tsx
  13. +40
    -0
      src/components/CreateGroup/CreateGroupLoading.tsx
  14. +24
    -0
      src/components/CreateGroup/CreateGroupWrapper.tsx
  15. +81
    -0
      src/components/CreateGroup/GroupInfo.tsx
  16. +209
    -0
      src/components/CreateGroup/UserAllocation.tsx
  17. +1
    -0
      src/components/CreateGroup/index.ts
  18. +1
    -1
      src/components/CreateTeam/TeamInfo.tsx
  19. +1
    -2
      src/components/EditTeam/Allocation.tsx
  20. +1
    -0
      src/components/NavigationContent/NavigationContent.tsx
  21. +94
    -0
      src/components/UserGroupSearch/UserGroupSearch.tsx
  22. +40
    -0
      src/components/UserGroupSearch/UserGroupSearchLoading.tsx
  23. +19
    -0
      src/components/UserGroupSearch/UserGroupSearchWrapper.tsx
  24. +1
    -0
      src/components/UserGroupSearch/index.ts

+ 22
- 0
src/app/(main)/settings/group/create/page.tsx Ver arquivo

@@ -0,0 +1,22 @@
// 'use client';
import { I18nProvider, getServerI18n } from "@/i18n";
import React, { useCallback, useState } from "react";
import { Typography } from "@mui/material";
import CreateGroup from "@/components/CreateGroup";

// const Title = ["title1", "title2"];

const CreateStaff: React.FC = async () => {
const { t } = await getServerI18n("group");

return (
<>
<Typography variant="h4">{t("Create Group")}</Typography>
<I18nProvider namespaces={["group"]}>
<CreateGroup />
</I18nProvider>
</>
);
};

export default CreateStaff;

+ 0
- 0
src/app/(main)/settings/group/edit/page.tsx Ver arquivo


+ 55
- 0
src/app/(main)/settings/group/page.tsx Ver arquivo

@@ -0,0 +1,55 @@
import { preloadClaims } from "@/app/api/claims";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import StaffSearch from "@/components/StaffSearch";
import TeamSearch from "@/components/TeamSearch";
import UserGroupSearch from "@/components/UserGroupSearch";
import UserSearch from "@/components/UserSearch";
import { I18nProvider, getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";


export const metadata: Metadata = {
title: "User Group",
};


const UserGroup: React.FC = async () => {
const { t } = await getServerI18n("User Group");
// preloadTeamLeads();
// preloadStaff();
return (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("User Group")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/group/create"
>
{t("Create User Group")}
</Button>
</Stack>
<I18nProvider namespaces={["User Group", "common"]}>
<Suspense fallback={<UserGroupSearch.Loading />}>
<UserGroupSearch />
</Suspense>
</I18nProvider>
</>
);
};
export default UserGroup;

+ 0
- 4
src/app/(main)/settings/team/create/page.tsx Ver arquivo

@@ -28,10 +28,6 @@ import CreateTeam from "@/components/CreateTeam";
const CreateTeamPage: React.FC = async () => {
const { t } = await getServerI18n("team");

const title = ['', t('Additional Info')]
// const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$")
// console.log(regex)

return (
<>
<Typography variant="h4">{t("Create Team")}</Typography>


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

@@ -0,0 +1,24 @@
import { Edit } from "@mui/icons-material";
import { useSearchParams } from "next/navigation";
// import EditStaff from "@/components/EditStaff";
import { Suspense } from "react";
import { I18nProvider } from "@/i18n";
// import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper";
import { Metadata } from "next";
import EditUser from "@/components/EditUser";


const EditUserPage: React.FC = () => {

return (
<>
<I18nProvider namespaces={["team", "common"]}>
<Suspense fallback={<EditUser.Loading />}>
<EditUser />
</Suspense>
</I18nProvider>
</>
);
};

export default EditUserPage;

+ 2
- 2
src/app/(main)/settings/user/page.tsx Ver arquivo

@@ -33,14 +33,14 @@ export const metadata: Metadata = {
<Typography variant="h4" marginInlineEnd={2}>
{t("User")}
</Typography>
<Button
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/settings/team/create"
>
{t("Create User")}
</Button>
</Button> */}
</Stack>
<I18nProvider namespaces={["User", "common"]}>
<Suspense fallback={<UserSearch.Loading />}>


+ 44
- 0
src/app/api/group/actions.ts Ver arquivo

@@ -0,0 +1,44 @@
"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 () => {
return serverFetchJson<record>(`${BASE_API_URL}/group/auth/combo`, {
next: { tags: ["auth"] },
});
});
export const saveGroup = async (data: CreateGroupInputs) => {
return serverFetchJson(`${BASE_API_URL}/group/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 21
- 0
src/app/api/group/index.ts Ver arquivo

@@ -0,0 +1,21 @@
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 const fetchGroup = cache(async () => {
return serverFetchJson<Records>(`${BASE_API_URL}/group`, {
next: { tags: ["group"] },
});
});

+ 9
- 2
src/app/api/user/actions.ts Ver arquivo

@@ -8,8 +8,7 @@ import { cache } from "react";

export interface UserInputs {
username: string;
firstname: string;
lastname: string;
email: string;
}


@@ -19,6 +18,14 @@ export const fetchUserDetails = cache(async (id: number) => {
});
});

export const editUser = async (id: number, data: UserInputs) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "PUT",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const deleteUser = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "DELETE",


+ 1
- 0
src/app/api/user/index.ts Ver arquivo

@@ -19,6 +19,7 @@ export interface UserResult {
phone1: string;
phone2: string;
remarks: string;
groupId: number;
}

// export interface DetailedUser extends UserResult {


+ 211
- 0
src/components/CreateGroup/AuthorityAllocation.tsx Ver arquivo

@@ -0,0 +1,211 @@
"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>();
const initialAuths = auth.map((a) => ({ ...a })).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)
);
}
);
// 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));
}, []);

const clearAuth = useCallback(() => {
if (defaultValues !== undefined) {
resetField("addAuthIds");
setSelectedAuths(
initialAuths.filter((s) => defaultValues.addAuthIds?.includes(s.id))
);
}
}, [defaultValues]);

// Sync with form
useEffect(() => {
setValue(
"addAuthIds",
selectedAuths.map((a) => a.id)
);
}, [selectedAuths, 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]);

useEffect(() => {
// console.log(getValues("addStaffIds"))
}, [initialAuths]);

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;

+ 130
- 0
src/components/CreateGroup/CreateGroup.tsx Ver arquivo

@@ -0,0 +1,130 @@
"use client";

import { CreateGroupInputs, auth, saveGroup } from "@/app/api/group/actions";
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { FieldErrors, FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Check, Close, Error } from "@mui/icons-material";
import GroupInfo from "./GroupInfo";
import AuthorityAllocation from "./AuthorityAllocation";
import UserAllocation from "./UserAllocation";
import { UserResult } from "@/app/api/user";

interface Props {
auth?: auth[]
users?: UserResult[]
}

const CreateGroup: React.FC<Props> = ({ auth, users }) => {
const formProps = useForm<CreateGroupInputs>();
const [serverError, setServerError] = useState("");
const router = useRouter();
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();

const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateGroupInputs>>(
async (data) => {
try {
console.log(data);
const postData = {
...data,
removeUserIds: [],
removeAuthIds: [],

}
console.log(postData)
await saveGroup(postData)
router.replace("/settings/group")
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
}
},
[router]
);
const handleCancel = () => {
router.back();
};

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;
}
};

return (
<>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<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>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <GroupInfo/>}
{tabIndex === 1 && <AuthorityAllocation auth={auth!!}/>}
{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 CreateGroup;

+ 40
- 0
src/components/CreateGroup/CreateGroupLoading.tsx Ver arquivo

@@ -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 CreateGroupLoading: 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>Create Group
<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 CreateGroupLoading;

+ 24
- 0
src/components/CreateGroup/CreateGroupWrapper.tsx Ver arquivo

@@ -0,0 +1,24 @@
import React from "react";
import CreateGroupLoading from "./CreateGroupLoading";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { useSearchParams } from "next/navigation";
import CreateGroup from "./CreateGroup";
import { auth, fetchAuth } from "@/app/api/group/actions";
import { fetchUser } from "@/app/api/user";

interface SubComponents {
Loading: typeof CreateGroupLoading;
}

const CreateGroupWrapper: React.FC & SubComponents = async () => {
const records = await fetchAuth()
const users = await fetchUser()
console.log(users)
const auth = records.records as auth[]

return <CreateGroup auth={auth} users={users}/>;
};

CreateGroupWrapper.Loading = CreateGroupLoading;

export default CreateGroupWrapper;

+ 81
- 0
src/components/CreateGroup/GroupInfo.tsx Ver arquivo

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

+ 209
- 0
src/components/CreateGroup/UserAllocation.tsx Ver arquivo

@@ -0,0 +1,209 @@
"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).filter((u) => u.groupId !== null);
const [filteredUsers, setFilteredUsers] = useState(initialUsers);
const [selectedUsers, setSelectedUsers] = useState<typeof filteredUsers>(
() => {
return filteredUsers.filter(
(s) => getValues("addUserIds")?.includes(s.id)
);
}
);
// 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));
}, []);

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)
);
}, [selectedUsers, 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;

+ 1
- 0
src/components/CreateGroup/index.ts Ver arquivo

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

+ 1
- 1
src/components/CreateTeam/TeamInfo.tsx Ver arquivo

@@ -27,7 +27,7 @@ const TeamInfo: React.FC = (
setValue,
} = useFormContext<CreateTeamInputs>();

const resetCustomer = useCallback(() => {
const resetTeam = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("description");


+ 1
- 2
src/components/EditTeam/Allocation.tsx Ver arquivo

@@ -49,7 +49,7 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
reset,
resetField,
} = useFormContext<CreateTeamInputs>();
// let firstFilter: StaffResult[] = []

const initialStaffs = staff.map((s) => ({ ...s }));
@@ -63,7 +63,6 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
return rearrangedStaff.filter((s) => getValues("addStaffIds")?.includes(s.id))
}
);
console.log(filteredStaff.filter((s) => getValues("addStaffIds")?.includes(s.id)))
const [seletedTeamLead, setSeletedTeamLead] = useState<number>();
const [deletedStaffIds, setDeletedStaffIds] = useState<number[]>([]);



+ 1
- 0
src/components/NavigationContent/NavigationContent.tsx Ver arquivo

@@ -143,6 +143,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => {
{ icon: <Salary />, label: "Salary", path: "/settings/salary" },
{ icon: <Team />, label: "Team", path: "/settings/team" },
{ icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" },
{ icon: <ManageAccountsIcon />, label: "User Group", path: "/settings/group" },
{ icon: <Holiday />, label: "Holiday", path: "/settings/holiday" },
],
},


+ 94
- 0
src/components/UserGroupSearch/UserGroupSearch.tsx Ver arquivo

@@ -0,0 +1,94 @@
"use client";

import SearchBox, { Criterion } from "../SearchBox";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults/index";
import EditNote from "@mui/icons-material/EditNote";
import DeleteIcon from "@mui/icons-material/Delete";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { UserGroupResult } from "@/app/api/group";
import { deleteUser } from "@/app/api/user/actions";

interface Props {
users: UserGroupResult[];
}
type SearchQuery = Partial<Omit<UserGroupResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const UserGroupSearch: React.FC<Props> = ({ users }) => {
const { t } = useTranslation();
const [filteredUser, setFilteredUser] = useState(users);
const router = useRouter();

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("User Name"),
paramName: "name",
type: "text",
},
],
[t]
);

const onUserClick = useCallback(
(users: UserGroupResult) => {
console.log(users);
// router.push(`/settings/user/edit?id=${users.id}`)
},
[router, t]
);

const onDeleteClick = useCallback((users: UserGroupResult) => {
deleteDialog(async () => {
await deleteUser(users.id);

successDialog(t("Delete Success"), t);

setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id));
}, t);
}, []);

const columns = useMemo<Column<UserGroupResult>[]>(
() => [
{
name: "action",
label: t("Edit"),
onClick: onUserClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Group Name") },
{ name: "description", label: t("Description") },
{
name: "action",
label: t("Delete"),
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
color: "error"
},
],
[t]
);

return (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
// setFilteredUser(
// users.filter(
// (t) =>
// t.name.toLowerCase().includes(query.name.toLowerCase()) &&
// t.code.toLowerCase().includes(query.code.toLowerCase()) &&
// t.description.toLowerCase().includes(query.description.toLowerCase())
// )
// )
}}
/>
<SearchResults<UserGroupResult> items={filteredUser} columns={columns} />
</>
);
};
export default UserGroupSearch;

+ 40
- 0
src/components/UserGroupSearch/UserGroupSearchLoading.tsx Ver arquivo

@@ -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 UserGroupSearchLoading: 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>
<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 UserGroupSearchLoading;

+ 19
- 0
src/components/UserGroupSearch/UserGroupSearchWrapper.tsx Ver arquivo

@@ -0,0 +1,19 @@
import React from "react";
import UserGroupSearchLoading from "./UserGroupSearchLoading";
import { UserGroupResult, fetchGroup } from "@/app/api/group";
import UserGroupSearch from "./UserGroupSearch";

interface SubComponents {
Loading: typeof UserGroupSearchLoading;
}

const UserGroupSearchWrapper: React.FC & SubComponents = async () => {
const group = await fetchGroup()
console.log(group.records);

return <UserGroupSearch users={group.records} />;
};

UserGroupSearchWrapper.Loading = UserGroupSearchLoading;

export default UserGroupSearchWrapper;

+ 1
- 0
src/components/UserGroupSearch/index.ts Ver arquivo

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

Carregando…
Cancelar
Salvar