| @@ -28,18 +28,6 @@ import { ProjectCategory } from "@/app/api/projects"; | |||
| import { Grid, Typography } from "@mui/material"; | |||
| import CreateStaffForm from "@/components/CreateStaff/CreateStaffForm"; | |||
| // import { Metadata } from "next"; | |||
| // export const metadata: Metadata = { | |||
| // title: "staffCreate", | |||
| // }; | |||
| // export interface Props { | |||
| // allTasks: Task[]; | |||
| // projectCategories: ProjectCategory[]; | |||
| // taskTemplates: TaskTemplate[]; | |||
| // teamLeads: Staff[]; | |||
| // } | |||
| interface CreateCustomInputs { | |||
| projectCode: string; | |||
| projectName: string; | |||
| @@ -52,65 +40,84 @@ const createCustomInputs: CreateCustomInputs = { | |||
| // const Title = ["title1", "title2"]; | |||
| const CreateStaff: React.FC = async () => { | |||
| const { t } = await getServerI18n("createStaff"); | |||
| const title = ['', 'Additional Info'] | |||
| const { t } = await getServerI18n("staff"); | |||
| const title = ['', t('Additional Info')] | |||
| // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$") | |||
| // console.log(regex) | |||
| const fieldLists = [ | |||
| [ | |||
| { | |||
| id: "name", | |||
| label: t("Staff Id"), | |||
| id: "staffId", | |||
| label: t("Staff ID"), | |||
| type: "text", | |||
| value: "asdasd", | |||
| // required: "asdasd", | |||
| // option: "asdasd", | |||
| value: "", | |||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
| message: t("input matching format"), | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "name", | |||
| label: t("Staff Name"), | |||
| type: "text", | |||
| value: "asdasd", | |||
| // required: "asdasd", | |||
| // option: "asdasd", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "companyId", | |||
| label: t("Company"), | |||
| type: "combo-Obj", | |||
| // value: "asdasd", | |||
| // required: "asdasd", | |||
| options: [{id: 1, key: 1, value: 1, label: "Company A"}, {id: 2, key: 2, value: 2, label: "Company B"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "teamId", | |||
| label: t("Team"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "A"}, {id: 2, key: 2, value: 2, label: "B"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "departmentId", | |||
| label: t("Department"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "Department A"}, {id: 2, key: 2, value: 2, label: "Department B"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "gradeId", | |||
| label: t("Grade"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "A"}, {id: 2, key: 2, value: 2, label: "B"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "skillSetId", | |||
| label: t("Skillset"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "excel"}, {id: 2, key: 2, value: 2, label: "word"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "currentPosition", | |||
| id: "currentPositionId", | |||
| label: t("Current Position"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "pos1"}, {id: 2, key: 2, value: 2, label: "pos2"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "salaryEffId", | |||
| label: t("Salary point ID with effective date"), | |||
| label: t("Salary Point"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: t("first")}, {id: 2, key: 2, value: 2, label: t("second")}], | |||
| options: [{id: 1, key: 1, value: 1, label: t("15")}, {id: 2, key: 2, value: 2, label: t("20")}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "hourlyRate", | |||
| label: t("Hourly Rate"), | |||
| type: "numeric", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "employType", | |||
| @@ -118,25 +125,29 @@ const CreateStaff: React.FC = async () => { | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: "FT", value: "FT", label: t("FT")}, {id: 2, key: "PT", value: "PT", label: t("PT")}], | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "email", | |||
| label: t("Email"), | |||
| type: "text", | |||
| type: "email", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "phone1", | |||
| label: t("Phone1"), | |||
| type: "numeric", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "phone2", | |||
| label: t("Phone2"), | |||
| type: "numeric", | |||
| value: "", | |||
| }, | |||
| required: true, | |||
| }, | |||
| ], | |||
| [ | |||
| { | |||
| @@ -144,24 +155,28 @@ const CreateStaff: React.FC = async () => { | |||
| label: t("Emergency Contact Name"), | |||
| type: "text", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "emergContactPhone", | |||
| label: t("Emergency Contact Phone"), | |||
| type: "numeric", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "joinDate", | |||
| label: t("Join Date"), | |||
| type: "multiDate", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "joinPosition", | |||
| id: "joinPositionId", | |||
| label: t("Join Position"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "pos1"}, {id: 2, key: 2, value: 2, label: "pos2"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "departDate", | |||
| @@ -171,7 +186,7 @@ const CreateStaff: React.FC = async () => { | |||
| }, | |||
| { | |||
| id: "departReason", | |||
| label: t("Reason"), | |||
| label: t("Depart Reason"), | |||
| type: "text", | |||
| value: "", | |||
| }, | |||
| @@ -187,7 +202,7 @@ const CreateStaff: React.FC = async () => { | |||
| return ( | |||
| <> | |||
| <Typography variant="h4">{t("Create Staff")}</Typography> | |||
| <I18nProvider namespaces={["createStaff"]}> | |||
| <I18nProvider namespaces={["staff"]}> | |||
| <CreateStaffForm | |||
| Title={title} | |||
| fieldLists={fieldLists} | |||
| @@ -2,7 +2,7 @@ | |||
| import { preloadClaims } from "@/app/api/claims"; | |||
| import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; | |||
| import StaffSearch from "@/components/StaffSearch"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| @@ -16,9 +16,9 @@ export const metadata: Metadata = { | |||
| }; | |||
| const Staff: React.FC = async () => { | |||
| const { t } = await getServerI18n("projects"); | |||
| preloadTeamLeads() | |||
| preloadStaff() | |||
| const { t } = await getServerI18n("staff"); | |||
| preloadTeamLeads(); | |||
| preloadStaff(); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| @@ -39,9 +39,11 @@ const Staff: React.FC = async () => { | |||
| {t("Create Staff")} | |||
| </Button> | |||
| </Stack> | |||
| <Suspense fallback={<StaffSearch.Loading />}> | |||
| <StaffSearch /> | |||
| </Suspense> | |||
| <I18nProvider namespaces={["staff", "common"]}> | |||
| <Suspense fallback={<StaffSearch.Loading />}> | |||
| <StaffSearch /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -25,12 +25,13 @@ export interface CreateStaffInputs { | |||
| emergContactName: string; | |||
| emergContactPhone: string; | |||
| employType: string; | |||
| departDate: string; | |||
| departReason: string; | |||
| remark: string; | |||
| departDate: string | null; | |||
| departReason: string | null; | |||
| remark: string | null; | |||
| } | |||
| export const saveStaff = async (data: CreateStaffInputs) => { | |||
| console.log(`${BASE_API_URL}/staffs/new`) | |||
| return serverFetchJson(`${BASE_API_URL}/staffs/new`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| @@ -4,6 +4,7 @@ import { cache } from "react"; | |||
| import "server-only"; | |||
| export interface StaffResult { | |||
| action: any; | |||
| id: number; | |||
| name: string; | |||
| team: string; | |||
| @@ -37,16 +37,10 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| Title, | |||
| fieldLists | |||
| }) => { | |||
| // const [formData, setFormData] = useState<any>(null); | |||
| const router = useRouter(); | |||
| const { t } = useTranslation(); | |||
| const [serverError, setServerError] = useState(""); | |||
| // const handleSubmit = (data: any) => { | |||
| // console.log(data); | |||
| // // Handle the form submission logic here | |||
| // // setFormData(data); | |||
| // }; | |||
| const handleCancel = () => { | |||
| router.back(); | |||
| }; | |||
| @@ -54,8 +48,16 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| async (data) => { | |||
| try { | |||
| console.log(data); | |||
| const postData = { | |||
| ...data, | |||
| emergContactPhone: data.emergContactPhone.toString(), | |||
| phone1: data.phone1.toString(), | |||
| phone2: data.phone2.toString(), | |||
| } | |||
| console.log(postData); | |||
| setServerError(""); | |||
| await saveStaff(data); | |||
| await saveStaff(postData); | |||
| router.replace("/staff"); | |||
| } catch (e) { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| @@ -63,6 +65,13 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| [router, t] | |||
| ); | |||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateStaffInputs>>( | |||
| (errors) => { | |||
| console.log(errors) | |||
| }, | |||
| [], | |||
| ); | |||
| return ( | |||
| <> | |||
| {serverError && ( | |||
| @@ -75,6 +84,7 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| fieldLists={fieldLists} | |||
| isActive={true} | |||
| onSubmit={onSubmit} | |||
| onSubmitError={onSubmitError} | |||
| onCancel={handleCancel} | |||
| /> | |||
| </> | |||
| @@ -27,7 +27,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import dayjs from "dayjs"; | |||
| import { useEffect, useState } from "react"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import { Check, Close } from "@mui/icons-material"; | |||
| // interface Option { | |||
| @@ -45,6 +45,8 @@ interface Field { | |||
| type: string; | |||
| value?: any; | |||
| required?: boolean; | |||
| pattern?: string; | |||
| message?: string; | |||
| options?: any[]; | |||
| readOnly?: boolean; | |||
| size?: number; | |||
| @@ -53,6 +55,7 @@ interface Field { | |||
| interface CustomInputFormProps { | |||
| onSubmit: (data: any) => void; | |||
| onSubmitError?: (data: any) => void; | |||
| onCancel: () => void; | |||
| // resetForm: () => void; | |||
| Title?: string[]; | |||
| @@ -60,20 +63,24 @@ interface CustomInputFormProps { | |||
| fieldLists: Field[][]; | |||
| } | |||
| // interface SubComponents { | |||
| // Loading: typeof CustomerSearchLoading; | |||
| // } | |||
| const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| Title, | |||
| isActive, | |||
| fieldLists, | |||
| onSubmit, | |||
| onSubmitError, | |||
| onCancel, | |||
| // resetForm, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { reset, register, handleSubmit, control } = useForm(); | |||
| const { reset, register, handleSubmit, control, formState: { errors } } = useForm(); | |||
| const [dateObj, setDateObj] = useState<any>(null); | |||
| const [value, setValue] = useState<any>({}); | |||
| const [checkboxValue, setCheckboxValue] = useState({}); | |||
| // const [dateObj, setDateObj] = useState({}); | |||
| interface DateObj { | |||
| [key: string]: string; | |||
| @@ -98,10 +105,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| if (checkboxValue !== null) { | |||
| data = { ...data, ...checkboxValue }; | |||
| } | |||
| // if (value !== null) { | |||
| // data.dropdownCombo = value; | |||
| // } | |||
| const finalData = { | |||
| ...value, | |||
| ...data, | |||
| @@ -172,14 +176,8 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| }); | |||
| }); | |||
| // useEffect(() => { | |||
| // if (dateObj) { | |||
| // console.log(dateObj); | |||
| // } | |||
| // }, [dateObj]); | |||
| return ( | |||
| <form onSubmit={handleSubmit(handleFormSubmit)}> | |||
| <form onSubmit={handleSubmit(handleFormSubmit, onSubmitError)}> | |||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <> | |||
| @@ -200,17 +198,35 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| return ( | |||
| <Grid item xs={field.size ?? 6} key={field.id}> | |||
| <TextField | |||
| label={t(`${field.label}`)} | |||
| label={field.label} | |||
| fullWidth | |||
| {...register(field.id, { | |||
| pattern: field.pattern ? new RegExp(field.pattern) : /.*/, | |||
| })} | |||
| defaultValue={!field.value ? `${field.value}` : ""} | |||
| required={field.required ?? false} | |||
| error={Boolean(errors[field.id])} | |||
| helperText={Boolean(errors[field.id]) && field.message} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| } else if (field.type === "email") { | |||
| return ( | |||
| <Grid item xs={field.size ?? 6} key={field.id}> | |||
| <TextField | |||
| label={field.label} | |||
| fullWidth | |||
| // {...register(`asdasd`, { | |||
| {...register(`${field.id}`, { | |||
| required: field.required ?? false, | |||
| {...register(field.id, { | |||
| pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, | |||
| })} | |||
| // error={Boolean(errors.projectCode)} | |||
| defaultValue={!field.value ? `${field.value}` : ""} | |||
| required={field.required ?? false} | |||
| error={Boolean(errors[field.id])} | |||
| helperText={Boolean(errors[field.id]) && field.message} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| } else if (field.type === "multiDate") { | |||
| }else if (field.type === "multiDate") { | |||
| return ( | |||
| <Grid item xs={field.size ?? 6} key={field.id}> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| @@ -408,4 +424,6 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| ); | |||
| }; | |||
| // CustomInputForm.Loading = CustomerSearchLoading; | |||
| export default CustomInputForm; | |||
| @@ -15,7 +15,7 @@ type SearchQuery = Partial<Omit<StaffResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| const { t } = useTranslation("staff"); | |||
| const { t } = useTranslation(); | |||
| // If claim searching is done on the server-side, then no need for this. | |||
| const [filteredStaff, setFilteredStaff] = useState(staff); | |||
| @@ -61,12 +61,12 @@ const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| const columns = useMemo<Column<StaffResult>[]>( | |||
| () => [ | |||
| // { | |||
| // name: "action", | |||
| // label: t("Actions"), | |||
| // onClick: onClaimClick, | |||
| // buttonIcon: <EditNote />, | |||
| // }, | |||
| { | |||
| name: "action", | |||
| label: t("Actions"), | |||
| onClick: onStaffClick, | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { name: "team", label: t("Team") }, | |||
| { name: "name", label: t("Staff Name") }, | |||
| { name: "staffId", label: t("Staff ID") }, | |||
| @@ -0,0 +1,27 @@ | |||
| { | |||
| "Staff": "員工", | |||
| "Team": "隊伍", | |||
| "Staff Name": "員工姓名", | |||
| "Staff ID": "員工編號", | |||
| "Grade": "級別", | |||
| "Current Position": "現職", | |||
| "Actions": "編輯", | |||
| "Create Staff": "新增員工", | |||
| "Company": "公司", | |||
| "Department": "部門", | |||
| "Skillset": "技能", | |||
| "Salary Point": "薪金點", | |||
| "Employ Type": "職位類別", | |||
| "Hourly Rate": "時薪", | |||
| "Email": "時薪", | |||
| "Phone1": "聯絡電話", | |||
| "Phone2": "次要聯絡電話", | |||
| "Additional Info": "更多資料", | |||
| "Emergency Contact Name": "緊急聯絡人", | |||
| "Emergency Contact Phone": "緊急聯絡人電話", | |||
| "Join Date": "入職日期", | |||
| "Join Position": "入職職位", | |||
| "Depart Date": "離職日期", | |||
| "Depart Reason": "離職原因", | |||
| "Remark": "備註" | |||
| } | |||