@@ -80,8 +80,8 @@ export interface searchInput { | |||||
export interface SalaryEffectiveInfo { | export interface SalaryEffectiveInfo { | ||||
id: number; | id: number; | ||||
date: string; | |||||
salaryPoint: number; | |||||
date: string | Date; | |||||
salaryPoint: number | string; | |||||
} | } | ||||
export const preloadTeamLeads = () => { | export const preloadTeamLeads = () => { | ||||
@@ -28,6 +28,7 @@ import { IndividualStaff, SalaryEffectiveInfo } from "@/app/api/staff"; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
import { List, differenceBy } from "lodash"; | import { List, differenceBy } from "lodash"; | ||||
// import { useGridApiContext } from '@mui/x-data-grid'; | |||||
export interface comboItem { | export interface comboItem { | ||||
company: comboProp[]; | company: comboProp[]; | ||||
@@ -47,6 +48,8 @@ interface formProps { | |||||
const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) => { | const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) => { | ||||
console.log(combos.salary) | |||||
console.log(SalaryEffectiveInfo) | |||||
const defaultSkillset = Staff.skillset.map((s: any) => s.skill.id) | const defaultSkillset = Staff.skillset.map((s: any) => s.skill.id) | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
@@ -74,7 +77,12 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) | |||||
departDate: dayjs(Staff.departDate).toString() || "", | departDate: dayjs(Staff.departDate).toString() || "", | ||||
departReason: Staff.departReason, | departReason: Staff.departReason, | ||||
remark: Staff.remark, | remark: Staff.remark, | ||||
salaryEffectiveInfo: SalaryEffectiveInfo | |||||
salaryEffectiveInfo: SalaryEffectiveInfo.map(item => { | |||||
return ({ | |||||
id: Math.random(), | |||||
salaryPoint: combos.salary.filter(sal => sal.id === item.salaryPoint)[0].label, | |||||
date: dayjs(item.date).toDate(), | |||||
})}) | |||||
}}); | }}); | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -147,6 +155,11 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) | |||||
const postData = { | const postData = { | ||||
id: id, | id: id, | ||||
...data, | ...data, | ||||
salaryEffectiveInfo: SalaryEffectiveInfo.map(item => ({ | |||||
id: item.id, | |||||
salaryPoint: item.salaryPoint, | |||||
date: dayjs(item.date).format('YYYY-MM-DD') | |||||
})) | |||||
} | } | ||||
console.log(postData) | console.log(postData) | ||||
await saveStaff(postData) | await saveStaff(postData) | ||||
@@ -168,37 +181,60 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) | |||||
router.back(); | router.back(); | ||||
}; | }; | ||||
const resetStaff = useCallback(() => { | |||||
console.log(dayjs(Staff.joinDate).format(INPUT_DATE_FORMAT)) | |||||
console.log(formProps.getValues("joinDate")) | |||||
formProps.reset({ | |||||
staffId: Staff.staffId, | |||||
name: Staff.name, | |||||
companyId: Staff.company.id, | |||||
teamId: Staff.team?.id, | |||||
departmentId: Staff.department?.id, | |||||
gradeId: Staff.grade?.id, | |||||
skillSetId: defaultSkillset, | |||||
currentPositionId: Staff.currentPosition?.id, | |||||
salaryId: Staff.salary.salaryPoint, | |||||
employType: Staff.employType, | |||||
email: Staff.email, | |||||
phone1: Staff.phone1, | |||||
phone2: Staff.phone2, | |||||
emergContactName: Staff.emergContactName, | |||||
emergContactPhone: Staff.emergContactPhone, | |||||
joinDate: Staff.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, | |||||
salaryEffectiveInfo: SalaryEffectiveInfo | |||||
}); | |||||
}, [Staff,formProps]); | |||||
// const resetStaff = useCallback(() => { | |||||
// window.location.reload() | |||||
// console.log(dayjs(Staff.joinDate).format(INPUT_DATE_FORMAT)) | |||||
// console.log(formProps.getValues("joinDate")) | |||||
// formProps.setValue('salaryEffectiveInfo', SalaryEffectiveInfo.map(item => { | |||||
// return ({ | |||||
// id: item.id, | |||||
// salaryPoint: combos.salary.filter(sal => sal.id === item.salaryPoint)[0].label, | |||||
// date: dayjs(item.date).toDate(), | |||||
// })})) | |||||
// formProps.reset({ | |||||
// staffId: Staff.staffId, | |||||
// name: Staff.name, | |||||
// companyId: Staff.company.id, | |||||
// teamId: Staff.team?.id, | |||||
// departmentId: Staff.department?.id, | |||||
// gradeId: Staff.grade?.id, | |||||
// skillSetId: defaultSkillset, | |||||
// currentPositionId: Staff.currentPosition?.id, | |||||
// salaryId: Staff.salary.salaryPoint, | |||||
// employType: Staff.employType, | |||||
// email: Staff.email, | |||||
// phone1: Staff.phone1, | |||||
// phone2: Staff.phone2, | |||||
// emergContactName: Staff.emergContactName, | |||||
// emergContactPhone: Staff.emergContactPhone, | |||||
// joinDate: Staff.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, | |||||
// salaryEffectiveInfo: SalaryEffectiveInfo.map(item => { | |||||
// return ({ | |||||
// id: item.id, | |||||
// salaryPoint: combos.salary.filter(sal => sal.id === item.salaryPoint)[0].label, | |||||
// date: dayjs(item.date).toDate(), | |||||
// })}) | |||||
// }); | |||||
// }, []); | |||||
// useEffect(() => { | |||||
// formProps.setValue('salaryEffectiveInfo', SalaryEffectiveInfo.map(item => { | |||||
// return ({ | |||||
// id: item.id, | |||||
// salaryPoint: combos.salary.filter(sal => sal.id === item.salaryPoint)[0].label, | |||||
// date: dayjs(item.date).toDate(), | |||||
// })}) | |||||
// ) | |||||
// }, []); | |||||
useEffect(() => { | |||||
resetStaff() | |||||
}, [Staff, formProps, combos]); | |||||
// useEffect(() => { | |||||
// resetStaff() | |||||
// }, [Staff, formProps, combos]); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -218,7 +254,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo }) | |||||
<Button | <Button | ||||
variant="text" | variant="text" | ||||
startIcon={<RestartAlt />} | startIcon={<RestartAlt />} | ||||
onClick={resetStaff} | |||||
onClick={()=> window.location.reload()} | |||||
> | > | ||||
{t("Reset")} | {t("Reset")} | ||||
</Button> | </Button> | ||||
@@ -42,6 +42,8 @@ const EditStaffWrapper: React.FC<Props> & SubComponents = async ({ | |||||
fetchSkillCombo(), | fetchSkillCombo(), | ||||
fetchSalaryCombo(), | fetchSalaryCombo(), | ||||
]); | ]); | ||||
console.log(SalaryCombo.records) | |||||
const combos: comboItem = { | const combos: comboItem = { | ||||
company: CompanyCombo.records, | company: CompanyCombo.records, | ||||
team: TeamCombo.records, | team: TeamCombo.records, | ||||
@@ -54,7 +56,7 @@ const EditStaffWrapper: React.FC<Props> & SubComponents = async ({ | |||||
console.log(Staff.data) | console.log(Staff.data) | ||||
return <EditStaff Staff={Staff.data} combos={combos} SalaryEffectiveInfo={[{id:0, salaryPoint: 1, date:"2021-05-05"}]}/>; | |||||
return <EditStaff Staff={Staff.data} combos={combos} SalaryEffectiveInfo={[{id:0, salaryPoint: 1, date:"2021-05-05"}, {id:1, salaryPoint: 43, date:"2024-05-05"}]}/>; | |||||
}; | }; | ||||
EditStaffWrapper.Loading = EditStaffLoading; | EditStaffWrapper.Loading = EditStaffLoading; | ||||
@@ -1,17 +1,25 @@ | |||||
import React, { useEffect } from 'react'; | |||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material'; | import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material'; | ||||
import { useForm, Controller } from 'react-hook-form'; | |||||
import { useForm, Controller, useFormContext } from 'react-hook-form'; | |||||
import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | ||||
import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
import { DatePicker } from '@mui/x-date-pickers'; | import { DatePicker } from '@mui/x-date-pickers'; | ||||
import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid'; | |||||
import StyledDataGrid from '../StyledDataGrid'; | |||||
import AddIcon from '@mui/icons-material/Add'; | |||||
import SaveIcon from '@mui/icons-material/Save'; | |||||
import DeleteIcon from '@mui/icons-material/Delete'; | |||||
import CancelIcon from '@mui/icons-material/Cancel'; | |||||
import EditIcon from '@mui/icons-material/Edit'; | |||||
import { GridActionsCellItem } from '@mui/x-data-grid'; | |||||
interface SalaryEffectiveModelProps { | interface SalaryEffectiveModelProps { | ||||
open: boolean; | open: boolean; | ||||
onClose: () => void; | onClose: () => void; | ||||
modalSx?: SxProps; | modalSx?: SxProps; | ||||
onSave: () => void; | onSave: () => void; | ||||
columns: any[] | |||||
} | } | ||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
@@ -20,17 +28,57 @@ const modalSx: SxProps = { | |||||
left: "50%", | left: "50%", | ||||
transform: "translate(-50%, -50%)", | transform: "translate(-50%, -50%)", | ||||
width: "90%", | width: "90%", | ||||
maxWidth: "sm", | |||||
maxHeight: "90%", | |||||
maxWidth: "auto", | |||||
maxHeight: "auto", | |||||
padding: 3, | padding: 3, | ||||
display: "flex", | display: "flex", | ||||
flexDirection: "column", | flexDirection: "column", | ||||
gap: 2, | gap: 2, | ||||
}; | }; | ||||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, onSave }) => { | |||||
function EditToolbar(props: React.JSXElementConstructor<any> | null | undefined | any) { | |||||
// const intl = useIntl(); | |||||
// const addRecordBtn = intl.formatMessage({ id: 'add' }); | |||||
const { count, setCount, setRows, setRowModesModel, _columns } = props; | |||||
let obj: { [key: string]: string } = {}; | |||||
for (let i = 0; i < _columns.length - 1; i++) { | |||||
obj[_columns[i].field as string] = ''; | |||||
} | |||||
const handleClick = React.useCallback(() => { | |||||
const id = Math.random(); | |||||
setRows((oldRows: any) => [...oldRows, { id, ...obj, isNew: true }]); | |||||
setRowModesModel((oldModel: any) => ({ | |||||
...oldModel, | |||||
[id]: { mode: GridRowModes.Edit, | |||||
// fieldToFocus: 'material' | |||||
} | |||||
})); | |||||
setCount((prev: number) => prev+1) | |||||
}, [count, setCount, setRowModesModel, setRows]) | |||||
return ( | |||||
<GridToolbarContainer> | |||||
<Button disabled={count>=1} color="primary" startIcon={<AddIcon />} onClick={handleClick}> | |||||
{"addRecordBtn"} | |||||
</Button> | |||||
{/* <Button color="primary" startIcon={<AddIcon />} onClick={handleSave}> | |||||
SAVE | |||||
</Button> */} | |||||
</GridToolbarContainer> | |||||
); | |||||
} | |||||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, onSave, columns }) => { | |||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const { control, register, formState, trigger, watch, setValue } = useForm({}); | |||||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
const [count, setCount] = useState(0); | |||||
const [_rows, setRows] = useState(() => { | |||||
const list = getValues('salaryEffectiveInfo') | |||||
console.log(list) | |||||
return list && list.length > 0 ? list : [] | |||||
}); | |||||
const formValues = watch(); // This line of code is using the watch function from react-hook-form to get the current values of the form fields. | const formValues = watch(); // This line of code is using the watch function from react-hook-form to get the current values of the form fields. | ||||
@@ -38,14 +86,133 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
onClose(); | onClose(); | ||||
}; | }; | ||||
const handleSave = async () => { | |||||
const isValid = await trigger(); | |||||
if (isValid) { | |||||
onSave(); | |||||
onClose(); | |||||
// const handleSave = async () => { | |||||
// const isValid = await trigger(); | |||||
// // if (isValid) { | |||||
// // onSave(); | |||||
// // onClose(); | |||||
// // } | |||||
// }; | |||||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||||
params, | |||||
event, | |||||
) => { | |||||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
event.defaultMuiPrevented = true; | |||||
} | } | ||||
}; | }; | ||||
const processRowUpdate = useCallback((newRow: GridRowModel) => { | |||||
console.log(newRow) | |||||
const updatedRow = { ...newRow, updated: true }; | |||||
console.log(_rows) | |||||
if (_rows.length != 0) { | |||||
setRows(_rows?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||||
} | |||||
return updatedRow; | |||||
}, [_rows, setValue, setRows]) | |||||
useEffect(()=> { | |||||
setValue('salaryEffectiveInfo', _rows) | |||||
}, [_rows]) | |||||
const handleSaveClick = useCallback( | |||||
(id: any) => () => { | |||||
setRowModesModel((prevRowModesModel) => ({ | |||||
...prevRowModesModel, | |||||
[id]: { mode: GridRowModes.View } | |||||
})); | |||||
}, | |||||
[setRowModesModel] | |||||
); | |||||
const handleCancelClick = useCallback( | |||||
(id: any) => () => { | |||||
setRowModesModel((prevRowModesModel) => ({ | |||||
...prevRowModesModel, | |||||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||||
})); | |||||
}, | |||||
[setRowModesModel] | |||||
); | |||||
const handleEditClick = useCallback( | |||||
(id: any) => () => { | |||||
setRowModesModel((prevRowModesModel) => ({ | |||||
...prevRowModesModel, | |||||
[id]: { mode: GridRowModes.Edit } | |||||
})); | |||||
}, | |||||
[setRowModesModel] | |||||
); | |||||
const handleDeleteClick = useCallback( | |||||
(id: any) => () => { | |||||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||||
setCount((prev: number) => prev - 1) | |||||
}, | |||||
[setRows, setCount] | |||||
); | |||||
const defaultCol = useMemo( | |||||
() => ( | |||||
{ | |||||
field: 'actions', | |||||
type: 'actions', | |||||
headerName: 'edit', | |||||
width: 100, | |||||
cellClassName: 'actions', | |||||
getActions: ({ id }: { id: number }) => { | |||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||||
if (isInEditMode) { | |||||
return [ | |||||
<GridActionsCellItem | |||||
icon={<SaveIcon />} | |||||
label="Save" | |||||
key="edit" | |||||
sx={{ | |||||
color: 'primary.main' | |||||
}} | |||||
onClick={handleSaveClick(id)} | |||||
/>, | |||||
<GridActionsCellItem | |||||
icon={<CancelIcon />} | |||||
label="Cancel" | |||||
key="edit" | |||||
onClick={handleCancelClick(id)} | |||||
/> | |||||
]; | |||||
} | |||||
return [ | |||||
<GridActionsCellItem | |||||
icon={<EditIcon />} | |||||
label="Edit" | |||||
className="textPrimary" | |||||
onClick={handleEditClick(id)} | |||||
color="inherit" | |||||
key="edit" | |||||
/>, | |||||
<GridActionsCellItem | |||||
icon={<DeleteIcon />} | |||||
label="Delete" | |||||
sx={{ | |||||
color: 'error.main' | |||||
}} | |||||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||||
]; | |||||
} | |||||
} | |||||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||||
) | |||||
let _columns: any[] = [] | |||||
if (columns) { | |||||
_columns = [...columns, defaultCol] | |||||
} | |||||
useEffect(() => { | useEffect(() => { | ||||
console.log(formValues) | console.log(formValues) | ||||
}, [open]) | }, [open]) | ||||
@@ -56,30 +223,33 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
<Typography variant="h6" component="h2"> | <Typography variant="h6" component="h2"> | ||||
{t('Salary Effective Date Change')} | {t('Salary Effective Date Change')} | ||||
</Typography> | </Typography> | ||||
<FormControl> | |||||
<TextField | |||||
label={t('Salary')} | |||||
type="number" | |||||
fullWidth | |||||
{...register('salary', { | |||||
valueAsNumber: true, | |||||
required: t('Salary is required'), | |||||
})} | |||||
error={Boolean(formState.errors.salary)} | |||||
// helperText={formState.errors.salary?.message} | |||||
/> | |||||
<StyledDataGrid | |||||
rows={_rows} | |||||
columns={_columns} | |||||
editMode="row" | |||||
rowModesModel={rowModesModel} | |||||
onRowModesModelChange={setRowModesModel} | |||||
onRowEditStop={handleRowEditStop} | |||||
processRowUpdate={processRowUpdate} | |||||
slots={{ | |||||
toolbar: EditToolbar | |||||
}} | |||||
slotProps={{ | |||||
toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||||
}} | |||||
/> | |||||
<Box display="flex" justifyContent="flex-end" gap={2}> | <Box display="flex" justifyContent="flex-end" gap={2}> | ||||
<Button variant="text" onClick={handleClose}> | <Button variant="text" onClick={handleClose}> | ||||
{t('Cancel')} | {t('Cancel')} | ||||
</Button> | </Button> | ||||
<Button variant="contained" onClick={handleSave}> | |||||
<Button variant="contained" onClick={handleClose}> | |||||
{t("Save")} | {t("Save")} | ||||
</Button> | </Button> | ||||
</Box> | </Box> | ||||
</FormControl> | |||||
{/* </FormControl> */} | |||||
</Paper> | </Paper> | ||||
</Modal> | </Modal> | ||||
); | ); | ||||
}; | }; | ||||
export default SalaryEffectiveModel; | |||||
export default SalaryEffectiveModel; |
@@ -9,7 +9,7 @@ import Typography from "@mui/material/Typography"; | |||||
import { CreateGroupInputs } from "@/app/api/group/actions"; | import { CreateGroupInputs } from "@/app/api/group/actions"; | ||||
import { Controller, useFormContext } from "react-hook-form"; | import { Controller, useFormContext } from "react-hook-form"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { useCallback, useEffect, useState } from "react"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { CreateStaffInputs } from "@/app/api/staff/actions"; | import { CreateStaffInputs } from "@/app/api/staff/actions"; | ||||
import { | import { | ||||
Button, | Button, | ||||
@@ -95,6 +95,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
if (departDate) clearErrors("departDate"); | if (departDate) clearErrors("departDate"); | ||||
}, [joinDate, departDate]); | }, [joinDate, departDate]); | ||||
const salaryCols = useMemo( | |||||
() => [ | |||||
{ | |||||
field: 'salaryPoint', | |||||
headerName: 'salaryPoint', | |||||
flex: 1, | |||||
editable: true, | |||||
type: 'singleSelect', | |||||
valueOptions: combos?.salary.map(item => item.label), | |||||
// valueOptions: [], | |||||
// width: 150 | |||||
}, | |||||
{ | |||||
field: 'date', | |||||
headerName: 'date', | |||||
flex: 1, | |||||
editable: true, | |||||
type: 'date', | |||||
// width: 150 | |||||
}, | |||||
], [combos]) | |||||
return ( | return ( | ||||
<Card sx={{ display: "block" }}> | <Card sx={{ display: "block" }}> | ||||
<CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
@@ -305,6 +327,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
{...field} | {...field} | ||||
error={Boolean(errors.salaryId)} | error={Boolean(errors.salaryId)} | ||||
style={{ flex: 1, marginRight: '8px' }} | style={{ flex: 1, marginRight: '8px' }} | ||||
disabled | |||||
> | > | ||||
{combos.salary.map((salary, index) => ( | {combos.salary.map((salary, index) => ( | ||||
<MenuItem | <MenuItem | ||||
@@ -544,6 +567,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
open={salaryEffectiveModelOpen} | open={salaryEffectiveModelOpen} | ||||
onClose={closeSalaryEffectiveModel} | onClose={closeSalaryEffectiveModel} | ||||
onSave={onSalaryEffectiveSave} | onSave={onSalaryEffectiveSave} | ||||
columns={salaryCols} | |||||
/> | /> | ||||
</Card> | </Card> | ||||
); | ); | ||||