Просмотр исходного кода

update

tags/Baseline_30082024_FRONTEND_UAT
MSI\derek 1 год назад
Родитель
Сommit
796398474d
16 измененных файлов: 457 добавлений и 165 удалений
  1. +2
    -2
      src/app/(main)/settings/group/edit/page.tsx
  2. +4
    -10
      src/app/(main)/settings/staff/create/page.tsx
  3. +2
    -0
      src/app/api/positions/actions.ts
  4. +14
    -0
      src/app/api/user/actions.ts
  5. +3
    -0
      src/components/AppBar/Profile.tsx
  6. +14
    -108
      src/components/CreateStaff/CreateStaff.tsx
  7. +35
    -4
      src/components/CreateStaff/CreateStaffWrapper.tsx
  8. +1
    -1
      src/components/EditStaffForm/EditStaffForm.tsx
  9. +164
    -0
      src/components/EditUser/EditUser.tsx
  10. +40
    -0
      src/components/EditUser/EditUserLoading.tsx
  11. +23
    -0
      src/components/EditUser/EditUserWrapper.tsx
  12. +136
    -0
      src/components/EditUser/UserDetail.tsx
  13. +1
    -0
      src/components/EditUser/index.ts
  14. +0
    -3
      src/components/EditUserGroup/EditUserGroupWrapper.tsx
  15. +15
    -0
      src/components/StaffSearch/StaffSearch.tsx
  16. +3
    -37
      src/middleware.ts

+ 2
- 2
src/app/(main)/settings/group/edit/page.tsx Просмотреть файл

@@ -8,7 +8,7 @@ export const metadata: Metadata = {
title: "Edit User Group",
};

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

// Preload necessary dependencies
@@ -23,4 +23,4 @@ const Positions: React.FC = async () => {
);
};

export default Positions;
export default Group;

+ 4
- 10
src/app/(main)/settings/staff/create/page.tsx Просмотреть файл

@@ -22,7 +22,7 @@ import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Grid, Typography } from "@mui/material";
import CreateStaffForm from "@/components/CreateStaff/CreateStaff";
import CreateStaff from "@/components/CreateStaff";

interface CreateCustomInputs {
projectCode: string;
@@ -31,23 +31,17 @@ interface CreateCustomInputs {

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

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

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 Staff")}</Typography>
<I18nProvider namespaces={["staff"]}>
<CreateStaffForm
Title={title}
/>
<CreateStaff/>
</I18nProvider>
</>
);
};

export default CreateStaff;
export default CreateStaffPage;

+ 2
- 0
src/app/api/positions/actions.ts Просмотреть файл

@@ -15,6 +15,8 @@ export interface combo {
}

