| @@ -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; | |||
| } | |||