| @@ -1,5 +1,4 @@ | |||
| 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"; | |||
| @@ -8,10 +7,7 @@ import { Metadata } from "next"; | |||
| import EditSkill from "@/components/EditSkill"; | |||
| import { Typography } from "@mui/material"; | |||
| import { fetchSkill } from "@/app/api/skill"; | |||
| export interface searchParamsProps { | |||
| searchParams: { [key: string]: string | string[] | undefined }; | |||
| } | |||
| import { searchParamsProps } from "@/app/utils/fetchUtil"; | |||
| const EditSkillPage: React.FC<searchParamsProps> = async ({ | |||
| searchParams, | |||
| @@ -6,12 +6,15 @@ import { Suspense } from "react"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | |||
| import { Metadata } from "next"; | |||
| import { searchParamsProps } from "@/app/utils/fetchUtil"; | |||
| // export const metadata: Metadata = { | |||
| // title: "staff-edit", | |||
| // }; | |||
| const EditStaffPage: React.FC = () => { | |||
| const EditStaffPage: React.FC<searchParamsProps> = async ({ | |||
| searchParams, | |||
| }) => { | |||
| // const searchParams = useSearchParams(); | |||
| // const userId = searchParams.get('param'); | |||
| // console.log(userId); // Access the value of the "user_id" parameter | |||
| @@ -20,7 +23,7 @@ const EditStaffPage: React.FC = () => { | |||
| <> | |||
| <I18nProvider namespaces={["staff", "common"]}> | |||
| <Suspense fallback={<EditStaff.Loading />}> | |||
| <EditStaff /> | |||
| <EditStaff id={parseInt(searchParams.id as string)}/> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| {/* <EditStaff /> */} | |||
| @@ -26,6 +26,35 @@ export interface StaffTeamTable { | |||
| currentPosition: string; | |||
| } | |||
| export type IndivStaff = { | |||
| data: IndividualStaff | |||
| } | |||
| export type IndividualStaff = { | |||
| id: number | |||
| staffId: string | |||
| name: string | |||
| company: data | |||
| team: data | |||
| department: data | |||
| grade: data | |||
| skill: data | |||
| skillset: any | |||
| currentPosition: data | |||
| salary: data | |||
| employType: string | |||
| email: string | |||
| phone1: string | |||
| phone2?: string | |||
| emergContactName: string; | |||
| emergContactPhone: string; | |||
| joinDate: string; | |||
| joinPosition: data; | |||
| departDate?: string; | |||
| departReason?: string; | |||
| remark?: string; | |||
| } | |||
| export interface StaffResult { | |||
| action: any; | |||
| id: number; | |||
| @@ -35,10 +64,11 @@ export interface StaffResult { | |||
| grade: string; | |||
| joinPosition: string; | |||
| currentPosition: string; | |||
| data: data; | |||
| teamId: number; | |||
| staffName: string; | |||
| userId: number; | |||
| companyId: number; | |||
| data: data; | |||
| } | |||
| export interface searchInput { | |||
| staffId: string; | |||
| @@ -68,6 +98,12 @@ export const fetchStaff = cache(async () => { | |||
| }); | |||
| }); | |||
| export const fetchIndivStaff = cache(async (id: number) => { | |||
| return serverFetchJson<IndivStaff>(`${BASE_API_URL}/staffs/${id}`, { | |||
| next: { tags: ["staffs"] }, | |||
| }); | |||
| }); | |||
| export const fetchStaffWithoutTeam = cache(async () => { | |||
| return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs/noteam`, { | |||
| next: { tags: ["staffs"] }, | |||
| @@ -3,6 +3,10 @@ import { getServerSession } from "next-auth"; | |||
| import { headers } from "next/headers"; | |||
| import { redirect } from "next/navigation"; | |||
| export interface searchParamsProps { | |||
| searchParams: { [key: string]: string | string[] | undefined }; | |||
| } | |||
| export class ServerFetchError extends Error { | |||
| public readonly response: Response | undefined; | |||
| constructor(message?: string, response?: Response) { | |||
| @@ -55,6 +55,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| (acc, skill) => ({ ...acc, [skill.id]: skill.label }), | |||
| {} | |||
| ); | |||
| console.log(skillIdNameMap) | |||
| const resetStaff = useCallback(() => { | |||
| console.log(defaultValues); | |||
| @@ -235,6 +236,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| > | |||
| <Checkbox | |||
| checked={field.value!.indexOf(skill.id) > -1} | |||
| // checked={true} | |||
| /> | |||
| <ListItemText primary={skill.label} /> | |||
| </MenuItem> | |||
| @@ -1,11 +1,18 @@ | |||
| "use client"; | |||
| import EditStaffForm from "../EditStaffForm"; | |||
| import { useSearchParams } from "next/navigation"; | |||
| import { useEffect, useState } from "react"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { fetchStaffEdit } from "@/app/api/staff/actions"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import CustomInputForm from "../CustomInputForm"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| FieldErrors, | |||
| FormProvider, | |||
| SubmitErrorHandler, | |||
| SubmitHandler, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; | |||
| import { Button, Stack, Typography } from "@mui/material"; | |||
| // import CreateStaffForm from "../CreateStaffForm"; | |||
| import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; | |||
| import { fetchTeamCombo } from "@/app/api/team/actions"; | |||
| import { fetchDepartmentCombo } from "@/app/api/departments/actions"; | |||
| @@ -13,346 +20,234 @@ 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 { Field } from "react-hook-form"; | |||
| // import StaffInfo from "./StaffInfo"; | |||
| import { Check, Close } from "@mui/icons-material"; | |||
| import { ServerFetchError } from "@/app/utils/fetchUtil"; | |||
| import StaffInfo from "./StaffInfo"; | |||
| import { IndividualStaff } from "@/app/api/staff"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { List, differenceBy } from "lodash"; | |||
| interface skill { | |||
| id: number; | |||
| name: string; | |||
| code: string; | |||
| } | |||
| interface skillObj { | |||
| id: number; | |||
| skill: skill; | |||
| export interface comboItem { | |||
| company: comboProp[]; | |||
| team: comboProp[]; | |||
| department: comboProp[]; | |||
| position: comboProp[]; | |||
| grade: comboProp[]; | |||
| skill: comboProp[]; | |||
| salary: comboProp[]; | |||
| } | |||
| interface Options { | |||
| id: any; | |||
| label: string; | |||
| [key: string]: any; | |||
| interface formProps { | |||
| Staff: IndividualStaff | |||
| combos: comboItem; | |||
| } | |||
| // interface Field { | |||
| // id: string; | |||
| // label: string; | |||
| // type: string; | |||
| // value: any; | |||
| // required?: boolean; | |||
| // options?: comboProp[] | undefined | null; | |||
| // readOnly?: boolean; | |||
| // } | |||
| export interface Field { | |||
| // subtitle: string; | |||
| id: string; | |||
| label: string; | |||
| type: string; | |||
| value?: any; | |||
| required?: boolean; | |||
| pattern?: string; | |||
| message?: string; | |||
| options?: Options[] | null; | |||
| readOnly?: boolean; | |||
| size?: number; | |||
| setValue?: any[]; | |||
| } | |||
| const EditStaff: React.FC = async () => { | |||
| const searchParams = useSearchParams(); | |||
| const EditStaff: React.FC<formProps> = ({ Staff, combos }) => { | |||
| const defaultSkillset = Staff.skillset.map((s: any) => s.skill.id) | |||
| const { t } = useTranslation(); | |||
| const idString = searchParams.get("id"); | |||
| const [fieldLists, setFieldLists] = useState<Field[][]>(); | |||
| 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 employTypeCombo = [{id: "FT", label: t("FT")}, {id: "PT", label: t("PT")}]; | |||
| const title = ["", t('Additional Info')] | |||
| const employTypeCombo = [ | |||
| { id: "FT", label: t("FT") }, | |||
| { id: "PT", label: t("PT") }, | |||
| ]; | |||
| const keyOrder1 = [ | |||
| "staffId", | |||
| "name", | |||
| "company", | |||
| "team", | |||
| "department", | |||
| "grade", | |||
| "skill", | |||
| "currentPosition", | |||
| "salary", | |||
| "hourlyRate", | |||
| "employType", | |||
| "email", | |||
| "phone1", | |||
| "phone2", | |||
| ]; | |||
| const searchParams = useSearchParams() | |||
| const id = parseInt(searchParams.get("id") || "0"); | |||
| const formProps = useForm<CreateStaffInputs>({ | |||
| defaultValues: { | |||
| staffId: Staff.staffId, | |||
| name: Staff.name, | |||
| companyId: Staff.company.id, | |||
| teamId: Staff.team?.id, | |||
| departmentId: Staff.department.id, | |||
| gradeId: Staff.department.id, | |||
| skillSetId: defaultSkillset, | |||
| // removeSkillSetId: [], | |||
| currentPositionId: Staff.currentPosition.id, | |||
| salaryId: Staff.salary.id, | |||
| employType: Staff.employType, | |||
| email: Staff.email, | |||
| phone1: Staff.phone1, | |||
| phone2: Staff.phone2, | |||
| emergContactName: Staff.emergContactName, | |||
| emergContactPhone: Staff.emergContactPhone, | |||
| joinDate: dayjs(Staff.joinDate).toString() || "", | |||
| joinPositionId: Staff.joinPosition.id, | |||
| departDate: dayjs(Staff.departDate).toString() || "", | |||
| departReason: Staff.departReason, | |||
| remark: Staff.remark, | |||
| }}); | |||
| const [serverError, setServerError] = useState(""); | |||
| const router = useRouter(); | |||
| // const [tabIndex, setTabIndex] = useState(0); | |||
| const keyOrder2 = [ | |||
| "emergContactName", | |||
| "emergContactPhone", | |||
| "joinDate", | |||
| "joinPosition", | |||
| "departDate", | |||
| "departPosition", | |||
| "departReason", | |||
| "remark", | |||
| ]; | |||
| const errors = formProps.formState.errors; | |||
| //fetch all combo | |||
| useEffect(() => { | |||
| fetchCompanyCombo().then((data) => { | |||
| if (data) setCompanyCombo(data.records); | |||
| }); | |||
| fetchTeamCombo().then((data) => { | |||
| if (data) setTeamCombo(data.records); | |||
| }); | |||
| fetchDepartmentCombo().then((data) => { | |||
| if (data) setDepartmentCombo(data.records); | |||
| }); | |||
| fetchPositionCombo().then((data) => { | |||
| if (data) setPositionCombo(data.records); | |||
| }); | |||
| fetchGradeCombo().then((data) => { | |||
| if (data) setGradeCombo(data.records); | |||
| }); | |||
| fetchSkillCombo().then((data) => { | |||
| if (data) { | |||
| }setSkillCombo(data.records); | |||
| console.log(data.records) | |||
| }); | |||
| fetchSalaryCombo().then((data) => { | |||
| if (data) setSalaryCombo(data.records); | |||
| }); | |||
| }, [searchParams]); | |||
| const checkDuplicates = (str1: string, str2: string, str3: string) => { | |||
| return str1 === str2 || str1 === str3 || str2 === str3; | |||
| } | |||
| const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | |||
| async (data) => { | |||
| try { | |||
| console.log(data); | |||
| let haveError = false; | |||
| let regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ | |||
| let regex_phone = /^\d{8}$/ | |||
| // let removeSkillSetId: List<number> = [] | |||
| // if (data.skillSetId && defaultSkillset.length > data.skillSetId) { | |||
| // removeSkillSetId = differenceBy(defaultSkillset, data.skillSetId) | |||
| // } | |||
| console.log(data.skillSetId) | |||
| console.log(defaultSkillset) | |||
| console.log(differenceBy(data.skillSetId, defaultSkillset)) | |||
| if (!regex_email.test(data.email)) { | |||
| haveError = true | |||
| formProps.setError("email", { message: t("Please Enter Correct Email."), type: "required" }) | |||
| } | |||
| if(!regex_phone.test(data.phone1)) { | |||
| haveError = true | |||
| formProps.setError("phone1", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| if(!regex_phone.test(data.emergContactPhone)) { | |||
| haveError = true | |||
| formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| if (data.phone2 && data.phone2?.length > 0) { | |||
| if(!regex_phone.test(data.phone2)) { | |||
| haveError = true | |||
| formProps.setError("phone2", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| } | |||
| if (data.phone1 === data.phone2 || data.phone1 === data.emergContactPhone || data.phone2 === data.emergContactPhone) { | |||
| haveError = true | |||
| formProps.setError("phone1", { message: t("Please Enter Different Phone No.."), type: "required" }) | |||
| if (data.phone2!.length > 0) { | |||
| formProps.setError("phone2", { message: t("Please Enter Different Phone No.."), type: "required" }) | |||
| } | |||
| formProps.setError("emergContactPhone", { message: t("Please Enter Different Phone No.."), type: "required" }) | |||
| } | |||
| if (!regex_email.test(data.email)) { | |||
| haveError = true | |||
| formProps.setError("email", { message: t("Please Enter Correct Email."), type: "required" }) | |||
| } | |||
| if (!data.companyId) { | |||
| haveError = true | |||
| formProps.setError("companyId", { message: t("Please Enter Company."), type: "required" }) | |||
| } | |||
| if (!data.employType) { | |||
| haveError = true | |||
| formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | |||
| } | |||
| if (!data.departmentId) { | |||
| haveError = true | |||
| formProps.setError("departmentId", { message: t("Please Enter Department."), type: "required" }) | |||
| } | |||
| if (!data.salaryId) { | |||
| haveError = true | |||
| formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) | |||
| } | |||
| if (!data.joinDate) { | |||
| haveError = true | |||
| formProps.setError("joinDate", { message: t("Please Enter Join Date."), type: "required" }) | |||
| } | |||
| if (data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
| haveError = true | |||
| formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
| } | |||
| if (haveError) { | |||
| return | |||
| } | |||
| console.log("passed") | |||
| const postData = { | |||
| id: id, | |||
| ...data, | |||
| // removeSkillSetId: removeSkillSetId | |||
| } | |||
| await saveStaff(postData) | |||
| router.replace("/settings/staff") | |||
| } catch (e: any) { | |||
| console.log(e); | |||
| formProps.setError("staffId", { message: t("Please Enter Employ Type."), type: "required" }) | |||
| let msg = "" | |||
| if (e.message === "Duplicated StaffId Found") { | |||
| msg = t("Duplicated StaffId Found") | |||
| } | |||
| setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `); | |||
| } | |||
| }, | |||
| [router] | |||
| ); | |||
| const handleCancel = () => { | |||
| router.back(); | |||
| }; | |||
| const resetStaff = useCallback(() => { | |||
| formProps.reset({ | |||
| staffId: Staff.staffId, | |||
| name: Staff.name, | |||
| companyId: Staff.company.id, | |||
| teamId: Staff.team?.id, | |||
| departmentId: Staff.department.id, | |||
| gradeId: Staff.department.id, | |||
| skillSetId: defaultSkillset, | |||
| // removeSkillSetId: [], | |||
| currentPositionId: Staff.currentPosition.id, | |||
| salaryId: Staff.salary.id, | |||
| employType: Staff.employType, | |||
| email: Staff.email, | |||
| phone1: Staff.phone1, | |||
| phone2: Staff.phone2, | |||
| emergContactName: Staff.emergContactName, | |||
| emergContactPhone: Staff.emergContactPhone, | |||
| joinDate: dayjs(Staff.joinDate).format(INPUT_DATE_FORMAT) || "", | |||
| joinPositionId: Staff.joinPosition.id, | |||
| departDate: !Staff.departDate ? "" : dayjs(Staff.departDate).format(INPUT_DATE_FORMAT), | |||
| departReason: Staff.departReason, | |||
| remark: Staff.remark, | |||
| }); | |||
| }, []); | |||
| useEffect(() => { | |||
| let id = 0; | |||
| if (idString) { | |||
| id = parseInt(idString); | |||
| console.log(id) | |||
| fetchStaffEdit(id).then((staff) => { | |||
| console.log(staff.data); | |||
| const skillset = staff.data.skillset | |||
| console.log(skillset); | |||
| const skillIds = skillset.map((item: skillObj) => item.skill.id); | |||
| console.log(skillIds) | |||
| const data = staff.data; | |||
| ///////////////////// list 1 ///////////////////// | |||
| const list1 = keyOrder1 | |||
| .map((key) => { | |||
| switch (key) { | |||
| case "staffId": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Staff ID`), | |||
| type: "text", | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| }; | |||
| case "name": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Staff Name`), | |||
| type: "text", | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| }; | |||
| case "company": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Company`), | |||
| type: "combo-Obj", | |||
| options: companyCombo, | |||
| value: data[key].id ?? "", | |||
| required: true, | |||
| }; | |||
| case "team": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Team`), | |||
| type: "combo-Obj", | |||
| options: teamCombo, | |||
| value: data[key]?.id ?? "", | |||
| }; | |||
| case "department": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Department`), | |||
| type: "combo-Obj", | |||
| options: departmentCombo, | |||
| value: data[key]?.id ?? "", | |||
| required: true, | |||
| // later check | |||
| }; | |||
| case "grade": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Grade`), | |||
| type: "combo-Obj", | |||
| options: gradeCombo, | |||
| value: data[key]?.id ?? "", | |||
| }; | |||
| case "skill": | |||
| console.log(skillIds) | |||
| return { | |||
| id: `${key}SetId`, | |||
| label: t(`Skillset`), | |||
| type: "multiSelect-Obj", | |||
| options: skillCombo, | |||
| value: skillIds ?? [], | |||
| //array problem | |||
| }; | |||
| case "currentPosition": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Current Position`), | |||
| type: "combo-Obj", | |||
| options: positionCombo, | |||
| value: data[key].id ?? "", | |||
| required: true, | |||
| }; | |||
| case "salary": | |||
| // console.log("salary", data[key]) | |||
| return { | |||
| id: `salaryId`, | |||
| label: t(`Salary Point`), | |||
| type: "combo-Obj", | |||
| options: salaryCombo, | |||
| value: data[key]?.salaryPoint ?? "", | |||
| required: true, | |||
| }; | |||
| // case "hourlyRate": | |||
| // return { | |||
| // id: `${key}`, | |||
| // label: t(`hourlyRate`), | |||
| // type: "text", | |||
| // value: "", | |||
| // // value: data[key], | |||
| // readOnly: true, | |||
| // }; | |||
| case "employType": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Employ Type`), | |||
| type: "combo-Obj", | |||
| options: employTypeCombo, | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| }; | |||
| case "email": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Email`), | |||
| type: "text", | |||
| value: data[key] ?? "", | |||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
| message: t("input matching format"), | |||
| required: true, | |||
| }; | |||
| case "phone1": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Phone1`), | |||
| type: "text", | |||
| // pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| }; | |||
| case "phone2": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Phone2`), | |||
| type: "text", | |||
| // pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| value: data[key] ?? "", | |||
| } as Field; | |||
| default: | |||
| return null; | |||
| } | |||
| }).filter((item): item is Field => item !== null); | |||
| ///////////////////// list 2 ///////////////////// | |||
| const list2 = keyOrder2 | |||
| .map((key) => { | |||
| switch (key) { | |||
| case "emergContactName": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Emergency Contact Name`), | |||
| type: "text", | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| } as Field; | |||
| case "emergContactPhone": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Emergency Contact Phone`), | |||
| type: "text", | |||
| // pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| } as Field; | |||
| case "joinDate": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Join Date`), | |||
| type: "multiDate", | |||
| value: data[key] ?? "", | |||
| required: true, | |||
| } as Field; | |||
| case "joinPosition": | |||
| return { | |||
| id: `${key}Id`, | |||
| label: t(`Join Position`), | |||
| type: "combo-Obj", | |||
| options: positionCombo, | |||
| value: data[key]?.id ?? "", | |||
| required: true, | |||
| } as Field; | |||
| case "departDate": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Depart Date`), | |||
| type: "multiDate", | |||
| value: data[key] ?? "", | |||
| } as Field; | |||
| case "departReason": | |||
| return { | |||
| id: `${key}`, | |||
| label: t(`Depart Reason`), | |||
| type: "text", | |||
| value: data[key] ?? "", | |||
| } as Field; | |||
| case "remark": | |||
| return { | |||
| id: `remark`, | |||
| label: t(`Remark`), | |||
| type: "remarks", | |||
| value: data[key] ?? "", | |||
| } as Field; | |||
| default: | |||
| return null; | |||
| } | |||
| }).filter((item): item is Field => item !== null); | |||
| console.log(list2); | |||
| console.log([list1]); | |||
| setFieldLists([list1,list2]); | |||
| }); | |||
| } | |||
| }, [companyCombo, teamCombo, departmentCombo, positionCombo, gradeCombo, skillCombo, salaryCombo, idString]); | |||
| console.log(Staff) | |||
| resetStaff() | |||
| }, [Staff, combos]); | |||
| return ( | |||
| <> | |||
| <EditStaffForm Title={title} fieldLists={fieldLists as Field[][] || [[]]} /> | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| {Staff && <StaffInfo combos={combos}/>} | |||
| <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 EditStaff; | |||
| export default EditStaff; | |||
| @@ -1,17 +1,60 @@ | |||
| import React from "react"; | |||
| import EditStaff from "./EditStaff"; | |||
| import EditStaff, { comboItem } from "./EditStaff"; | |||
| import EditStaffLoading from "./EditStaffLoading"; | |||
| import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | |||
| import { StaffResult, fetchIndivStaff, fetchStaff, fetchTeamLeads, preloadStaff } 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 EditStaffLoading; | |||
| } | |||
| const EditStaffWrapper: React.FC & SubComponents = async () => { | |||
| interface Props { | |||
| id: number | |||
| } | |||
| const EditStaffWrapper: React.FC<Props> & SubComponents = async ({ | |||
| id | |||
| }) => { | |||
| preloadStaff() | |||
| const [ | |||
| Staff, | |||
| CompanyCombo, | |||
| TeamCombo, | |||
| DepartmentCombo, | |||
| PositionCombo, | |||
| GradeCombo, | |||
| SkillCombo, | |||
| SalaryCombo, | |||
| ] = await Promise.all([ | |||
| fetchIndivStaff(id), | |||
| 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 <EditStaff/>; | |||
| console.log(Staff.data) | |||
| return <EditStaff Staff={Staff.data} combos={combos}/>; | |||
| }; | |||
| EditStaffWrapper.Loading = EditStaffLoading; | |||
| @@ -0,0 +1,514 @@ | |||
| "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 { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useCallback, useEffect } from "react"; | |||
| import { CreateStaffInputs } from "@/app/api/staff/actions"; | |||
| import { | |||
| Checkbox, | |||
| FormControl, | |||
| InputLabel, | |||
| ListItemText, | |||
| MenuItem, | |||
| Select, | |||
| } from "@mui/material"; | |||
| import { comboItem } from "./EditStaff"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| interface Props { | |||
| combos: comboItem; | |||
| } | |||
| const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues }, | |||
| control, | |||
| reset, | |||
| resetField, | |||
| setValue, | |||
| getValues, | |||
| clearErrors, | |||
| } = useFormContext<CreateStaffInputs>(); | |||
| const employType = [ | |||
| { id: 1, label: "FT" }, | |||
| { id: 2, label: "PT" }, | |||
| ]; | |||
| const skillIdNameMap = combos.skill.reduce<{ [id: number]: string }>( | |||
| (acc, skill) => ({ ...acc, [skill.id]: skill.label }), | |||
| {} | |||
| ); | |||
| const resetStaff = useCallback(() => { | |||
| console.log(defaultValues); | |||
| if (defaultValues !== undefined) { | |||
| // resetField("description"); | |||
| } | |||
| }, [defaultValues]); | |||
| const joinDate = getValues("joinDate"); | |||
| const departDate = getValues("departDate"); | |||
| useEffect(() => { | |||
| if (joinDate) clearErrors("joinDate"); | |||
| if (departDate) clearErrors("departDate"); | |||
| }, [joinDate, departDate]); | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Staff")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Staff ID")} | |||
| fullWidth | |||
| required | |||
| {...register("staffId", { | |||
| required: "Staff Id required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| helperText={ | |||
| Boolean(errors.name) && | |||
| (errors.name?.message | |||
| ? t(errors.name.message) | |||
| : t("Please input correct staffId")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Staff Name")} | |||
| fullWidth | |||
| required | |||
| {...register("name", { | |||
| required: "Staff Name required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| helperText={ | |||
| Boolean(errors.name) && | |||
| (errors.name?.message | |||
| ? t(errors.name.message) | |||
| : t("Please input correct name")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Company")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="companyId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Company")} | |||
| {...field} | |||
| error={Boolean(errors.companyId)} | |||
| > | |||
| {combos.company.map((company, index) => ( | |||
| <MenuItem | |||
| key={`${company.id}-${index}`} | |||
| value={company.id} | |||
| > | |||
| {t(company.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Team")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="teamId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Team")} | |||
| {...field} | |||
| // error={Boolean(errors.teamId)} | |||
| > | |||
| {combos.team.map((team, index) => ( | |||
| <MenuItem key={`${team.id}-${index}`} value={team.id}> | |||
| {t(team.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Department")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="departmentId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Department")} | |||
| {...field} | |||
| error={Boolean(errors.departmentId)} | |||
| > | |||
| {combos.department.map((department, index) => ( | |||
| <MenuItem | |||
| key={`${department.id}-${index}`} | |||
| value={department.id} | |||
| > | |||
| {t(department.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Grade")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="gradeId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Grade")} | |||
| {...field} | |||
| error={Boolean(errors.gradeId)} | |||
| > | |||
| {combos.grade.map((grade, index) => ( | |||
| <MenuItem key={`${grade.id}-${index}`} value={grade.id}> | |||
| {t(grade.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Skillset")}</InputLabel> | |||
| <Controller | |||
| defaultValue={[]} | |||
| control={control} | |||
| name="skillSetId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| // error={Boolean(errors.skillSetId)} | |||
| renderValue={(types) => | |||
| types.map((type) => skillIdNameMap[type]).join(", ") | |||
| } | |||
| multiple | |||
| label={t("Skillset")} | |||
| {...field} | |||
| > | |||
| {combos.skill.map((skill, index) => { | |||
| // console.log(field) | |||
| return ( | |||
| <MenuItem | |||
| key={`${skill.id}-${index}`} | |||
| value={skill.id} | |||
| > | |||
| <Checkbox | |||
| checked={field.value!.indexOf(skill.id) > -1} | |||
| /> | |||
| <ListItemText primary={skill.label} /> | |||
| </MenuItem> | |||
| ); | |||
| })} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Current Position")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="currentPositionId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Current Position")} | |||
| {...field} | |||
| error={Boolean(errors.currentPositionId)} | |||
| > | |||
| {combos.position.map((position, index) => ( | |||
| <MenuItem | |||
| key={`${position.id}-${index}`} | |||
| value={position.id} | |||
| > | |||
| {t(position.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Salary Point")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="salaryId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Salary Point")} | |||
| {...field} | |||
| error={Boolean(errors.salaryId)} | |||
| > | |||
| {combos.salary.map((salary, index) => ( | |||
| <MenuItem | |||
| key={`${salary.id}-${index}`} | |||
| value={salary.id} | |||
| > | |||
| {t(salary.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Employ Type")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="employType" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Employ Type")} | |||
| {...field} | |||
| error={Boolean(errors.employType)} | |||
| > | |||
| {employType.map((type, index) => ( | |||
| <MenuItem | |||
| key={`${type.id}-${index}`} | |||
| value={type.label} | |||
| > | |||
| {t(type.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Email")} | |||
| fullWidth | |||
| required | |||
| {...register("email", { | |||
| required: "Email required!", | |||
| })} | |||
| error={Boolean(errors.email)} | |||
| helperText={ | |||
| Boolean(errors.email) && | |||
| (errors.email?.message | |||
| ? t(errors.email.message) | |||
| : t("Please input correct email")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Phone1")} | |||
| fullWidth | |||
| required | |||
| {...register("phone1", { | |||
| required: "phone1 required!", | |||
| })} | |||
| error={Boolean(errors.phone1)} | |||
| helperText={ | |||
| Boolean(errors.phone1) && | |||
| (errors.phone1?.message | |||
| ? t(errors.phone1.message) | |||
| : t("Please input correct phone1")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Phone2")} | |||
| fullWidth | |||
| {...register("phone2")} | |||
| error={Boolean(errors.phone2)} | |||
| helperText={ | |||
| Boolean(errors.phone2) && | |||
| (errors.phone2?.message | |||
| ? t(errors.phone2.message) | |||
| : t("Please input correct phone2")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }} marginTop={3}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Emergency Contact Name")} | |||
| fullWidth | |||
| required | |||
| {...register("emergContactName", { | |||
| required: "Emergency Contact Name required!", | |||
| })} | |||
| error={Boolean(errors.emergContactName)} | |||
| helperText={ | |||
| Boolean(errors.emergContactName) && | |||
| (errors.emergContactName?.message | |||
| ? t(errors.emergContactName.message) | |||
| : t("Please input correct Emergency Contact Name")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Emergency Contact Phone")} | |||
| fullWidth | |||
| required | |||
| {...register("emergContactPhone", { | |||
| required: "Emergency Contact Phone required!", | |||
| })} | |||
| error={Boolean(errors.emergContactPhone)} | |||
| helperText={ | |||
| Boolean(errors.emergContactPhone) && | |||
| (errors.emergContactPhone?.message | |||
| ? t(errors.emergContactPhone.message) | |||
| : t("Please input correct Emergency Contact Phone")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| sx={{ width: "100%" }} | |||
| label={t("Join Date")} | |||
| value={joinDate ? dayjs(joinDate) : null} | |||
| onChange={(date) => { | |||
| if (!date) return; | |||
| setValue("joinDate", date.format(INPUT_DATE_FORMAT)); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| required: true, | |||
| error: | |||
| joinDate === "Invalid Date" || Boolean(errors.joinDate), | |||
| // value: errors.joinDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Join Position")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="joinPositionId" | |||
| render={({ field }) => ( | |||
| <Select | |||
| label={t("Join Position")} | |||
| {...field} | |||
| error={Boolean(errors.joinPositionId)} | |||
| > | |||
| {combos.position.map((position, index) => ( | |||
| <MenuItem | |||
| key={`${position.id}-${index}`} | |||
| value={position.id} | |||
| > | |||
| {t(position.label)} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| sx={{ width: "100%" }} | |||
| label={t("Depart Date")} | |||
| value={departDate ? dayjs(departDate) : null} | |||
| onChange={(date) => { | |||
| if (!date) return; | |||
| setValue("departDate", date.format(INPUT_DATE_FORMAT)); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| error: departDate | |||
| ? new Date(joinDate) > new Date(departDate) | |||
| : false, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Depart Reason")} | |||
| fullWidth | |||
| {...register("departReason")} | |||
| error={Boolean(errors.departReason)} | |||
| helperText={ | |||
| Boolean(errors.departReason) && | |||
| (errors.departReason?.message | |||
| ? t(errors.departReason.message) | |||
| : t("Please input correct departReason")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| label={t("Remark")} | |||
| fullWidth | |||
| multiline | |||
| rows={4} | |||
| {...register("remark")} | |||
| error={Boolean(errors.remark)} | |||
| helperText={ | |||
| Boolean(errors.remark) && | |||
| (errors.remark?.message | |||
| ? t(errors.remark.message) | |||
| : t("Please input correct remark")) | |||
| } | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default StaffInfo; | |||