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