| @@ -43,7 +43,7 @@ const CreateStaff: React.FC = async () => { | |||
| const { t } = await getServerI18n("staff"); | |||
| const title = ['', t('Additional Info')] | |||
| // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$") | |||
| // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") | |||
| // console.log(regex) | |||
| const fieldLists = [ | |||
| [ | |||
| @@ -52,8 +52,6 @@ const CreateStaff: React.FC = async () => { | |||
| label: t("Staff ID"), | |||
| type: "text", | |||
| value: "", | |||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
| message: t("input matching format"), | |||
| required: true, | |||
| }, | |||
| { | |||
| @@ -67,55 +65,55 @@ const CreateStaff: React.FC = async () => { | |||
| id: "companyId", | |||
| label: t("Company"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: "Company A"}, {id: 2, key: 2, value: 2, label: "Company B"}], | |||
| options: [{id: 1, label: "Company A"}, {id: 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"}], | |||
| options: [{id: 1, label: "A"}, {id: 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"}], | |||
| options: [{id: 1, label: "Department A"}, {id: 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"}], | |||
| options: [{id: 1, label: "A"}, {id: 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"}], | |||
| options: [{id: 1, label: "excel"}, {id: 2, label: "word"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| 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"}], | |||
| options: [{id: 1, label: "pos1"}, {id: 2, label: "pos2"}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "salaryEffId", | |||
| label: t("Salary Point"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: 1, value: 1, label: t("15")}, {id: 2, key: 2, value: 2, label: t("20")}], | |||
| options: [{id: 1, label: t("15")}, {id: 2, label: t("20")}], | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "hourlyRate", | |||
| label: t("Hourly Rate"), | |||
| type: "numeric", | |||
| type: "numeric-testing", | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| @@ -123,29 +121,35 @@ const CreateStaff: React.FC = async () => { | |||
| id: "employType", | |||
| label: t("Employ Type"), | |||
| type: "combo-Obj", | |||
| options: [{id: 1, key: "FT", value: "FT", label: t("FT")}, {id: 2, key: "PT", value: "PT", label: t("PT")}], | |||
| options: [{id: "FT", label: t("FT")}, {id: "PT", label: t("PT")}], | |||
| value: "", | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "email", | |||
| label: t("Email"), | |||
| type: "email", | |||
| type: "text", | |||
| value: "", | |||
| pattern: "^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$", | |||
| message: t("input matching format"), | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "phone1", | |||
| label: t("Phone1"), | |||
| type: "numeric", | |||
| type: "text", | |||
| value: "", | |||
| pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| required: true, | |||
| }, | |||
| { | |||
| id: "phone2", | |||
| label: t("Phone2"), | |||
| type: "numeric", | |||
| type: "text", | |||
| value: "", | |||
| pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| required: true, | |||
| }, | |||
| ], | |||
| @@ -160,8 +164,10 @@ const CreateStaff: React.FC = async () => { | |||
| { | |||
| id: "emergContactPhone", | |||
| label: t("Emergency Contact Phone"), | |||
| type: "numeric", | |||
| type: "text", | |||
| value: "", | |||
| pattern: "^\\d{8}$", | |||
| message: t("input correct phone no."), | |||
| required: true, | |||
| }, | |||
| { | |||
| @@ -0,0 +1,10 @@ | |||
| const EditStaff: React.FC = async () => { | |||
| return ( | |||
| <> | |||
| sdsadasd | |||
| </> | |||
| ) | |||
| } | |||
| export default EditStaff; | |||
| @@ -34,7 +34,7 @@ const Staff: React.FC = async () => { | |||
| variant="contained" | |||
| startIcon={<Add />} | |||
| LinkComponent={Link} | |||
| href="/staff/create" | |||
| href="/settings/staff/create" | |||
| > | |||
| {t("Create Staff")} | |||
| </Button> | |||
| @@ -1,7 +1,7 @@ | |||
| "use server"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { StaffResult } from "."; | |||
| export interface CreateCustomInputs { | |||
| // Project details | |||
| projectCode: string; | |||
| @@ -22,19 +22,28 @@ export interface CreateStaffInputs { | |||
| email: string; | |||
| phone1: string; | |||
| phone2: string; | |||
| hourlyRate: string | number; | |||
| emergContactName: string; | |||
| emergContactPhone: string; | |||
| employType: string; | |||
| joinDate: string | null; | |||
| 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), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| }; | |||
| export const deleteStaff = async (id: number) => { | |||
| return serverFetchJson(`${BASE_API_URL}/staffs/delete/${id}`, { | |||
| method: "DELETE", | |||
| // body: JSON.stringify(id), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| }; | |||
| @@ -25,18 +25,11 @@ interface Field { | |||
| } | |||
| interface formProps { | |||
| // onSubmit: (data: any) => void; | |||
| // resetForm: () => void; | |||
| // Title?: string[]; | |||
| // isActive: boolean; | |||
| Title?: string[] | |||
| Title?: string[]; | |||
| fieldLists: Field[][]; | |||
| } | |||
| const CreateStaffForm: React.FC<formProps> = ({ | |||
| Title, | |||
| fieldLists | |||
| }) => { | |||
| const CreateStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
| const router = useRouter(); | |||
| const { t } = useTranslation(); | |||
| const [serverError, setServerError] = useState(""); | |||
| @@ -48,16 +41,38 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| async (data) => { | |||
| try { | |||
| console.log(data); | |||
| let haveError = false; | |||
| //check if joinDate exist | |||
| if (data.joinDate == null && data.joinDate == "Invalid Date") { | |||
| haveError = true; | |||
| return haveError; | |||
| } | |||
| //check if joinDate > departDate | |||
| if (data.departDate != null && data.departDate != "Invalid Date") { | |||
| if (data.joinDate != null) { | |||
| const joinDate = new Date(data.joinDate); | |||
| const departDate = new Date(data.departDate); | |||
| if (joinDate.getTime() > departDate.getTime()) { | |||
| haveError = true; | |||
| return haveError; | |||
| } | |||
| } | |||
| } | |||
| if (haveError) { | |||
| return | |||
| } | |||
| const postData = { | |||
| ...data, | |||
| emergContactPhone: data.emergContactPhone.toString(), | |||
| phone1: data.phone1.toString(), | |||
| phone2: data.phone2.toString(), | |||
| } | |||
| hourlyRate: typeof data.hourlyRate === 'string' ? parseInt(data.hourlyRate.replace("$", "").replace(",", "")) : 0 | |||
| }; | |||
| console.log(postData); | |||
| setServerError(""); | |||
| await saveStaff(postData); | |||
| router.replace("/staff"); | |||
| router.replace("/settings/staff"); | |||
| } catch (e) { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| @@ -67,10 +82,10 @@ const CreateStaffForm: React.FC<formProps> = ({ | |||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateStaffInputs>>( | |||
| (errors) => { | |||
| console.log(errors) | |||
| console.log(errors); | |||
| }, | |||
| [], | |||
| ); | |||
| [] | |||
| ); | |||
| return ( | |||
| <> | |||
| @@ -29,6 +29,8 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import dayjs from "dayjs"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import { Check, Close } from "@mui/icons-material"; | |||
| import { NumericFormat, NumericFormatProps } from "react-number-format"; | |||
| import * as React from "react"; | |||
| // interface Option { | |||
| // // Define properties of each option object | |||
| @@ -52,7 +54,10 @@ interface Field { | |||
| size?: number; | |||
| setValue?: any[]; | |||
| } | |||
| interface CustomProps { | |||
| onChange: (event: { target: { name: string; value: string } }) => void; | |||
| name: string; | |||
| } | |||
| interface CustomInputFormProps { | |||
| onSubmit: (data: any) => void; | |||
| onSubmitError?: (data: any) => void; | |||
| @@ -77,7 +82,13 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| // resetForm, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { reset, register, handleSubmit, control, formState: { errors } } = useForm(); | |||
| const { | |||
| reset, | |||
| register, | |||
| handleSubmit, | |||
| control, | |||
| formState: { errors }, | |||
| } = useForm(); | |||
| const [dateObj, setDateObj] = useState<any>(null); | |||
| const [value, setValue] = useState<any>({}); | |||
| const [checkboxValue, setCheckboxValue] = useState({}); | |||
| @@ -105,7 +116,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| if (checkboxValue !== null) { | |||
| data = { ...data, ...checkboxValue }; | |||
| } | |||
| const finalData = { | |||
| ...value, | |||
| ...data, | |||
| @@ -176,6 +187,39 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| }); | |||
| }); | |||
| const NumericFormatCustom = React.forwardRef<NumericFormatProps, CustomProps>( | |||
| function NumericFormatCustom(props, ref) { | |||
| const { onChange, ...other } = props; | |||
| return ( | |||
| <NumericFormat | |||
| {...other} | |||
| getInputRef={ref} | |||
| onValueChange={(values) => { | |||
| onChange({ | |||
| target: { | |||
| name: props.name, | |||
| value: values.value, | |||
| }, | |||
| }); | |||
| }} | |||
| thousandSeparator | |||
| valueIsNumericString | |||
| prefix="$" | |||
| /> | |||
| ); | |||
| } | |||
| ); | |||
| const [values, setValues] = React.useState({ | |||
| hourlyRate: "", | |||
| }); | |||
| const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
| setValues({ | |||
| ...values, | |||
| [event.target.name]: event.target.value, | |||
| }); | |||
| }; | |||
| return ( | |||
| <form onSubmit={handleSubmit(handleFormSubmit, onSubmitError)}> | |||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||
| @@ -201,12 +245,16 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| label={field.label} | |||
| fullWidth | |||
| {...register(field.id, { | |||
| pattern: field.pattern ? new RegExp(field.pattern) : /.*/, | |||
| 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} | |||
| helperText={ | |||
| Boolean(errors[field.id]) && field.message | |||
| } | |||
| /> | |||
| </Grid> | |||
| ); | |||
| @@ -217,16 +265,19 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| label={field.label} | |||
| fullWidth | |||
| {...register(field.id, { | |||
| pattern: /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, | |||
| pattern: | |||
| /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/, | |||
| })} | |||
| defaultValue={!field.value ? `${field.value}` : ""} | |||
| required={field.required ?? false} | |||
| error={Boolean(errors[field.id])} | |||
| helperText={Boolean(errors[field.id]) && field.message} | |||
| 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}> | |||
| @@ -324,6 +375,34 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| /> | |||
| </Grid> | |||
| ); | |||
| } else if (field.type === "numeric-testing") { | |||
| return ( | |||
| <Grid item xs={field.size ?? 6} key={field.id}> | |||
| <FormControl fullWidth> | |||
| <Controller | |||
| {...register(field.id)} | |||
| name={field.id} | |||
| control={control} | |||
| defaultValue={ | |||
| !field.value ? `${field.value}` : "" | |||
| } | |||
| render={({ field }) => { | |||
| console.log(field); | |||
| return ( | |||
| <NumericFormat | |||
| {...field} | |||
| customInput={TextField} | |||
| thousandSeparator | |||
| valueIsNumericString | |||
| prefix="$" | |||
| label={t(field.name)} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| ); | |||
| } else if (field.type === "numeric-positive") { | |||
| return ( | |||
| <Grid item xs={field.size ?? 6} key={field.id}> | |||
| @@ -331,6 +410,7 @@ const CustomInputForm: React.FC<CustomInputFormProps> = ({ | |||
| fullWidth | |||
| {...register(field.id)} | |||
| id={field.id} | |||
| name={field.id} | |||
| label={field.label} | |||
| defaultValue={!field.value ? `${field.value}` : ""} | |||
| inputProps={{ | |||
| @@ -0,0 +1,106 @@ | |||
| "use client"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import Button from "@mui/material/Button"; | |||
| import { Card, Modal, Stack, Typography } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Add } from "@mui/icons-material"; | |||
| import Check from "@mui/icons-material/Check"; | |||
| import Close from "@mui/icons-material/Close"; | |||
| import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| interface Props { | |||
| isOpen: boolean; | |||
| onConfirm: (data: any) => void; | |||
| onCancel: (data: any | null) => void; | |||
| // staff: StaffResult[]; | |||
| } | |||
| const ConfirmModal: React.FC<Props> = ({ ...props }) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <> | |||
| <Modal open={props.isOpen} onClose={props.onCancel}> | |||
| <Card | |||
| style={{ | |||
| flex: 10, | |||
| marginBottom: "20px", | |||
| width: "auto", | |||
| minWidth: "400px", | |||
| minHeight: "200px", | |||
| position: "fixed", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| }} | |||
| > | |||
| <> | |||
| <Typography | |||
| variant="h5" | |||
| id="modal-title" | |||
| sx={{ | |||
| flex: 1, | |||
| ml: 4, | |||
| mt: 2, | |||
| }} | |||
| > | |||
| {t("Confirm")} | |||
| </Typography> | |||
| <> | |||
| <Typography | |||
| variant="h6" | |||
| id="modal-title" | |||
| sx={{ | |||
| flex: 1, | |||
| mt: 4, | |||
| justifyContent: "center", | |||
| textAlign: "center", | |||
| }} | |||
| > | |||
| {t("Are You Sure")} | |||
| </Typography> | |||
| </> | |||
| {/* <ThemeProvider theme={TSMS_BUTTON_THEME}> */} | |||
| <Stack direction="row"> | |||
| <Button | |||
| variant="contained" | |||
| endIcon={<Check />} | |||
| sx={{ | |||
| flex: 1, | |||
| ml: 5, | |||
| mr: 2, | |||
| mt: 4, | |||
| justifyContent: "space-between", | |||
| }} | |||
| onClick={props.onConfirm} | |||
| // LinkComponent={Link} | |||
| // href="/settings/department/new" | |||
| > | |||
| Proceed | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<Close />} | |||
| sx={{ | |||
| flex: 1, | |||
| mr: 5, | |||
| mt: 4, | |||
| justifyContent: "space-between", | |||
| }} | |||
| color="warning" | |||
| onClick={props.onCancel} | |||
| // LinkComponent={Link} | |||
| // href="/settings/department/new" | |||
| > | |||
| Cancel | |||
| </Button> | |||
| </Stack> | |||
| {/* </ThemeProvider> */} | |||
| </> | |||
| </Card> | |||
| </Modal> | |||
| </> | |||
| ); | |||
| }; | |||
| export default ConfirmModal; | |||
| @@ -1,11 +1,14 @@ | |||
| "use client"; | |||
| import { StaffResult } from "@/app/api/staff"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import SearchBox, { Criterion } from "../SearchBox/index"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults/index"; | |||
| import EditNote from "@mui/icons-material/EditNote"; | |||
| import DeleteIcon from '@mui/icons-material/Delete'; | |||
| import ConfirmModal from "./ConfirmDeleteModal"; | |||
| import { deleteStaff } from "@/app/api/staff/actions"; | |||
| interface Props { | |||
| staff: StaffResult[]; | |||
| @@ -16,10 +19,9 @@ type SearchParamNames = keyof SearchQuery; | |||
| const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| const { t } = useTranslation(); | |||
| // If claim searching is done on the server-side, then no need for this. | |||
| const [filteredStaff, setFilteredStaff] = useState(staff); | |||
| // const [filteredStaffRef, setFilteredStaffRef] = useState(staff); | |||
| const [id, setId] = useState(0); | |||
| const [isOpen, setIsOpen] = useState(false); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| @@ -59,6 +61,30 @@ const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| console.log(staff); | |||
| }, []); | |||
| const deleteClick = (staff: StaffResult) => { | |||
| console.log(staff.id); | |||
| const temp = staff.id | |||
| console.log(temp) | |||
| setId(temp) | |||
| setIsOpen(!isOpen) | |||
| }; | |||
| const onConfirm = (staff: StaffResult) => { | |||
| console.log(staff); | |||
| console.log(id); | |||
| deleteStaff(id) | |||
| // setIsOpen(!isOpen) | |||
| } | |||
| const onCancel = useCallback((staff: StaffResult) => { | |||
| console.log(staff); | |||
| // setId(0) | |||
| setIsOpen(false) | |||
| }, []); | |||
| useEffect(() => { | |||
| console.log("id"); | |||
| console.log(id); | |||
| }, [id]); | |||
| const columns = useMemo<Column<StaffResult>[]>( | |||
| () => [ | |||
| { | |||
| @@ -72,8 +98,14 @@ const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| { name: "staffId", label: t("Staff ID") }, | |||
| { name: "grade", label: t("Grade") }, | |||
| { name: "currentPosition", label: t("Current Position") }, | |||
| { | |||
| name: "action", | |||
| label: t("Actions"), | |||
| onClick: deleteClick, | |||
| buttonIcon: <DeleteIcon />, | |||
| }, | |||
| ], | |||
| [t, onStaffClick], | |||
| [t, onStaffClick, deleteClick], | |||
| ); | |||
| return ( | |||
| @@ -94,6 +126,11 @@ const StaffSearch: React.FC<Props> = ({ staff }) => { | |||
| }} | |||
| /> | |||
| <SearchResults<StaffResult> items={filteredStaff} columns={columns} /> | |||
| <ConfirmModal | |||
| isOpen={isOpen} | |||
| onConfirm={onConfirm} | |||
| onCancel={onCancel} | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||