export interface CreatePositionInputs {
positionCode: string;
positionName: string;
code: string;
name: string;
description: string;


+ 14
- 0
src/app/api/user/actions.ts Просмотреть файл

@@ -11,6 +11,12 @@ export interface UserInputs {
email: string;
}

export interface PasswordInputs {
password: string;
newPassword: string;
newPasswordCheck: string;
}


export const fetchUserDetails = cache(async (id: number) => {
return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, {
@@ -31,4 +37,12 @@ export const deleteUser = async (id: number) => {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
};

export const changePassword = async (data: any) => {
return serverFetchJson(`${BASE_API_URL}/user/change-password`, {
method: "PATCH",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 3
- 0
src/components/AppBar/Profile.tsx Просмотреть файл

@@ -10,6 +10,7 @@ import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";

type Props = Pick<AppBarProps, "avatarImageSrc" | "profileName">;

@@ -26,6 +27,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => {
};

const { t } = useTranslation("login");
const router = useRouter();

return (
<>
@@ -52,6 +54,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => {
{profileName}
</Typography>
<Divider />
<MenuItem onClick={() => {router.replace("/settings/changepassword")}}>{t("Change Password")}</MenuItem>
<MenuItem onClick={() => signOut()}>{t("Sign out")}</MenuItem>
</Menu>
</>


+ 14
- 108
src/components/CreateStaff/CreateStaff.tsx Просмотреть файл

@@ -22,7 +22,6 @@ import { fetchSkillCombo } from "@/app/api/skill/actions";
import { fetchSalaryCombo } from "@/app/api/salarys/actions";

interface Field {
// subtitle: string;
id: string;
label: string;
type: string;
@@ -33,12 +32,6 @@ interface Field {
options?: any[];
readOnly?: boolean;
}

interface formProps {
Title?: string[];
// fieldLists: Field[][];
}

export interface comboItem {
company: comboProp[];
team: comboProp[];
@@ -49,101 +42,14 @@ export interface comboItem {
salary: comboProp[];
}

const CreateStaff: React.FC<formProps> = ({ Title }) => {
// const router = useRouter();
const { t } = useTranslation();
const [companyCombo, setCompanyCombo] = useState<comboProp[]>();
const [teamCombo, setTeamCombo] = useState<comboProp[]>();
const [departmentCombo, setDepartmentCombo] = useState<comboProp[]>();
const [positionCombo, setPositionCombo] = useState<comboProp[]>();
const [gradeCombo, setGradeCombo] = useState<comboProp[]>();
const [skillCombo, setSkillCombo] = useState<comboProp[]>();
const [salaryCombo, setSalaryCombo] = useState<comboProp[]>();
// const [serverError, setServerError] = useState("");

let comboItem: comboItem = {
company: [],
team: [],
department: [],
position: [],
grade: [],
skill: [],
salary: [],
};

const fetchCompany = async () => {
await fetchCompanyCombo().then((data) => {
if (data) setCompanyCombo(data.records);
});
}

const fetchTeam = async () => {
await fetchTeamCombo().then((data) => {
if (data) setTeamCombo(data.records);
});
}

const fetchDepartment = async () => {
await fetchDepartmentCombo().then((data) => {
if (data) setDepartmentCombo(data.records);
});
}

const fetchPosition = async () => {
await fetchPositionCombo().then((data) => {
if (data) setPositionCombo(data.records);
});
}

const fetchGrade = async () => {
await fetchGradeCombo().then((data) => {
if (data) setGradeCombo(data.records);
});
}

const fetchSkill = async () => {
await fetchSkillCombo().then((data) => {
if (data) setSkillCombo(data.records);
});
}

const fetchSalary = async () => {
await fetchSalaryCombo().then((data) => {
if (data) setSalaryCombo(data.records);
});
}

useEffect(() => {
fetchCompany()
fetchTeam()
fetchDepartment()
fetchPosition()
fetchGrade()
fetchSkill()
fetchSalary()
}, []);

useEffect(() => {
if(!companyCombo)
fetchCompany()
if(!teamCombo)
fetchTeam()
if(!departmentCombo)
fetchDepartment()
if(!positionCombo)
fetchPosition()
if(!gradeCombo)
fetchGrade()
if(!skillCombo)
fetchSkill()
if(!salaryCombo)
fetchSalary()
interface formProps {
Title?: string[];
combos: comboItem;
}

}, [companyCombo, teamCombo, departmentCombo, positionCombo, gradeCombo, skillCombo, salaryCombo]);

// useEffect(() => {
// console.log(companyCombo)
// }, [companyCombo]);
const CreateStaff: React.FC<formProps> = ({ Title, combos }) => {
const { t } = useTranslation();

const fieldLists: Field[][] = [
[
@@ -163,49 +69,49 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
id: "companyId",
label: t("Company"),
type: "combo-Obj",
options: companyCombo || [],
options: combos.company || [],
required: true,
},
{
id: "teamId",
label: t("Team"),
type: "combo-Obj",
options: teamCombo || [],
options: combos.team || [],
required: false,
},
{
id: "departmentId",
label: t("Department"),
type: "combo-Obj",
options: departmentCombo || [],
options: combos.department || [],
required: true,
},
{
id: "gradeId",
label: t("Grade"),
type: "combo-Obj",
options: gradeCombo || [],
options: combos.grade || [],
required: false,
},
{
id: "skillSetId",
label: t("Skillset"),
type: "multiSelect-Obj",
options: skillCombo || [],
options: combos.skill || [],
required: false,
},
{
id: "currentPositionId",
label: t("Current Position"),
type: "combo-Obj",
options: positionCombo || [],
options: combos.position || [],
required: true,
},
{
id: "salaryId",
label: t("Salary Point"),
type: "combo-Obj",
options: salaryCombo || [],
options: combos.salary || [],
required: true,
},
// {
@@ -279,7 +185,7 @@ const CreateStaff: React.FC<formProps> = ({ Title }) => {
id: "joinPositionId",
label: t("Join Position"),
type: "combo-Obj",
options: positionCombo || [],
options: combos.position || [],
required: true,
},
{


+ 35
- 4
src/components/CreateStaff/CreateStaffWrapper.tsx Просмотреть файл

@@ -1,17 +1,48 @@
import React from "react";
import CreateStaff from "./CreateStaff";
import CreateStaff, { comboItem } from "./CreateStaff";
import CreateStaffLoading from "./CreateStaffLoading";
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { useSearchParams } from "next/navigation";

import { fetchTeamCombo } from "@/app/api/team/actions";
import { fetchDepartmentCombo } from "@/app/api/departments/actions";
import { fetchPositionCombo } from "@/app/api/positions/actions";
import { fetchGradeCombo } from "@/app/api/grades/actions";
import { fetchSkillCombo } from "@/app/api/skill/actions";
import { fetchSalaryCombo } from "@/app/api/salarys/actions";
import { fetchCompanyCombo } from "@/app/api/companys/actions";
interface SubComponents {
Loading: typeof CreateStaffLoading;
}

const CreateStaffWrapper: React.FC & SubComponents = async () => {
const [
CompanyCombo,
TeamCombo,
DepartmentCombo,
PositionCombo,
GradeCombo,
SkillCombo,
SalaryCombo,
] = await Promise.all([
fetchCompanyCombo(),
fetchTeamCombo(),
fetchDepartmentCombo(),
fetchPositionCombo(),
fetchGradeCombo(),
fetchSkillCombo(),
fetchSalaryCombo(),
]);
const combos: comboItem = {
company: CompanyCombo.records,
team: TeamCombo.records,
department: DepartmentCombo.records,
position: PositionCombo.records,
grade: GradeCombo.records,
skill: SkillCombo.records,
salary: SalaryCombo.records,
}


return <CreateStaff/>;
return <CreateStaff combos={combos}/>;
};

CreateStaffWrapper.Loading = CreateStaffLoading;


+ 1
- 1
src/components/EditStaffForm/EditStaffForm.tsx Просмотреть файл

@@ -83,7 +83,7 @@ const EditStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => {
};
return (
<>
{serverError && (
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>


+ 164
- 0
src/components/EditUser/EditUser.tsx Просмотреть файл

@@ -0,0 +1,164 @@
"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,
Card,
CardContent,
Grid,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
import { StaffResult } from "@/app/api/staff";
import { editUser, fetchUserDetails } from "@/app/api/user/actions";
import UserDetail from "./UserDetail";
import { UserResult } from "@/app/api/user";

interface Props {
// users: UserResult[]
}

const EditUser: React.FC<Props> = async ({ }) => {
const { t } = useTranslation();
const formProps = useForm<UserResult>();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const [tabIndex, setTabIndex] = useState(0);
const router = useRouter();

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);

const [serverError, setServerError] = useState("");
const [data, setData] = useState<UserResult>();

const fetchUserDetail = async () => {
console.log(id);
try {
const userDetail = await fetchUserDetails(id);
console.log(userDetail);
const _data = userDetail.data as UserResult;
console.log(_data);
setData(_data);
formProps.reset({
username: _data.username,
firstname: _data.firstname,
lastname: _data.lastname,
title: _data.title,
department: _data.department,
email: _data.email,
phone1: _data.phone1,
phone2: _data.phone2,
remarks: _data.remarks,
});
} catch (error) {
console.log(error);
setServerError(t("An error has occurred. Please try again later."));
}
};

useEffect(() => {
fetchUserDetail();
}, []);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<UserResult>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

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

const onSubmit = useCallback<SubmitHandler<UserResult>>(
async (data) => {
try {
console.log(data);
const tempData = {
username: data.username,
email: data.email,
locked: false
}
console.log(tempData);
await editUser(id, tempData);
router.replace("/settings/staff");
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
}
},
[router]
);
const onSubmitError = useCallback<SubmitErrorHandler<UserResult>>(
(errors) => {
console.log(errors);
},
[]
);

return (
<>
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<UserDetail data={data!!} />
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
// onClick={() => console.log("asdasd")}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
</Button>
</Stack>
</Stack>
</FormProvider>
</>
);
};
export default EditUser;

+ 40
- 0
src/components/EditUser/EditUserLoading.tsx Просмотреть файл

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const EditUserLoading: React.FC = () => {
return (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>EditUser
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default EditUserLoading;

+ 23
- 0
src/components/EditUser/EditUserWrapper.tsx Просмотреть файл

@@ -0,0 +1,23 @@
import React from "react";
import EditUser from "./EditUser";
import EditUserLoading from "./EditUserLoading";
// import { fetchTeam, fetchTeamLeads } from "@/app/api/Team";
import { useSearchParams } from "next/navigation";
import { fetchTeam, fetchTeamDetail } from "@/app/api/team";
import { fetchStaff } from "@/app/api/staff";
import { fetchUser } from "@/app/api/user";

interface SubComponents {
Loading: typeof EditUserLoading;
}

const EditUserWrapper: React.FC & SubComponents = async () => {
// const users = await fetchUser()
// console.log(users)

return <EditUser />;
};

EditUserWrapper.Loading = EditUserLoading;

export default EditUserWrapper;

+ 136
- 0
src/components/EditUser/UserDetail.tsx Просмотреть файл

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

import { UserResult } from "@/app/api/user";
import {
Card,
CardContent,
Grid,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

interface Props {
data: UserResult
}


const UserDetail: React.FC<Props> = ({
data
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
control,
} = useFormContext<UserResult>();

return (
<Card>
<CardContent component={Stack} spacing={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("User Detail")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("username")}
fullWidth
{...register("username", {
required: "username required!",
})}
error={Boolean(errors.name)}
/>
</Grid>
{/* <Grid item xs={6}>
<TextField
label={t("First Name")}
fullWidth
{...register("firstname", {
required: "Name required!",
})}
error={Boolean(errors.firstname)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Last Name")}
fullWidth
{...register("lastname", {
required: "Name required!",
})}
error={Boolean(errors.lastname)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("title")}
fullWidth
{...register("title", {
required: "title required!",
})}
error={Boolean(errors.title)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("department")}
fullWidth
{...register("department", {
required: "department required!",
})}
error={Boolean(errors.department)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("email")}
fullWidth
{...register("email", {
required: "email required!",
})}
error={Boolean(errors.email)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("phone1")}
fullWidth
{...register("phone1", {
required: "phone1 required!",
})}
error={Boolean(errors.phone1)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("phone2")}
fullWidth
{...register("phone2", {
required: "phone2 required!",
})}
error={Boolean(errors.phone2)}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("remarks")}
fullWidth
multiline
rows={4}
variant="filled"
{...register("remarks", {
required: "remarks required!",
})}
error={Boolean(errors.remarks)}
/>
</Grid> */}
</Grid>
</CardContent>
</Card>
);
};

export default UserDetail;

+ 1
- 0
src/components/EditUser/index.ts Просмотреть файл

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

+ 0
- 3
src/components/EditUserGroup/EditUserGroupWrapper.tsx Просмотреть файл

@@ -14,14 +14,11 @@ 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}/>;
};


+ 15
- 0
src/components/StaffSearch/StaffSearch.tsx Просмотреть файл

@@ -9,6 +9,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
import { deleteStaff } from "@/app/api/staff/actions";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import Person from '@mui/icons-material/Person';

interface Props {
staff: StaffResult[];
@@ -65,6 +66,14 @@ const StaffSearch: React.FC<Props> = ({ staff }) => {
[router, t]
);

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

const deleteClick = useCallback((staff: StaffResult) => {
deleteDialog(async () => {
await deleteStaff(staff.id);
@@ -81,6 +90,12 @@ const StaffSearch: React.FC<Props> = ({ staff }) => {
onClick: onStaffClick,
buttonIcon: <EditNote />,
},
{
name: "id",
label: t("Actions"),
onClick: onUserClick,
buttonIcon: <Person />,
},
{ name: "team", label: t("Team") },
{ name: "name", label: t("Staff Name") },
{ name: "staffId", label: t("Staff ID") },


+ 3
- 37
src/middleware.ts Просмотреть файл

@@ -71,9 +71,6 @@ export default async function middleware(
return response;
}
// const session = await getServerSession(authOptions);
// console.log(session);

let abilities: string[] = []
if (token) {
abilities = (token.abilities as ability[]).map((item: ability) => item.actionSubjectCombo);
@@ -93,6 +90,9 @@ export default async function middleware(
if (req.nextUrl.pathname.startsWith('/settings/user')) {
isAuth = [MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability));
}
if (req.nextUrl.pathname.startsWith('/settings/staff/user')) {
isAuth = [MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability));
}
if (req.nextUrl.pathname.startsWith('/analytics')) {
isAuth = [GENERATE_REPORTS].some((ability) => abilities.includes(ability));
}
@@ -104,40 +104,6 @@ export default async function middleware(
}
});


// for (const obj of abilities) {
// switch (obj.actionSubjectCombo.toLowerCase()) {
// case "maintain_user":
// // appendRoutes(settings)
// break;
// case "maintain_group":
// // appendRoutes("/testing-maintain_user")
// break;
// case "view_user":
// // appendRoutes("/testing-maintain_user")
// break;
// case "view_group":
// // appendRoutes("/testing-maintain_user")
// break;
// }
// }

// console.log("TESTING_ROUTES: ")
// console.log(TESTING_ROUTES)

// TESTING_ROUTES.some((route) => {
// if (req.nextUrl.pathname.startsWith(route)) {
// console.log("////////////////start//////////////// ")
// console.log("TESTING_ROUTES:")
// console.log("route:")
// console.log(route)
// console.log("pathname:")
// console.log(req.nextUrl.pathname)
// console.log("////////////////end////////////////")
// }
// return (req.nextUrl.pathname.startsWith(route))
// })

// Matcher for using the auth middleware
return PRIVATE_ROUTES.some((route) => req.nextUrl.pathname.startsWith(route))
? await authMiddleware(req, event) // Let auth middleware handle response


Загрузка…
Отмена
Сохранить