@@ -1,5 +1,4 @@ | |||||
import { Edit } from "@mui/icons-material"; | import { Edit } from "@mui/icons-material"; | ||||
import { useSearchParams } from "next/navigation"; | |||||
// import EditStaff from "@/components/EditStaff"; | // import EditStaff from "@/components/EditStaff"; | ||||
import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
@@ -8,10 +7,7 @@ import { Metadata } from "next"; | |||||
import EditSkill from "@/components/EditSkill"; | import EditSkill from "@/components/EditSkill"; | ||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import { fetchSkill } from "@/app/api/skill"; | 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 ({ | const EditSkillPage: React.FC<searchParamsProps> = async ({ | ||||
searchParams, | searchParams, | ||||
@@ -6,12 +6,15 @@ import { Suspense } from "react"; | |||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | ||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { searchParamsProps } from "@/app/utils/fetchUtil"; | |||||
// export const metadata: Metadata = { | // export const metadata: Metadata = { | ||||
// title: "staff-edit", | // title: "staff-edit", | ||||
// }; | // }; | ||||
const EditStaffPage: React.FC = () => { | |||||
const EditStaffPage: React.FC<searchParamsProps> = async ({ | |||||
searchParams, | |||||
}) => { | |||||
// const searchParams = useSearchParams(); | // const searchParams = useSearchParams(); | ||||
// const userId = searchParams.get('param'); | // const userId = searchParams.get('param'); | ||||
// console.log(userId); // Access the value of the "user_id" parameter | // console.log(userId); // Access the value of the "user_id" parameter | ||||
@@ -20,7 +23,7 @@ const EditStaffPage: React.FC = () => { | |||||
<> | <> | ||||
<I18nProvider namespaces={["staff", "common"]}> | <I18nProvider namespaces={["staff", "common"]}> | ||||
<Suspense fallback={<EditStaff.Loading />}> | <Suspense fallback={<EditStaff.Loading />}> | ||||
<EditStaff /> | |||||
<EditStaff id={parseInt(searchParams.id as string)}/> | |||||
</Suspense> | </Suspense> | ||||
</I18nProvider> | </I18nProvider> | ||||
{/* <EditStaff /> */} | {/* <EditStaff /> */} | ||||
@@ -26,6 +26,35 @@ export interface StaffTeamTable { | |||||
currentPosition: string; | 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 { | export interface StaffResult { | ||||
action: any; | action: any; | ||||
id: number; | id: number; | ||||
@@ -35,10 +64,11 @@ export interface StaffResult { | |||||
grade: string; | grade: string; | ||||
joinPosition: string; | joinPosition: string; | ||||
currentPosition: string; | currentPosition: string; | ||||
data: data; | |||||
teamId: number; | teamId: number; | ||||
staffName: string; | staffName: string; | ||||
userId: number; | userId: number; | ||||
companyId: number; | |||||
data: data; | |||||
} | } | ||||
export interface searchInput { | export interface searchInput { | ||||
staffId: string; | 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 () => { | export const fetchStaffWithoutTeam = cache(async () => { | ||||
return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs/noteam`, { | return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs/noteam`, { | ||||
next: { tags: ["staffs"] }, | next: { tags: ["staffs"] }, | ||||
@@ -3,6 +3,10 @@ import { getServerSession } from "next-auth"; | |||||
import { headers } from "next/headers"; | import { headers } from "next/headers"; | ||||
import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
export interface searchParamsProps { | |||||
searchParams: { [key: string]: string | string[] | undefined }; | |||||
} | |||||
export class ServerFetchError extends Error { | export class ServerFetchError extends Error { | ||||
public readonly response: Response | undefined; | public readonly response: Response | undefined; | ||||
constructor(message?: string, response?: Response) { | constructor(message?: string, response?: Response) { | ||||
@@ -55,6 +55,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
(acc, skill) => ({ ...acc, [skill.id]: skill.label }), | (acc, skill) => ({ ...acc, [skill.id]: skill.label }), | ||||
{} | {} | ||||
); | ); | ||||
console.log(skillIdNameMap) | |||||
const resetStaff = useCallback(() => { | const resetStaff = useCallback(() => { | ||||
console.log(defaultValues); | console.log(defaultValues); | ||||
@@ -235,6 +236,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
> | > | ||||
<Checkbox | <Checkbox | ||||
checked={field.value!.indexOf(skill.id) > -1} | checked={field.value!.indexOf(skill.id) > -1} | ||||
// checked={true} | |||||
/> | /> | ||||
<ListItemText primary={skill.label} /> | <ListItemText primary={skill.label} /> | ||||
</MenuItem> | </MenuItem> | ||||
@@ -1,11 +1,18 @@ | |||||
"use client"; | "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 { 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 { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; | ||||
import { fetchTeamCombo } from "@/app/api/team/actions"; | import { fetchTeamCombo } from "@/app/api/team/actions"; | ||||
import { fetchDepartmentCombo } from "@/app/api/departments/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 { fetchGradeCombo } from "@/app/api/grades/actions"; | ||||
import { fetchSkillCombo } from "@/app/api/skill/actions"; | import { fetchSkillCombo } from "@/app/api/skill/actions"; | ||||
import { fetchSalaryCombo } from "@/app/api/salarys/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 { 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(() => { | 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 ( | 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 React from "react"; | ||||
import EditStaff from "./EditStaff"; | |||||
import EditStaff, { comboItem } from "./EditStaff"; | |||||
import EditStaffLoading from "./EditStaffLoading"; | 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 { 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 { | interface SubComponents { | ||||
Loading: typeof EditStaffLoading; | 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; | 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; |