@@ -17,21 +17,18 @@ export type teamHistory = { | |||
id: number, | |||
team: string | number, | |||
from: Date | string, | |||
to?: Date | string | |||
} | |||
export type gradeHistory = { | |||
id: number, | |||
grade: string | number, | |||
from: Date | string, | |||
to?: Date | string | |||
} | |||
export type positionHistory = { | |||
id: number, | |||
position: string | number, | |||
from: Date | string, | |||
to?: Date | string | |||
} | |||
export interface CreateStaffInputs { | |||
id?: number | |||
@@ -62,6 +59,11 @@ export interface CreateStaffInputs { | |||
delGradeHistory: number[]; | |||
positionHistory: positionHistory[]; | |||
delPositionHistory: number[]; | |||
// new modal | |||
salary: salary[]; | |||
team: team[]; | |||
grade: grade[]; | |||
position: position[]; | |||
} | |||
export interface records { | |||
@@ -69,6 +71,35 @@ export interface CreateStaffInputs { | |||
name: string; | |||
// team: Team[]; | |||
} | |||
export type DataLog = | |||
| (salary & {id: number; type: "salary"}) | |||
| (team & {id: number; type: "team"}) | |||
| (grade & {id: number; type: "grade"}) | |||
| (position & {id: number; type: "position"}) | |||
export type salary = { | |||
from: string; | |||
to: string; | |||
salaryPoint: number; | |||
} | |||
export type team = { | |||
from: string; | |||
to: string; | |||
teamId: number; | |||
} | |||
export type grade = { | |||
from: string; | |||
to: string; | |||
gradeId: number; | |||
} | |||
export type position = { | |||
from: string; | |||
to: string; | |||
positionId: number; | |||
} | |||
export interface salaryEffectiveInfo { | |||
id: number; | |||
@@ -10,9 +10,11 @@ import { | |||
} from "react-hook-form"; | |||
import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; | |||
import { Button, Stack, Typography } from "@mui/material"; | |||
import { comboProp} from "@/app/api/companys/actions"; | |||
import { comboProp } from "@/app/api/companys/actions"; | |||
import StaffInfo from "./StaffInfo"; | |||
import { Check, Close } from "@mui/icons-material"; | |||
import dayjs from "dayjs"; | |||
import { SalaryEffectiveInfo } from "@/app/api/staff"; | |||
interface Field { | |||
id: string; | |||
@@ -45,15 +47,29 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
const { t } = useTranslation(); | |||
const formProps = useForm<CreateStaffInputs>(); | |||
const [serverError, setServerError] = useState(""); | |||
const [errorMsg, setErrorMsg] = useState("An error has occurred. Please try again later.") | |||
const router = useRouter(); | |||
const [tabIndex, setTabIndex] = useState(0); | |||
// const [tabIndex, setTabIndex] = useState(0); | |||
const errors = formProps.formState.errors; | |||
function chopSalaryPoints(input: string | number): number | null { | |||
if (typeof input === 'string') { | |||
const match = input.match(/(\d+) \((\d+) - (\d+)\)/); | |||
if (match) { | |||
return parseInt(match[1], 10); | |||
} | |||
} else if (typeof input === 'number') { | |||
return input; | |||
} | |||
return null; | |||
} | |||
const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | |||
async (data) => { | |||
try { | |||
console.log(data); | |||
formProps.clearErrors() | |||
let haveError = false; | |||
const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ | |||
const regex_phone = /^\d{8}$/ | |||
@@ -96,20 +112,71 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
haveError = true | |||
formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | |||
} | |||
if (!data.salaryId) { | |||
if (data.joinDate && data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
haveError = true | |||
formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) | |||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
} | |||
if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
if (!data.salaryEffectiveInfo || data.salaryEffectiveInfo.length < 1) { | |||
haveError = true | |||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
formProps.setError("salaryId", { message: t("Please Enter Salary"), type: "required" }) | |||
} | |||
if (!data.gradeHistory || data.gradeHistory.length < 1) { | |||
haveError = true | |||
formProps.setError("gradeId", { message: t("Please Enter Grade"), type: "required" }) | |||
} | |||
if (!data.positionHistory || data.positionHistory.length < 1) { | |||
console.log("asdadsasd") | |||
haveError = true | |||
formProps.setError("currentPositionId", { message: t("Please Enter Current Position"), type: "required" }) | |||
} | |||
if (haveError) { | |||
return | |||
} | |||
const teamHistory = data.teamHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
team: combos.team.filter(team => team.label === item.team)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const gradeHistory = data.gradeHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
console.log(data.positionHistory) | |||
const positionHistory = data.positionHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
position: combos.position.filter(position => position.label === item.position)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ | |||
id: item.id, | |||
salaryPoint: chopSalaryPoints(item.salaryPoint), | |||
date: dayjs(item.date).format('YYYY-MM-DD').toString() | |||
})) // backend sort | |||
const postData: CreateStaffInputs = { | |||
// id: id, | |||
...data, | |||
salaryEffectiveInfo: salaryEffectiveInfo, | |||
teamHistory: teamHistory ?? [], | |||
gradeHistory: gradeHistory ?? [], | |||
positionHistory: positionHistory ?? [], | |||
delTeamHistory: data.delTeamHistory ? data.delTeamHistory : [], | |||
delGradeHistory: data.delGradeHistory ? data.delGradeHistory : [], | |||
delPositionHistory: data.delPositionHistory ? data.delPositionHistory : [], | |||
} | |||
console.log("passed") | |||
console.log(data) | |||
await saveStaff(data) | |||
console.log(postData) | |||
// return | |||
await saveStaff(postData) | |||
router.replace("/settings/staff") | |||
} catch (e: any) { | |||
console.log(e); | |||
@@ -118,15 +185,19 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
if (e.message === "Duplicated StaffId Found") { | |||
msg = t("Duplicated StaffId Found") | |||
} | |||
setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `); | |||
setServerError(`${t(errorMsg)} ${msg} `); | |||
} | |||
}, | |||
[router] | |||
[errorMsg, router] | |||
); | |||
const handleCancel = () => { | |||
const errorKey = Object.keys(formProps.formState.errors)[0] | |||
const err = errors[errorKey as keyof CreateStaffInputs] | |||
const handleCancel = useCallback(() => { | |||
router.back(); | |||
}; | |||
}, [router]); | |||
return ( | |||
<> | |||
@@ -141,6 +212,12 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
{serverError} | |||
</Typography> | |||
)} | |||
{err && ( | |||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||
{err.message?.toString()} | |||
</Typography> | |||
) | |||
} | |||
<StaffInfo combos={combos}/> | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
@@ -9,9 +9,10 @@ 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 { useCallback, useEffect, useMemo, useReducer, useState } from "react"; | |||
import { CreateStaffInputs } from "@/app/api/staff/actions"; | |||
import { | |||
Button, | |||
Checkbox, | |||
FormControl, | |||
InputLabel, | |||
@@ -25,11 +26,52 @@ 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"; | |||
import TableModal from "./TableModal"; | |||
import { Preview } from "@mui/icons-material"; | |||
import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel"; | |||
import TeamHistoryModal from "../EditStaff/TeamHistoryModal"; | |||
import GradeHistoryModal from "../EditStaff/GradeHistoryModal"; | |||
import PositionHistoryModal from "../EditStaff/PositionHistoryModal"; | |||
interface Props { | |||
combos: comboItem; | |||
} | |||
type tableKey = "salary" | "team" | "grade" | "position"; | |||
//// | |||
const initState = { | |||
teamModal: false, | |||
seModal: false, | |||
gradeModal: false, | |||
positionModal: false, | |||
} | |||
const enum REDUCER_ACTION_TYPE { | |||
TOGGLE_TEAM_MODAL, | |||
TOGGLE_SALARY_EFFECTIVE_MODAL, | |||
TOGGLE_GRADE_MODAL, | |||
TOGGLE_POSITION_MODAL, | |||
} | |||
type ReducerAction = { | |||
type: REDUCER_ACTION_TYPE | |||
} | |||
const reducer = (state: typeof initState, action: ReducerAction): typeof initState => { | |||
switch (action.type) { | |||
case REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL: | |||
return { ...state, teamModal: !state.teamModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL: | |||
return { ...state, seModal: !state.seModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL: | |||
return { ...state, gradeModal: !state.gradeModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL: | |||
return { ...state, positionModal: !state.positionModal }; | |||
default: | |||
return state; | |||
} | |||
} | |||
///// | |||
const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
const { | |||
t, | |||
@@ -45,6 +87,14 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
getValues, | |||
clearErrors | |||
} = useFormContext<CreateStaffInputs>(); | |||
const [tableKey, setTableKey] = useState<tableKey>() | |||
const [isOpen, setIsOpen] = useState(false) | |||
const [state, dispatch] = useReducer(reducer, initState) | |||
const toggleSeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL}) | |||
const toggleTeamModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL}) | |||
const toggleGradeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL}) | |||
const togglePositionModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL}) | |||
const employType = [ | |||
{ id: 1, label: "FT" }, | |||
@@ -64,6 +114,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
} | |||
}, [defaultValues]); | |||
const toggleModal = useCallback((key: tableKey) => { | |||
setIsOpen(!isOpen) | |||
setTableKey(key) | |||
}, [isOpen]) | |||
const joinDate = getValues("joinDate"); | |||
const departDate = getValues("departDate"); | |||
@@ -148,17 +203,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
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> | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Team")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
// error={Boolean(errors.teamId)} | |||
disabled | |||
> | |||
{combos.team.map((team, index) => ( | |||
<MenuItem key={`${team.id}-${index}`} value={team.id}> | |||
{t(team.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button | |||
variant="contained" | |||
size="small" | |||
onClick={toggleTeamModal} | |||
> | |||
{t("Team History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
@@ -191,7 +257,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel required>{t("Grade")}</InputLabel> | |||
<Controller | |||
{/* <Controller | |||
control={control} | |||
name="gradeId" | |||
render={({ field }) => ( | |||
@@ -207,6 +273,30 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
)} | |||
/> */} | |||
<Controller | |||
control={control} | |||
name="gradeId" | |||
render={({ field }) => ( | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Grade")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.gradeId)} | |||
disabled | |||
> | |||
{combos.grade.map((grade, index) => ( | |||
<MenuItem key={`${grade.id}-${index}`} value={grade.id}> | |||
{t(grade.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal}> | |||
{t("Grade History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
@@ -254,9 +344,12 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
control={control} | |||
name="currentPositionId" | |||
render={({ field }) => ( | |||
<Select | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Current Position")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
disabled | |||
error={Boolean(errors.currentPositionId)} | |||
> | |||
{combos.position.map((position, index) => ( | |||
@@ -268,6 +361,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={togglePositionModal}> | |||
{t("Position History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
@@ -275,7 +372,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel required>{t("Salary Point")}</InputLabel> | |||
<Controller | |||
{/* <Controller | |||
control={control} | |||
name="salaryId" | |||
render={({ field }) => ( | |||
@@ -294,7 +391,35 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
)} | |||
/> | |||
/> */} | |||
<Controller | |||
control={control} | |||
name="salaryId" | |||
render={({ field }) => ( | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Salary Point")} | |||
{...field} | |||
error={Boolean(errors.salaryId)} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
disabled | |||
> | |||
{combos.salary.map((salary, index) => ( | |||
<MenuItem | |||
key={`${salary.id}-${index}`} | |||
value={salary.id} | |||
> | |||
{t(salary.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
{/* <Button variant="contained" size="small" onClick={() => toggleModal("salary")}> */} | |||
<Button variant="contained" size="small" onClick={toggleSeModal}> | |||
{t("Edit Salary")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
@@ -512,6 +637,36 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</Grid> | |||
</Box> | |||
</CardContent> | |||
{state.seModal && | |||
<SalaryEffectiveModel | |||
open={state.seModal} | |||
onClose={toggleSeModal} | |||
combos={combos} | |||
// columns={salaryCols} | |||
/> | |||
} | |||
{state.teamModal && | |||
<TeamHistoryModal | |||
open={state.teamModal} | |||
onClose={toggleTeamModal} | |||
combos={combos} | |||
/> | |||
} | |||
{state.gradeModal && | |||
<GradeHistoryModal | |||
open={state.gradeModal} | |||
onClose={toggleGradeModal} | |||
combos={combos} | |||
/> | |||
} | |||
{state.positionModal && | |||
<PositionHistoryModal | |||
open={state.positionModal} | |||
onClose={togglePositionModal} | |||
combos={combos} | |||
/> | |||
} | |||
{/* {tableKey && <TableModal tableKey={tableKey} isOpen={isOpen} onClose={() => toggleModal("team")} combos={combos}/>} */} | |||
</Card> | |||
); | |||
}; | |||
@@ -88,7 +88,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
team: item.team.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delTeamHistory: [], | |||
@@ -97,7 +97,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
grade: item.grade.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delGradeHistory: [], | |||
@@ -106,7 +106,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
position: item.position.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delPositionHistory: [], | |||
@@ -180,21 +180,26 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
team: combos.team.filter(team => team.label === item.team)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const gradeHistory = data.gradeHistory.map((item) => ({ | |||
id: item.id, | |||
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const positionHistory = data.positionHistory.map((item) => ({ | |||
id: item.id, | |||
position: combos.position.filter(position => position.label === item.position)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ | |||
id: item.id, | |||
salaryPoint: chopSalaryPoints(item.salaryPoint), | |||
@@ -1,20 +1,26 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" | |||
import StyledDataGrid from "../StyledDataGrid" | |||
import { useTranslation } from "react-i18next"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
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 waitForCondition from "../utils/waitFor"; | |||
import { gradeHistory } from "@/app/api/staff/actions"; | |||
import { Add } from "@mui/icons-material"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { ProcessRowUpdateError } from "./TeamHistoryModal"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
// columns: any[] | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,119 +37,217 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type GradeModalRow = Partial< | |||
gradeHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
const thisField = "gradeHistory" | |||
const GradeHistoryModal: React.FC<Props> = ({ open, onClose, combos }) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const originalRows = getValues(thisField) | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('gradeHistory') | |||
const list: GradeModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const getRowId = useCallback<GridRowIdGetter<GradeModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}; | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]) | |||
const addRow = useCallback(() => { | |||
const id = Date.now() | |||
const newEntry = { id, _isNew: true } satisfies GradeModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "grade", | |||
} | |||
return updatedRow; | |||
})) | |||
}, []); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<GradeModalRow>) => { | |||
const errors = updateError.errors; | |||
// const prevRow = updateError.prevRow; | |||
const currRow = updateError.currRow; | |||
// if (updateError.prevRow) { | |||
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]); | |||
// } | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<GradeModalRow>, | |||
originalRow: GridRowModel<GradeModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: GradeModalRow) => row.id === newRow.id); | |||
const prevRow: GradeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return rowToSave; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
, [_rows, validateRowAndRowBefore]) | |||
const handleSaveClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
const handleCancel = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((r) => getRowId(r) === id) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('gradeHistory', _rows) | |||
// setValue(thisField, _rows) | |||
setValue('delGradeHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
const defaultCol = useMemo( | |||
() => ( | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'grade', | |||
headerName: 'grade', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.grade.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
@@ -161,42 +265,29 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
onClick={handleCancel(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" /> | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -204,25 +295,48 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
{t('GradeHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<GradeModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -231,4 +345,20 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default GradeHistoryModal |
@@ -1,20 +1,26 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" | |||
import StyledDataGrid from "../StyledDataGrid" | |||
import { useTranslation } from "react-i18next"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
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 waitForCondition from "../utils/waitFor"; | |||
import { Add } from "@mui/icons-material"; | |||
import { positionHistory } from "@/app/api/staff/actions"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { ProcessRowUpdateError } from "./TeamHistoryModal"; | |||
import { createSearchParamsBailoutProxy } from "next/dist/client/components/searchparams-bailout-proxy"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,171 +37,267 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type PositionModalRow = Partial< | |||
positionHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
const thisField = "positionHistory" | |||
const PositionHistoryModal: React.FC<Props> = ({ | |||
open, | |||
onClose, | |||
combos | |||
}) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const originalRows = getValues(thisField) | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('positionHistory') | |||
const list: PositionModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const getRowId = useCallback<GridRowIdGetter<PositionModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
console.log(originalRows) | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
}; | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<PositionModalRow>) => { | |||
const errors = updateError.errors; | |||
const prevRow = updateError.prevRow; | |||
const currRow = updateError.currRow; | |||
// if (updateError.prevRow) { | |||
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]); | |||
// } | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<PositionModalRow>, | |||
originalRow: GridRowModel<PositionModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: PositionModalRow) => row.id === newRow.id); | |||
const prevRow: PositionModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return updatedRow; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
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] | |||
); | |||
return rowToSave; | |||
} | |||
, [_rows, validateRowAndRowBefore]) | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies PositionModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "position", | |||
} | |||
})) | |||
}, [getRowId]); | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((row) => getRowId(row) === id); | |||
console.log(editedRow) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[setRows, setDelRows] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('positionHistory', _rows) | |||
setValue(thisField, _rows) | |||
setValue('delPositionHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
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)} | |||
/> | |||
]; | |||
} | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'position', | |||
headerName: 'position', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.position.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
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={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
color: 'primary.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -203,25 +305,48 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||
{t('PositionHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<PositionModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -230,4 +355,22 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default PositionHistoryModal |
@@ -1,11 +1,11 @@ | |||
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, ModalProps, Tooltip } from '@mui/material'; | |||
import { useForm, Controller, useFormContext } from 'react-hook-form'; | |||
import { useTranslation } from 'react-i18next'; | |||
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | |||
import dayjs from 'dayjs'; | |||
import { DatePicker } from '@mui/x-date-pickers'; | |||
import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid'; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditSingleSelectCell, GridCellParams, GridRowId } from '@mui/x-data-grid'; | |||
import StyledDataGrid from '../StyledDataGrid'; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
@@ -14,12 +14,17 @@ import CancelIcon from '@mui/icons-material/Cancel'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import { GridActionsCellItem } from '@mui/x-data-grid'; | |||
import waitForCondition from '../utils/waitFor'; | |||
import { StaffEntryError, validateRowAndRowBefore } from './validateDates'; | |||
import { salaryEffectiveInfo } from '@/app/api/staff/actions'; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { Add } from '@mui/icons-material'; | |||
import { GridEditDateCell } from '@mui/x-data-grid'; | |||
interface SalaryEffectiveModelProps { | |||
open: boolean; | |||
onClose: () => void; | |||
modalSx?: SxProps; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -36,125 +41,133 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
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 color="primary" startIcon={<AddIcon />} onClick={handleClick}> | |||
{"addRecordBtn"} | |||
</Button> | |||
{/* <Button color="primary" startIcon={<AddIcon />} onClick={handleSave}> | |||
SAVE | |||
</Button> */} | |||
</GridToolbarContainer> | |||
); | |||
} | |||
export type SeModalRow = Partial< | |||
salaryEffectiveInfo & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
} | |||
> | |||
export class ProcessRowUpdateError<T> extends Error { | |||
public readonly prevRow: T | null; | |||
public readonly currRow: T; | |||
public readonly errors: StaffEntryError | undefined; | |||
constructor( | |||
prevRow: T | null, | |||
currRow: T, | |||
message?: string, | |||
errors?: StaffEntryError, | |||
) { | |||
super(message); | |||
this.prevRow = prevRow; | |||
this.currRow = currRow; | |||
this.errors = errors; | |||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, columns }) => { | |||
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
} | |||
} | |||
const thisField = "salaryEffectiveInfo" | |||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, combos }) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('salaryEffectiveInfo') | |||
const list: SeModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
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 getRowId = useCallback<GridRowIdGetter<SeModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
return saved | |||
}, [rowModesModel]) | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
// const handleSave = async () => { | |||
// const isValid = await trigger(); | |||
// // if (isValid) { | |||
// // onSave(); | |||
// // onClose(); | |||
// // } | |||
// }; | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<SeModalRow>) => { | |||
const errors = updateError.errors; | |||
const currRow = updateError.currRow; | |||
console.log(errors) | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<SeModalRow>, | |||
originalRow: GridRowModel<SeModalRow> | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
const rowIndex = _rows.findIndex((row: SeModalRow) => row.id === newRow.id); | |||
const prevRow: SeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
_isNew, | |||
} | |||
}; | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.date || !b.date) return 0; | |||
return new Date(a.date).getTime() - new Date(b.date).getTime(); | |||
})); | |||
} | |||
return updatedRow; | |||
return rowToSave; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
, [validateRowAndRowBefore, _rows]) | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('salaryEffectiveInfo', _rows) | |||
}, [_rows]) | |||
// useEffect(()=> { | |||
// console.log(_rows) | |||
// setValue(thisField, _rows) | |||
// }, [_rows]) | |||
useEffect(()=> { | |||
console.log(_delRows) | |||
setValue('delSalaryEffectiveInfo', _delRows) | |||
}, [_delRows]) | |||
const handleSaveClick = useCallback((id: any) => () => { | |||
const handleSave = useCallback((id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
@@ -163,17 +176,8 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
[setRowModesModel] | |||
); | |||
const handleSaveClickAsync = useCallback(async(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
(id: any) => () => { | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
@@ -182,27 +186,56 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row: any) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRowModesModel] | |||
[] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
); | |||
const defaultCol = useMemo( | |||
() => ( | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'salaryPoint', | |||
headerName: 'salaryPoint', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.salary.map((item) => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'date', | |||
headerName: 'date', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
@@ -220,47 +253,54 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
onClick={handleCancel(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" /> | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies SeModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "team", | |||
} | |||
})) | |||
}, [getRowId]); | |||
useEffect(() => { | |||
console.log(_rows) | |||
}, [_rows]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
@@ -269,33 +309,70 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
{t('Salary Effective Date Change')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
processRowUpdate={processRowUpdate} | |||
getCellClassName={(params: GridCellParams<SeModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
toolbar: EditToolbar | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Cancel')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
{/* <Button variant="contained" onClick={handleSaveAll}> */} | |||
{t("Save")} | |||
</Button> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
{/* </FormControl> */} | |||
</Paper> | |||
</Modal> | |||
); | |||
}; | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default SalaryEffectiveModel; |
@@ -113,10 +113,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
useEffect(() => { | |||
resetStaff() | |||
}, [defaultValues]); | |||
// useEffect(() => { | |||
// console.log(state) | |||
// }, [state]); | |||
const joinDate = watch("joinDate"); | |||
const departDate = watch("departDate"); | |||
@@ -126,112 +122,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
if (departDate) clearErrors("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]) | |||
const teamHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'team', | |||
headerName: 'team', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.team.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
const gradeHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'grade', | |||
headerName: 'grade', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.grade.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
const positionHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'position', | |||
headerName: 'position', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.position.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
return ( | |||
<Card sx={{ display: "block" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
@@ -311,6 +201,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
label={t("Team")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
disabled | |||
// error={Boolean(errors.teamId)} | |||
> | |||
{combos.team.map((team, index) => ( | |||
@@ -320,7 +211,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleTeamModal} | |||
disabled={getValues("teamHistory").length == 0} | |||
// disabled={getValues("teamHistory").length == 0} | |||
> | |||
{t("Team History")} | |||
</Button> | |||
@@ -367,6 +258,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.gradeId)} | |||
disabled | |||
> | |||
{combos.grade.map((grade, index) => ( | |||
<MenuItem key={`${grade.id}-${index}`} value={grade.id}> | |||
@@ -374,8 +266,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal} | |||
disabled={getValues("gradeHistory").length == 0}> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal}> | |||
{t("Grade History")} | |||
</Button> | |||
</Box> | |||
@@ -432,6 +323,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.currentPositionId)} | |||
disabled | |||
> | |||
{combos.position.map((position, index) => ( | |||
<MenuItem | |||
@@ -442,8 +334,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={togglePositionModal} | |||
disabled={getValues("positionHistory").length == 0}> | |||
<Button variant="contained" size="small" onClick={togglePositionModal}> | |||
{t("Position History")} | |||
</Button> | |||
</Box> | |||
@@ -656,7 +547,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
label={t("Depart Date")} | |||
value={departDate ? dayjs(departDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
if (!date) { | |||
setValue("departDate", null); | |||
return | |||
}; | |||
dayjs(date).add(1, 'month') | |||
setValue("departDate", date.format(INPUT_DATE_FORMAT)); | |||
}} | |||
@@ -707,28 +601,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<SalaryEffectiveModel | |||
open={state.seModal} | |||
onClose={toggleSeModal} | |||
columns={salaryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.teamModal && | |||
<TeamHistoryModal | |||
open={state.teamModal} | |||
onClose={toggleTeamModal} | |||
columns={teamHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.gradeModal && | |||
<GradeHistoryModal | |||
open={state.gradeModal} | |||
onClose={toggleGradeModal} | |||
columns={gradeHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.positionModal && | |||
<PositionHistoryModal | |||
open={state.positionModal} | |||
onClose={togglePositionModal} | |||
columns={positionHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
</Card> | |||
@@ -1,20 +1,22 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material"; | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material"; | |||
import { useTranslation } from "react-i18next"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from "@mui/x-data-grid"; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditInputCell, GridColDef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
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 { useFormContext } from "react-hook-form"; | |||
import { useForm, useFormContext } from "react-hook-form"; | |||
import { Add } from "@mui/icons-material"; | |||
import { CreateStaffInputs, teamHistory } from "@/app/api/staff/actions"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import waitForCondition from "../utils/waitFor"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,172 +33,279 @@ interface Props { | |||
gap: 2, | |||
}; | |||
const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type TeamModalRow = Partial< | |||
teamHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
export class ProcessRowUpdateError<T> extends Error { | |||
public readonly prevRow: T | null; | |||
public readonly currRow: T; | |||
public readonly errors: StaffEntryError | undefined; | |||
constructor( | |||
prevRow: T | null, | |||
currRow: T, | |||
message?: string, | |||
errors?: StaffEntryError, | |||
) { | |||
super(message); | |||
this.prevRow = prevRow; | |||
this.currRow = currRow; | |||
this.errors = errors; | |||
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
} | |||
} | |||
const thisField = "teamHistory" | |||
const TeamHistoryModal: React.FC<Props> = ({ | |||
open, | |||
onClose, | |||
combos, | |||
}) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('teamHistory') | |||
const list: TeamModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const getRowId = useCallback<GridRowIdGetter<TeamModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<TeamModalRow>) => { | |||
const errors = updateError.errors; | |||
const currRow = updateError.currRow; | |||
console.log(errors) | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<TeamModalRow>, | |||
originalRow: GridRowModel<TeamModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: TeamModalRow) => row.id === newRow.id); | |||
const prevRow: TeamModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
if (_rows.length != 0) { | |||
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return rowToSave; | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
} | |||
}; | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
, [validateRowAndRowBefore, _rows]) | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies TeamModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "team", | |||
} | |||
return updatedRow; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
})) | |||
}, [getRowId]); | |||
const handleSaveClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((row) => getRowId(row) === id); | |||
console.log(editedRow) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('teamHistory', _rows) | |||
// setValue(thisField, _rows) | |||
setValue('delTeamHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
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)} | |||
/> | |||
]; | |||
} | |||
const columns = useMemo<GridColDef[]>( | |||
() => [ | |||
{ | |||
field: 'team', | |||
headerName: 'team', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.team.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
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={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
color: 'primary.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -204,25 +313,48 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
{t('TeamHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<TeamModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -231,4 +363,20 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default TeamHistoryModal |
@@ -0,0 +1,61 @@ | |||
import { gradeHistory, positionHistory, salaryEffectiveInfo, teamHistory } from "@/app/api/staff/actions"; | |||
import { GridRowId, GridRowModel } from "@mui/x-data-grid"; | |||
import dayjs, { Dayjs } from "dayjs"; | |||
import { TeamModalRow } from "./TeamHistoryModal"; | |||
import { GradeModalRow } from "./GradeHistoryModal"; | |||
import { PositionModalRow } from "./PositionHistoryModal"; | |||
import { SeModalRow } from "./SalaryEffectiveModel"; | |||
export type ValidateError = { | |||
from: number[] | |||
to: number[] | |||
} | |||
type RowModel = Partial<TeamModalRow & GradeModalRow & PositionModalRow & SeModalRow> | |||
type AllFields = Partial<teamHistory & gradeHistory & positionHistory & salaryEffectiveInfo> | |||
export type StaffEntryError = { | |||
[field in keyof AllFields]?: string; | |||
}; | |||
export const validateRowAndRowBefore = ( | |||
prevRow: RowModel | null, | |||
currRow: RowModel | |||
): StaffEntryError | undefined => { | |||
const error: StaffEntryError = {} | |||
if (prevRow) { | |||
if ('from' in currRow && currRow.from !== undefined) { | |||
if (dayjs(prevRow.from).diff(dayjs(currRow.from)) == 0) { | |||
error.from = "The date should not be the same as last entry" | |||
} | |||
} else if ('date' in currRow && currRow.date !== undefined) { | |||
if (dayjs(prevRow.date).diff(dayjs(currRow.date)) == 0) { | |||
error.date = "The date should not be the same as last entry" | |||
} | |||
} | |||
} | |||
console.log(currRow) | |||
if ('from' in currRow && !currRow.from) { | |||
error.from = "The date cannot be empty" | |||
} | |||
if ('date' in currRow && !currRow.date) { | |||
error.date = "The date cannot be empty" | |||
} | |||
// Check specific fields based on row type | |||
if ('grade' in currRow && !currRow.grade) { | |||
error.grade = "Grade cannot be empty" | |||
} | |||
if ('position' in currRow && !currRow.position) { | |||
error.position = "Position cannot be empty" | |||
} | |||
if ('team' in currRow && !currRow.team) { | |||
error.team = "Team cannot be empty" | |||
} | |||
console.log("error") | |||
console.log(error) | |||
console.log(currRow.from) | |||
return Object.keys(error).length > 0 ? error : undefined; | |||
} |