| @@ -17,21 +17,18 @@ export type teamHistory = { | |||||
| id: number, | id: number, | ||||
| team: string | number, | team: string | number, | ||||
| from: Date | string, | from: Date | string, | ||||
| to?: Date | string | |||||
| } | } | ||||
| export type gradeHistory = { | export type gradeHistory = { | ||||
| id: number, | id: number, | ||||
| grade: string | number, | grade: string | number, | ||||
| from: Date | string, | from: Date | string, | ||||
| to?: Date | string | |||||
| } | } | ||||
| export type positionHistory = { | export type positionHistory = { | ||||
| id: number, | id: number, | ||||
| position: string | number, | position: string | number, | ||||
| from: Date | string, | from: Date | string, | ||||
| to?: Date | string | |||||
| } | } | ||||
| export interface CreateStaffInputs { | export interface CreateStaffInputs { | ||||
| id?: number | id?: number | ||||
| @@ -62,6 +59,11 @@ export interface CreateStaffInputs { | |||||
| delGradeHistory: number[]; | delGradeHistory: number[]; | ||||
| positionHistory: positionHistory[]; | positionHistory: positionHistory[]; | ||||
| delPositionHistory: number[]; | delPositionHistory: number[]; | ||||
| // new modal | |||||
| salary: salary[]; | |||||
| team: team[]; | |||||
| grade: grade[]; | |||||
| position: position[]; | |||||
| } | } | ||||
| export interface records { | export interface records { | ||||
| @@ -69,6 +71,35 @@ export interface CreateStaffInputs { | |||||
| name: string; | name: string; | ||||
| // team: Team[]; | // 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 { | export interface salaryEffectiveInfo { | ||||
| id: number; | id: number; | ||||
| @@ -10,9 +10,11 @@ import { | |||||
| } from "react-hook-form"; | } from "react-hook-form"; | ||||
| import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; | import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; | ||||
| import { Button, Stack, Typography } from "@mui/material"; | 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 StaffInfo from "./StaffInfo"; | ||||
| import { Check, Close } from "@mui/icons-material"; | import { Check, Close } from "@mui/icons-material"; | ||||
| import dayjs from "dayjs"; | |||||
| import { SalaryEffectiveInfo } from "@/app/api/staff"; | |||||
| interface Field { | interface Field { | ||||
| id: string; | id: string; | ||||
| @@ -45,15 +47,29 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const formProps = useForm<CreateStaffInputs>(); | const formProps = useForm<CreateStaffInputs>(); | ||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| const [errorMsg, setErrorMsg] = useState("An error has occurred. Please try again later.") | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| // const [tabIndex, setTabIndex] = useState(0); | |||||
| const errors = formProps.formState.errors; | 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>>( | const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | ||||
| async (data) => { | async (data) => { | ||||
| try { | try { | ||||
| console.log(data); | console.log(data); | ||||
| formProps.clearErrors() | |||||
| let haveError = false; | let haveError = false; | ||||
| const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ | const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ | ||||
| const regex_phone = /^\d{8}$/ | const regex_phone = /^\d{8}$/ | ||||
| @@ -96,20 +112,71 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
| haveError = true | haveError = true | ||||
| formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | 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 | 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 | 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) { | if (haveError) { | ||||
| return | 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("passed") | ||||
| console.log(data) | |||||
| await saveStaff(data) | |||||
| console.log(postData) | |||||
| // return | |||||
| await saveStaff(postData) | |||||
| router.replace("/settings/staff") | router.replace("/settings/staff") | ||||
| } catch (e: any) { | } catch (e: any) { | ||||
| console.log(e); | console.log(e); | ||||
| @@ -118,15 +185,19 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
| if (e.message === "Duplicated StaffId Found") { | if (e.message === "Duplicated StaffId Found") { | ||||
| msg = t("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.back(); | ||||
| }; | |||||
| }, [router]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -141,6 +212,12 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
| {serverError} | {serverError} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| {err && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {err.message?.toString()} | |||||
| </Typography> | |||||
| ) | |||||
| } | |||||
| <StaffInfo combos={combos}/> | <StaffInfo combos={combos}/> | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| @@ -9,9 +9,10 @@ import Typography from "@mui/material/Typography"; | |||||
| import { CreateGroupInputs } from "@/app/api/group/actions"; | import { CreateGroupInputs } from "@/app/api/group/actions"; | ||||
| import { Controller, useFormContext } from "react-hook-form"; | import { Controller, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useCallback, useEffect } from "react"; | |||||
| import { useCallback, useEffect, useMemo, useReducer, useState } from "react"; | |||||
| import { CreateStaffInputs } from "@/app/api/staff/actions"; | import { CreateStaffInputs } from "@/app/api/staff/actions"; | ||||
| import { | import { | ||||
| Button, | |||||
| Checkbox, | Checkbox, | ||||
| FormControl, | FormControl, | ||||
| InputLabel, | InputLabel, | ||||
| @@ -25,11 +26,52 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
| import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import 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 { | interface Props { | ||||
| combos: comboItem; | 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 StaffInfo: React.FC<Props> = ({ combos }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -45,6 +87,14 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| getValues, | getValues, | ||||
| clearErrors | clearErrors | ||||
| } = useFormContext<CreateStaffInputs>(); | } = 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 = [ | const employType = [ | ||||
| { id: 1, label: "FT" }, | { id: 1, label: "FT" }, | ||||
| @@ -64,6 +114,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| } | } | ||||
| }, [defaultValues]); | }, [defaultValues]); | ||||
| const toggleModal = useCallback((key: tableKey) => { | |||||
| setIsOpen(!isOpen) | |||||
| setTableKey(key) | |||||
| }, [isOpen]) | |||||
| const joinDate = getValues("joinDate"); | const joinDate = getValues("joinDate"); | ||||
| const departDate = getValues("departDate"); | const departDate = getValues("departDate"); | ||||
| @@ -148,17 +203,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| control={control} | control={control} | ||||
| name="teamId" | name="teamId" | ||||
| render={({ field }) => ( | 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> | </FormControl> | ||||
| @@ -191,7 +257,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <InputLabel required>{t("Grade")}</InputLabel> | <InputLabel required>{t("Grade")}</InputLabel> | ||||
| <Controller | |||||
| {/* <Controller | |||||
| control={control} | control={control} | ||||
| name="gradeId" | name="gradeId" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| @@ -207,6 +273,30 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| ))} | ))} | ||||
| </Select> | </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> | </FormControl> | ||||
| </Grid> | </Grid> | ||||
| @@ -254,9 +344,12 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| control={control} | control={control} | ||||
| name="currentPositionId" | name="currentPositionId" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <Select | |||||
| <Box display="flex" justifyContent="space-between" alignItems="center"> | |||||
| <Select | |||||
| label={t("Current Position")} | label={t("Current Position")} | ||||
| style={{ flex: 1, marginRight: '8px' }} | |||||
| {...field} | {...field} | ||||
| disabled | |||||
| error={Boolean(errors.currentPositionId)} | error={Boolean(errors.currentPositionId)} | ||||
| > | > | ||||
| {combos.position.map((position, index) => ( | {combos.position.map((position, index) => ( | ||||
| @@ -268,6 +361,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </Select> | </Select> | ||||
| <Button variant="contained" size="small" onClick={togglePositionModal}> | |||||
| {t("Position History")} | |||||
| </Button> | |||||
| </Box> | |||||
| )} | )} | ||||
| /> | /> | ||||
| </FormControl> | </FormControl> | ||||
| @@ -275,7 +372,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <InputLabel required>{t("Salary Point")}</InputLabel> | <InputLabel required>{t("Salary Point")}</InputLabel> | ||||
| <Controller | |||||
| {/* <Controller | |||||
| control={control} | control={control} | ||||
| name="salaryId" | name="salaryId" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| @@ -294,7 +391,35 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| ))} | ))} | ||||
| </Select> | </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> | </FormControl> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| @@ -512,6 +637,36 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> | ||||
| </CardContent> | </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> | </Card> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -88,7 +88,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||||
| id: item.id, | id: item.id, | ||||
| team: item.team.name, | team: item.team.name, | ||||
| from: dayjs(item.from.join()).toDate(), | from: dayjs(item.from.join()).toDate(), | ||||
| to: item.to ? dayjs(item.to.join()).toDate() : "", | |||||
| to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||||
| }) | }) | ||||
| }) : [], | }) : [], | ||||
| delTeamHistory: [], | delTeamHistory: [], | ||||
| @@ -97,7 +97,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||||
| id: item.id, | id: item.id, | ||||
| grade: item.grade.name, | grade: item.grade.name, | ||||
| from: dayjs(item.from.join()).toDate(), | from: dayjs(item.from.join()).toDate(), | ||||
| to: item.to ? dayjs(item.to.join()).toDate() : "", | |||||
| to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||||
| }) | }) | ||||
| }) : [], | }) : [], | ||||
| delGradeHistory: [], | delGradeHistory: [], | ||||
| @@ -106,7 +106,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||||
| id: item.id, | id: item.id, | ||||
| position: item.position.name, | position: item.position.name, | ||||
| from: dayjs(item.from.join()).toDate(), | from: dayjs(item.from.join()).toDate(), | ||||
| to: item.to ? dayjs(item.to.join()).toDate() : "", | |||||
| to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||||
| }) | }) | ||||
| }) : [], | }) : [], | ||||
| delPositionHistory: [], | delPositionHistory: [], | ||||
| @@ -180,21 +180,26 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||||
| id: item.id, | id: item.id, | ||||
| team: combos.team.filter(team => team.label === item.team)[0].id, | team: combos.team.filter(team => team.label === item.team)[0].id, | ||||
| from: dayjs(item.from).format('YYYY-MM-DD'), | 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) => ({ | const gradeHistory = data.gradeHistory.map((item) => ({ | ||||
| id: item.id, | id: item.id, | ||||
| grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | ||||
| from: dayjs(item.from).format('YYYY-MM-DD'), | 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) => ({ | const positionHistory = data.positionHistory.map((item) => ({ | ||||
| id: item.id, | id: item.id, | ||||
| position: combos.position.filter(position => position.label === item.position)[0].id, | position: combos.position.filter(position => position.label === item.position)[0].id, | ||||
| from: dayjs(item.from).format('YYYY-MM-DD'), | 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) => ({ | const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ | ||||
| id: item.id, | id: item.id, | ||||
| salaryPoint: chopSalaryPoints(item.salaryPoint), | 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 StyledDataGrid from "../StyledDataGrid" | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | 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 AddIcon from '@mui/icons-material/Add'; | ||||
| import SaveIcon from '@mui/icons-material/Save'; | import SaveIcon from '@mui/icons-material/Save'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import CancelIcon from '@mui/icons-material/Cancel'; | import CancelIcon from '@mui/icons-material/Cancel'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | import EditIcon from '@mui/icons-material/Edit'; | ||||
| import waitForCondition from "../utils/waitFor"; | 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 { | interface Props { | ||||
| open: boolean; | open: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| columns: any[] | |||||
| combos: comboItem; | |||||
| // columns: any[] | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -31,119 +37,217 @@ const modalSx: SxProps = { | |||||
| gap: 2, | 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 { | const { | ||||
| t, | t, | ||||
| // i18n: { language }, | // i18n: { language }, | ||||
| } = useTranslation(); | } = useTranslation(); | ||||
| const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const [count, setCount] = useState(0); | |||||
| const apiRef = useGridApiRef() | |||||
| const originalRows = getValues(thisField) | |||||
| const [_rows, setRows] = useState(() => { | const [_rows, setRows] = useState(() => { | ||||
| const list = getValues('gradeHistory') | |||||
| const list: GradeModalRow[] = getValues(thisField) | |||||
| return list && list.length > 0 ? list : [] | return list && list.length > 0 ? list : [] | ||||
| }); | }); | ||||
| const [_delRows, setDelRows] = useState<number[]>([]); | 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, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View } | [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) => () => { | (id: any) => () => { | ||||
| setRowModesModel((prevRowModesModel) => ({ | setRowModesModel((prevRowModesModel) => ({ | ||||
| ...prevRowModesModel, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View, ignoreModifications: true } | [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(()=> { | useEffect(()=> { | ||||
| console.log(_rows) | console.log(_rows) | ||||
| setValue('gradeHistory', _rows) | |||||
| // setValue(thisField, _rows) | |||||
| setValue('delGradeHistory', _delRows) | setValue('delGradeHistory', _delRows) | ||||
| }, [_rows, _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', | field: 'actions', | ||||
| type: 'actions', | type: 'actions', | ||||
| @@ -161,42 +265,29 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||||
| sx={{ | sx={{ | ||||
| color: 'primary.main' | color: 'primary.main' | ||||
| }} | }} | ||||
| onClick={handleSaveClick(id)} | |||||
| onClick={handleSave(id)} | |||||
| />, | />, | ||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<CancelIcon />} | icon={<CancelIcon />} | ||||
| label="Cancel" | label="Cancel" | ||||
| key="edit" | key="edit" | ||||
| onClick={handleCancelClick(id)} | |||||
| onClick={handleCancel(id)} | |||||
| /> | /> | ||||
| ]; | ]; | ||||
| } | } | ||||
| return [ | return [ | ||||
| <GridActionsCellItem | |||||
| icon={<EditIcon />} | |||||
| label="Edit" | |||||
| className="textPrimary" | |||||
| onClick={handleEditClick(id)} | |||||
| color="inherit" | |||||
| key="edit" | |||||
| />, | |||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<DeleteIcon />} | icon={<DeleteIcon />} | ||||
| label="Delete" | label="Delete" | ||||
| sx={{ | sx={{ | ||||
| color: 'error.main' | 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 ( | return ( | ||||
| <Modal open={open} onClose={handleClose}> | <Modal open={open} onClose={handleClose}> | ||||
| <Paper sx={{ ...modalSx }}> | <Paper sx={{ ...modalSx }}> | ||||
| @@ -204,25 +295,48 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||||
| {t('GradeHistoryModal')} | {t('GradeHistoryModal')} | ||||
| </Typography> | </Typography> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| getRowId={getRowId} | |||||
| apiRef={apiRef} | |||||
| rows={_rows} | rows={_rows} | ||||
| columns={_columns} | |||||
| columns={columns} | |||||
| editMode="row" | 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} | rowModesModel={rowModesModel} | ||||
| onRowModesModelChange={setRowModesModel} | onRowModesModelChange={setRowModesModel} | ||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | 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}> | <Box display="flex" justifyContent="flex-end" gap={2}> | ||||
| <Button variant="text" onClick={handleClose}> | |||||
| {t('Cancel')} | |||||
| <Button variant="text" onClick={onCancel}> | |||||
| {t('Close')} | |||||
| </Button> | </Button> | ||||
| <Button variant="contained" onClick={bigTesting}> | |||||
| <Button variant="contained" onClick={doSave}> | |||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -231,4 +345,20 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||||
| </Modal> | </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 | 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 StyledDataGrid from "../StyledDataGrid" | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | 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 AddIcon from '@mui/icons-material/Add'; | ||||
| import SaveIcon from '@mui/icons-material/Save'; | import SaveIcon from '@mui/icons-material/Save'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import CancelIcon from '@mui/icons-material/Cancel'; | import CancelIcon from '@mui/icons-material/Cancel'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | import EditIcon from '@mui/icons-material/Edit'; | ||||
| import waitForCondition from "../utils/waitFor"; | 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 { | interface Props { | ||||
| open: boolean; | open: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| columns: any[] | |||||
| combos: comboItem; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -31,171 +37,267 @@ const modalSx: SxProps = { | |||||
| gap: 2, | 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 { | const { | ||||
| t, | t, | ||||
| // i18n: { language }, | // i18n: { language }, | ||||
| } = useTranslation(); | } = useTranslation(); | ||||
| const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const [count, setCount] = useState(0); | |||||
| const apiRef = useGridApiRef() | |||||
| const originalRows = getValues(thisField) | |||||
| const [_rows, setRows] = useState(() => { | const [_rows, setRows] = useState(() => { | ||||
| const list = getValues('positionHistory') | |||||
| const list: PositionModalRow[] = getValues(thisField) | |||||
| return list && list.length > 0 ? list : [] | return list && list.length > 0 ? list : [] | ||||
| }); | }); | ||||
| const [_delRows, setDelRows] = useState<number[]>([]); | 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, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View } | [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 | // 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) | console.log(_rows) | ||||
| if (_rows.length != 0) { | 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(()=> { | useEffect(()=> { | ||||
| console.log(_rows) | console.log(_rows) | ||||
| setValue('positionHistory', _rows) | |||||
| setValue(thisField, _rows) | |||||
| setValue('delPositionHistory', _delRows) | setValue('delPositionHistory', _delRows) | ||||
| }, [_rows, _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 [ | return [ | ||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<EditIcon />} | |||||
| label="Edit" | |||||
| className="textPrimary" | |||||
| onClick={handleEditClick(id)} | |||||
| color="inherit" | |||||
| icon={<SaveIcon />} | |||||
| label="Save" | |||||
| key="edit" | key="edit" | ||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<DeleteIcon />} | |||||
| label="Delete" | |||||
| sx={{ | 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 ( | return ( | ||||
| <Modal open={open} onClose={handleClose}> | <Modal open={open} onClose={handleClose}> | ||||
| <Paper sx={{ ...modalSx }}> | <Paper sx={{ ...modalSx }}> | ||||
| @@ -203,25 +305,48 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||||
| {t('PositionHistoryModal')} | {t('PositionHistoryModal')} | ||||
| </Typography> | </Typography> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| getRowId={getRowId} | |||||
| apiRef={apiRef} | |||||
| rows={_rows} | rows={_rows} | ||||
| columns={_columns} | |||||
| columns={columns} | |||||
| editMode="row" | 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} | rowModesModel={rowModesModel} | ||||
| onRowModesModelChange={setRowModesModel} | onRowModesModelChange={setRowModesModel} | ||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | 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}> | <Box display="flex" justifyContent="flex-end" gap={2}> | ||||
| <Button variant="text" onClick={handleClose}> | |||||
| {t('Cancel')} | |||||
| <Button variant="text" onClick={onCancel}> | |||||
| {t('Close')} | |||||
| </Button> | </Button> | ||||
| <Button variant="contained" onClick={bigTesting}> | |||||
| <Button variant="contained" onClick={doSave}> | |||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -230,4 +355,22 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||||
| </Modal> | </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 | export default PositionHistoryModal | ||||
| @@ -1,11 +1,11 @@ | |||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material'; | |||||
| import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps, ModalProps, Tooltip } from '@mui/material'; | |||||
| import { useForm, Controller, useFormContext } from 'react-hook-form'; | import { useForm, Controller, useFormContext } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | ||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import { DatePicker } from '@mui/x-date-pickers'; | import { DatePicker } from '@mui/x-date-pickers'; | ||||
| import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid'; | |||||
| import { GridRenderEditCellParams, FooterPropsOverrides, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditSingleSelectCell, GridCellParams, GridRowId } from '@mui/x-data-grid'; | |||||
| import StyledDataGrid from '../StyledDataGrid'; | import StyledDataGrid from '../StyledDataGrid'; | ||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||
| import SaveIcon from '@mui/icons-material/Save'; | 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 EditIcon from '@mui/icons-material/Edit'; | ||||
| import { GridActionsCellItem } from '@mui/x-data-grid'; | import { GridActionsCellItem } from '@mui/x-data-grid'; | ||||
| import waitForCondition from '../utils/waitFor'; | 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 { | interface SalaryEffectiveModelProps { | ||||
| open: boolean; | open: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| modalSx?: SxProps; | modalSx?: SxProps; | ||||
| columns: any[] | |||||
| combos: comboItem; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -36,125 +41,133 @@ const modalSx: SxProps = { | |||||
| gap: 2, | 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 { | const { | ||||
| t, | t, | ||||
| // i18n: { language }, | // i18n: { language }, | ||||
| } = useTranslation(); | } = useTranslation(); | ||||
| const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const [count, setCount] = useState(0); | |||||
| const apiRef = useGridApiRef() | |||||
| const [_rows, setRows] = useState(() => { | const [_rows, setRows] = useState(() => { | ||||
| const list = getValues('salaryEffectiveInfo') | |||||
| const list: SeModalRow[] = getValues(thisField) | |||||
| return list && list.length > 0 ? list : [] | return list && list.length > 0 ? list : [] | ||||
| }); | }); | ||||
| const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||||
| const [_delRows, setDelRows] = useState<number[]>([]); | 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) | console.log(_rows) | ||||
| if (_rows.length != 0) { | 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(()=> { | useEffect(()=> { | ||||
| console.log(_delRows) | console.log(_delRows) | ||||
| setValue('delSalaryEffectiveInfo', _delRows) | setValue('delSalaryEffectiveInfo', _delRows) | ||||
| }, [_delRows]) | }, [_delRows]) | ||||
| const handleSaveClick = useCallback((id: any) => () => { | |||||
| const handleSave = useCallback((id: GridRowId) => () => { | |||||
| setRowModesModel((prevRowModesModel) => ({ | setRowModesModel((prevRowModesModel) => ({ | ||||
| ...prevRowModesModel, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View } | [id]: { mode: GridRowModes.View } | ||||
| @@ -163,17 +176,8 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
| [setRowModesModel] | [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) => ({ | setRowModesModel((prevRowModesModel) => ({ | ||||
| ...prevRowModesModel, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View, ignoreModifications: true } | [id]: { mode: GridRowModes.View, ignoreModifications: true } | ||||
| @@ -182,27 +186,56 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
| [setRowModesModel] | [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', | field: 'actions', | ||||
| type: 'actions', | type: 'actions', | ||||
| @@ -220,47 +253,54 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
| sx={{ | sx={{ | ||||
| color: 'primary.main' | color: 'primary.main' | ||||
| }} | }} | ||||
| onClick={handleSaveClick(id)} | |||||
| onClick={handleSave(id)} | |||||
| />, | />, | ||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<CancelIcon />} | icon={<CancelIcon />} | ||||
| label="Cancel" | label="Cancel" | ||||
| key="edit" | key="edit" | ||||
| onClick={handleCancelClick(id)} | |||||
| onClick={handleCancel(id)} | |||||
| /> | /> | ||||
| ]; | ]; | ||||
| } | } | ||||
| return [ | return [ | ||||
| <GridActionsCellItem | |||||
| icon={<EditIcon />} | |||||
| label="Edit" | |||||
| className="textPrimary" | |||||
| onClick={handleEditClick(id)} | |||||
| color="inherit" | |||||
| key="edit" | |||||
| />, | |||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<DeleteIcon />} | icon={<DeleteIcon />} | ||||
| label="Delete" | label="Delete" | ||||
| sx={{ | sx={{ | ||||
| color: 'error.main' | 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 ( | return ( | ||||
| <Modal open={open} onClose={handleClose}> | <Modal open={open} onClose={handleClose}> | ||||
| @@ -269,33 +309,70 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||||
| {t('Salary Effective Date Change')} | {t('Salary Effective Date Change')} | ||||
| </Typography> | </Typography> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| getRowId={getRowId} | |||||
| apiRef={apiRef} | |||||
| rows={_rows} | rows={_rows} | ||||
| columns={_columns} | |||||
| columns={columns} | |||||
| editMode="row" | 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} | rowModesModel={rowModesModel} | ||||
| onRowModesModelChange={setRowModesModel} | onRowModesModelChange={setRowModesModel} | ||||
| onRowEditStop={handleRowEditStop} | |||||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||||
| processRowUpdate={processRowUpdate} | processRowUpdate={processRowUpdate} | ||||
| getCellClassName={(params: GridCellParams<SeModalRow>) => { | |||||
| let classname = ""; | |||||
| if (params.row._error) { | |||||
| classname = "hasError" | |||||
| } | |||||
| return classname; | |||||
| }} | |||||
| slots={{ | slots={{ | ||||
| toolbar: EditToolbar | |||||
| footer: FooterToolbar, | |||||
| noRowsOverlay: NoRowsOverlay, | |||||
| }} | }} | ||||
| slotProps={{ | slotProps={{ | ||||
| toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||||
| footer: { child: footer }, | |||||
| }} | }} | ||||
| /> | /> | ||||
| <Box display="flex" justifyContent="flex-end" gap={2}> | <Box display="flex" justifyContent="flex-end" gap={2}> | ||||
| <Button variant="text" onClick={handleClose}> | |||||
| <Button variant="text" onClick={onCancel}> | |||||
| {t('Cancel')} | {t('Cancel')} | ||||
| </Button> | </Button> | ||||
| <Button variant="contained" onClick={bigTesting}> | |||||
| {/* <Button variant="contained" onClick={handleSaveAll}> */} | |||||
| {t("Save")} | |||||
| </Button> | |||||
| <Button variant="contained" onClick={doSave}> | |||||
| {t("Save")} | |||||
| </Button> | |||||
| </Box> | </Box> | ||||
| {/* </FormControl> */} | {/* </FormControl> */} | ||||
| </Paper> | </Paper> | ||||
| </Modal> | </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; | export default SalaryEffectiveModel; | ||||
| @@ -113,10 +113,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| resetStaff() | resetStaff() | ||||
| }, [defaultValues]); | }, [defaultValues]); | ||||
| // useEffect(() => { | |||||
| // console.log(state) | |||||
| // }, [state]); | |||||
| const joinDate = watch("joinDate"); | const joinDate = watch("joinDate"); | ||||
| const departDate = watch("departDate"); | const departDate = watch("departDate"); | ||||
| @@ -126,112 +122,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| if (departDate) clearErrors("departDate"); | if (departDate) clearErrors("departDate"); | ||||
| }, [joinDate, departDate]); | }, [joinDate, departDate]); | ||||
| const salaryCols = useMemo( | |||||
| () => [ | |||||
| { | |||||
| field: 'salaryPoint', | |||||
| headerName: 'salaryPoint', | |||||
| flex: 1, | |||||
| editable: true, | |||||
| type: 'singleSelect', | |||||
| valueOptions: combos.salary.map((item) => item.label), | |||||
| // valueOptions: [], | |||||
| // width: 150 | |||||
| }, | |||||
| { | |||||
| field: 'date', | |||||
| headerName: 'date', | |||||
| flex: 1, | |||||
| editable: true, | |||||
| type: 'date', | |||||
| // width: 150 | |||||
| }, | |||||
| ], [combos]) | |||||
| 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 ( | return ( | ||||
| <Card sx={{ display: "block" }}> | <Card sx={{ display: "block" }}> | ||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| @@ -311,6 +201,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| label={t("Team")} | label={t("Team")} | ||||
| style={{ flex: 1, marginRight: '8px' }} | style={{ flex: 1, marginRight: '8px' }} | ||||
| {...field} | {...field} | ||||
| disabled | |||||
| // error={Boolean(errors.teamId)} | // error={Boolean(errors.teamId)} | ||||
| > | > | ||||
| {combos.team.map((team, index) => ( | {combos.team.map((team, index) => ( | ||||
| @@ -320,7 +211,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| ))} | ))} | ||||
| </Select> | </Select> | ||||
| <Button variant="contained" size="small" onClick={toggleTeamModal} | <Button variant="contained" size="small" onClick={toggleTeamModal} | ||||
| disabled={getValues("teamHistory").length == 0} | |||||
| // disabled={getValues("teamHistory").length == 0} | |||||
| > | > | ||||
| {t("Team History")} | {t("Team History")} | ||||
| </Button> | </Button> | ||||
| @@ -367,6 +258,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| style={{ flex: 1, marginRight: '8px' }} | style={{ flex: 1, marginRight: '8px' }} | ||||
| {...field} | {...field} | ||||
| error={Boolean(errors.gradeId)} | error={Boolean(errors.gradeId)} | ||||
| disabled | |||||
| > | > | ||||
| {combos.grade.map((grade, index) => ( | {combos.grade.map((grade, index) => ( | ||||
| <MenuItem key={`${grade.id}-${index}`} value={grade.id}> | <MenuItem key={`${grade.id}-${index}`} value={grade.id}> | ||||
| @@ -374,8 +266,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </Select> | </Select> | ||||
| <Button variant="contained" size="small" onClick={toggleGradeModal} | |||||
| disabled={getValues("gradeHistory").length == 0}> | |||||
| <Button variant="contained" size="small" onClick={toggleGradeModal}> | |||||
| {t("Grade History")} | {t("Grade History")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -432,6 +323,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| style={{ flex: 1, marginRight: '8px' }} | style={{ flex: 1, marginRight: '8px' }} | ||||
| {...field} | {...field} | ||||
| error={Boolean(errors.currentPositionId)} | error={Boolean(errors.currentPositionId)} | ||||
| disabled | |||||
| > | > | ||||
| {combos.position.map((position, index) => ( | {combos.position.map((position, index) => ( | ||||
| <MenuItem | <MenuItem | ||||
| @@ -442,8 +334,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| </MenuItem> | </MenuItem> | ||||
| ))} | ))} | ||||
| </Select> | </Select> | ||||
| <Button variant="contained" size="small" onClick={togglePositionModal} | |||||
| disabled={getValues("positionHistory").length == 0}> | |||||
| <Button variant="contained" size="small" onClick={togglePositionModal}> | |||||
| {t("Position History")} | {t("Position History")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -656,7 +547,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| label={t("Depart Date")} | label={t("Depart Date")} | ||||
| value={departDate ? dayjs(departDate) : null} | value={departDate ? dayjs(departDate) : null} | ||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (!date) return; | |||||
| if (!date) { | |||||
| setValue("departDate", null); | |||||
| return | |||||
| }; | |||||
| dayjs(date).add(1, 'month') | dayjs(date).add(1, 'month') | ||||
| setValue("departDate", date.format(INPUT_DATE_FORMAT)); | setValue("departDate", date.format(INPUT_DATE_FORMAT)); | ||||
| }} | }} | ||||
| @@ -707,28 +601,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
| <SalaryEffectiveModel | <SalaryEffectiveModel | ||||
| open={state.seModal} | open={state.seModal} | ||||
| onClose={toggleSeModal} | onClose={toggleSeModal} | ||||
| columns={salaryCols} | |||||
| combos={combos} | |||||
| /> | /> | ||||
| } | } | ||||
| {state.teamModal && | {state.teamModal && | ||||
| <TeamHistoryModal | <TeamHistoryModal | ||||
| open={state.teamModal} | open={state.teamModal} | ||||
| onClose={toggleTeamModal} | onClose={toggleTeamModal} | ||||
| columns={teamHistoryCols} | |||||
| combos={combos} | |||||
| /> | /> | ||||
| } | } | ||||
| {state.gradeModal && | {state.gradeModal && | ||||
| <GradeHistoryModal | <GradeHistoryModal | ||||
| open={state.gradeModal} | open={state.gradeModal} | ||||
| onClose={toggleGradeModal} | onClose={toggleGradeModal} | ||||
| columns={gradeHistoryCols} | |||||
| combos={combos} | |||||
| /> | /> | ||||
| } | } | ||||
| {state.positionModal && | {state.positionModal && | ||||
| <PositionHistoryModal | <PositionHistoryModal | ||||
| open={state.positionModal} | open={state.positionModal} | ||||
| onClose={togglePositionModal} | onClose={togglePositionModal} | ||||
| columns={positionHistoryCols} | |||||
| combos={combos} | |||||
| /> | /> | ||||
| } | } | ||||
| </Card> | </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 { useTranslation } from "react-i18next"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | 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 SaveIcon from '@mui/icons-material/Save'; | ||||
| import DeleteIcon from '@mui/icons-material/Delete'; | import DeleteIcon from '@mui/icons-material/Delete'; | ||||
| import CancelIcon from '@mui/icons-material/Cancel'; | 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"; | import waitForCondition from "../utils/waitFor"; | ||||
| interface Props { | interface Props { | ||||
| open: boolean; | open: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| columns: any[] | |||||
| combos: comboItem; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -31,172 +33,279 @@ interface Props { | |||||
| gap: 2, | 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 { | const { | ||||
| t, | t, | ||||
| // i18n: { language }, | // i18n: { language }, | ||||
| } = useTranslation(); | } = useTranslation(); | ||||
| const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const [count, setCount] = useState(0); | |||||
| const apiRef = useGridApiRef() | |||||
| const [_rows, setRows] = useState(() => { | const [_rows, setRows] = useState(() => { | ||||
| const list = getValues('teamHistory') | |||||
| const list: TeamModalRow[] = getValues(thisField) | |||||
| return list && list.length > 0 ? list : [] | return list && list.length > 0 ? list : [] | ||||
| }); | }); | ||||
| const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||||
| const [_delRows, setDelRows] = useState<number[]>([]); | 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, | ...prevRowModesModel, | ||||
| [id]: { mode: GridRowModes.View } | [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) | 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 } | [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(()=> { | useEffect(()=> { | ||||
| console.log(_rows) | console.log(_rows) | ||||
| setValue('teamHistory', _rows) | |||||
| // setValue(thisField, _rows) | |||||
| setValue('delTeamHistory', _delRows) | setValue('delTeamHistory', _delRows) | ||||
| }, [_rows, _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 [ | return [ | ||||
| <GridActionsCellItem | <GridActionsCellItem | ||||
| icon={<EditIcon />} | |||||
| label="Edit" | |||||
| className="textPrimary" | |||||
| onClick={handleEditClick(id)} | |||||
| color="inherit" | |||||
| icon={<SaveIcon />} | |||||
| label="Save" | |||||
| key="edit" | key="edit" | ||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<DeleteIcon />} | |||||
| label="Delete" | |||||
| sx={{ | 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 ( | return ( | ||||
| <Modal open={open} onClose={handleClose}> | <Modal open={open} onClose={handleClose}> | ||||
| <Paper sx={{ ...modalSx }}> | <Paper sx={{ ...modalSx }}> | ||||
| @@ -204,25 +313,48 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||||
| {t('TeamHistoryModal')} | {t('TeamHistoryModal')} | ||||
| </Typography> | </Typography> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| getRowId={getRowId} | |||||
| apiRef={apiRef} | |||||
| rows={_rows} | rows={_rows} | ||||
| columns={_columns} | |||||
| columns={columns} | |||||
| editMode="row" | 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} | rowModesModel={rowModesModel} | ||||
| onRowModesModelChange={setRowModesModel} | onRowModesModelChange={setRowModesModel} | ||||
| onRowEditStop={handleRowEditStop} | |||||
| processRowUpdate={processRowUpdate} | 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}> | <Box display="flex" justifyContent="flex-end" gap={2}> | ||||
| <Button variant="text" onClick={handleClose}> | |||||
| {t('Cancel')} | |||||
| <Button variant="text" onClick={onCancel}> | |||||
| {t('Close')} | |||||
| </Button> | </Button> | ||||
| <Button variant="contained" onClick={bigTesting}> | |||||
| <Button variant="contained" onClick={doSave}> | |||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| @@ -231,4 +363,20 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||||
| </Modal> | </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 | 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; | |||||
| } | |||||