Browse Source

update item 147

pull/5/head
MSI\derek 6 months ago
parent
commit
aa19b8599b
10 changed files with 1427 additions and 706 deletions
  1. +34
    -3
      src/app/api/staff/actions.ts
  2. +89
    -12
      src/components/CreateStaff/CreateStaff.tsx
  3. +171
    -16
      src/components/CreateStaff/StaffInfo.tsx
  4. +12
    -7
      src/components/EditStaff/EditStaff.tsx
  5. +242
    -112
      src/components/EditStaff/GradeHistoryModal.tsx
  6. +282
    -139
      src/components/EditStaff/PositionHistoryModal.tsx
  7. +231
    -154
      src/components/EditStaff/SalaryEffectiveModel.tsx
  8. +14
    -120
      src/components/EditStaff/StaffInfo.tsx
  9. +291
    -143
      src/components/EditStaff/TeamHistoryModal.tsx
  10. +61
    -0
      src/components/EditStaff/validateDates.ts

+ 34
- 3
src/app/api/staff/actions.ts View File

@@ -17,21 +17,18 @@ export type teamHistory = {
id: number,
team: string | number,
from: Date | string,
to?: Date | string
}

export type gradeHistory = {
id: number,
grade: string | number,
from: Date | string,
to?: Date | string
}

export type positionHistory = {
id: number,
position: string | number,
from: Date | string,
to?: Date | string
}
export interface CreateStaffInputs {
id?: number
@@ -62,6 +59,11 @@ export interface CreateStaffInputs {
delGradeHistory: number[];
positionHistory: positionHistory[];
delPositionHistory: number[];
// new modal
salary: salary[];
team: team[];
grade: grade[];
position: position[];
}
export interface records {
@@ -69,6 +71,35 @@ export interface CreateStaffInputs {
name: string;
// team: Team[];
}
export type DataLog =
| (salary & {id: number; type: "salary"})
| (team & {id: number; type: "team"})
| (grade & {id: number; type: "grade"})
| (position & {id: number; type: "position"})

export type salary = {
from: string;
to: string;
salaryPoint: number;
}

export type team = {
from: string;
to: string;
teamId: number;
}

export type grade = {
from: string;
to: string;
gradeId: number;
}

export type position = {
from: string;
to: string;
positionId: number;
}

export interface salaryEffectiveInfo {
id: number;


+ 89
- 12
src/components/CreateStaff/CreateStaff.tsx View File

@@ -10,9 +10,11 @@ import {
} from "react-hook-form";
import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions";
import { Button, Stack, Typography } from "@mui/material";
import { comboProp} from "@/app/api/companys/actions";
import { comboProp } from "@/app/api/companys/actions";
import StaffInfo from "./StaffInfo";
import { Check, Close } from "@mui/icons-material";
import dayjs from "dayjs";
import { SalaryEffectiveInfo } from "@/app/api/staff";

interface Field {
id: string;
@@ -45,15 +47,29 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
const { t } = useTranslation();
const formProps = useForm<CreateStaffInputs>();
const [serverError, setServerError] = useState("");
const [errorMsg, setErrorMsg] = useState("An error has occurred. Please try again later.")
const router = useRouter();
const [tabIndex, setTabIndex] = useState(0);
// const [tabIndex, setTabIndex] = useState(0);

const errors = formProps.formState.errors;

function chopSalaryPoints(input: string | number): number | null {
if (typeof input === 'string') {
const match = input.match(/(\d+) \((\d+) - (\d+)\)/);
if (match) {
return parseInt(match[1], 10);
}
} else if (typeof input === 'number') {
return input;
}
return null;
}

const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>(
async (data) => {
try {
console.log(data);
formProps.clearErrors()
let haveError = false;
const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
const regex_phone = /^\d{8}$/
@@ -96,20 +112,71 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
haveError = true
formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" })
}
if (!data.salaryId) {
if (data.joinDate && data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) {
haveError = true
formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" })
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" })
}
if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) {
if (!data.salaryEffectiveInfo || data.salaryEffectiveInfo.length < 1) {
haveError = true
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" })
formProps.setError("salaryId", { message: t("Please Enter Salary"), type: "required" })
}
if (!data.gradeHistory || data.gradeHistory.length < 1) {
haveError = true
formProps.setError("gradeId", { message: t("Please Enter Grade"), type: "required" })
}
if (!data.positionHistory || data.positionHistory.length < 1) {
console.log("asdadsasd")
haveError = true
formProps.setError("currentPositionId", { message: t("Please Enter Current Position"), type: "required" })
}
if (haveError) {
return
}
const teamHistory = data.teamHistory
.map((item) => ({
id: item.id,
team: combos.team.filter(team => team.label === item.team)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())
const gradeHistory = data.gradeHistory
.map((item) => ({
id: item.id,
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())
console.log(data.positionHistory)
const positionHistory = data.positionHistory
.map((item) => ({
id: item.id,
position: combos.position.filter(position => position.label === item.position)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())
const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({
id: item.id,
salaryPoint: chopSalaryPoints(item.salaryPoint),
date: dayjs(item.date).format('YYYY-MM-DD').toString()
})) // backend sort
const postData: CreateStaffInputs = {
// id: id,
...data,
salaryEffectiveInfo: salaryEffectiveInfo,
teamHistory: teamHistory ?? [],
gradeHistory: gradeHistory ?? [],
positionHistory: positionHistory ?? [],
delTeamHistory: data.delTeamHistory ? data.delTeamHistory : [],
delGradeHistory: data.delGradeHistory ? data.delGradeHistory : [],
delPositionHistory: data.delPositionHistory ? data.delPositionHistory : [],
}
console.log("passed")
console.log(data)
await saveStaff(data)
console.log(postData)
// return
await saveStaff(postData)
router.replace("/settings/staff")
} catch (e: any) {
console.log(e);
@@ -118,15 +185,19 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
if (e.message === "Duplicated StaffId Found") {
msg = t("Duplicated StaffId Found")
}
setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `);
setServerError(`${t(errorMsg)} ${msg} `);
}
},
[router]
[errorMsg, router]
);
const handleCancel = () => {
const errorKey = Object.keys(formProps.formState.errors)[0]
const err = errors[errorKey as keyof CreateStaffInputs]

const handleCancel = useCallback(() => {
router.back();
};
}, [router]);

return (
<>
@@ -141,6 +212,12 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
{serverError}
</Typography>
)}
{err && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{err.message?.toString()}
</Typography>
)
}
<StaffInfo combos={combos}/>
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button


+ 171
- 16
src/components/CreateStaff/StaffInfo.tsx View File

@@ -9,9 +9,10 @@ import Typography from "@mui/material/Typography";
import { CreateGroupInputs } from "@/app/api/group/actions";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
import { CreateStaffInputs } from "@/app/api/staff/actions";
import {
Button,
Checkbox,
FormControl,
InputLabel,
@@ -25,11 +26,52 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DemoItem } from "@mui/x-date-pickers/internals/demo";
import dayjs from "dayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import TableModal from "./TableModal";
import { Preview } from "@mui/icons-material";
import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel";
import TeamHistoryModal from "../EditStaff/TeamHistoryModal";
import GradeHistoryModal from "../EditStaff/GradeHistoryModal";
import PositionHistoryModal from "../EditStaff/PositionHistoryModal";

interface Props {
combos: comboItem;
}

type tableKey = "salary" | "team" | "grade" | "position";

////
const initState = {
teamModal: false,
seModal: false,
gradeModal: false,
positionModal: false,
}
const enum REDUCER_ACTION_TYPE {
TOGGLE_TEAM_MODAL,
TOGGLE_SALARY_EFFECTIVE_MODAL,
TOGGLE_GRADE_MODAL,
TOGGLE_POSITION_MODAL,
}

type ReducerAction = {
type: REDUCER_ACTION_TYPE
}

const reducer = (state: typeof initState, action: ReducerAction): typeof initState => {
switch (action.type) {
case REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL:
return { ...state, teamModal: !state.teamModal };
case REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL:
return { ...state, seModal: !state.seModal };
case REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL:
return { ...state, gradeModal: !state.gradeModal };
case REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL:
return { ...state, positionModal: !state.positionModal };
default:
return state;
}
}
/////
const StaffInfo: React.FC<Props> = ({ combos }) => {
const {
t,
@@ -45,6 +87,14 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
getValues,
clearErrors
} = useFormContext<CreateStaffInputs>();
const [tableKey, setTableKey] = useState<tableKey>()
const [isOpen, setIsOpen] = useState(false)

const [state, dispatch] = useReducer(reducer, initState)
const toggleSeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL})
const toggleTeamModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL})
const toggleGradeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL})
const togglePositionModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL})

const employType = [
{ id: 1, label: "FT" },
@@ -64,6 +114,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
}
}, [defaultValues]);

const toggleModal = useCallback((key: tableKey) => {
setIsOpen(!isOpen)
setTableKey(key)
}, [isOpen])

const joinDate = getValues("joinDate");
const departDate = getValues("departDate");

@@ -148,17 +203,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
control={control}
name="teamId"
render={({ field }) => (
<Select
label={t("Team")}
{...field}
// error={Boolean(errors.teamId)}
>
{combos.team.map((team, index) => (
<MenuItem key={`${team.id}-${index}`} value={team.id}>
{t(team.label)}
</MenuItem>
))}
</Select>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Select
label={t("Team")}
style={{ flex: 1, marginRight: '8px' }}
{...field}
// error={Boolean(errors.teamId)}
disabled
>
{combos.team.map((team, index) => (
<MenuItem key={`${team.id}-${index}`} value={team.id}>
{t(team.label)}
</MenuItem>
))}
</Select>
<Button
variant="contained"
size="small"
onClick={toggleTeamModal}
>
{t("Team History")}
</Button>
</Box>
)}
/>
</FormControl>
@@ -191,7 +257,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel required>{t("Grade")}</InputLabel>
<Controller
{/* <Controller
control={control}
name="gradeId"
render={({ field }) => (
@@ -207,6 +273,30 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
))}
</Select>
)}
/> */}
<Controller
control={control}
name="gradeId"
render={({ field }) => (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Select
label={t("Grade")}
style={{ flex: 1, marginRight: '8px' }}
{...field}
error={Boolean(errors.gradeId)}
disabled
>
{combos.grade.map((grade, index) => (
<MenuItem key={`${grade.id}-${index}`} value={grade.id}>
{t(grade.label)}
</MenuItem>
))}
</Select>
<Button variant="contained" size="small" onClick={toggleGradeModal}>
{t("Grade History")}
</Button>
</Box>
)}
/>
</FormControl>
</Grid>
@@ -254,9 +344,12 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
control={control}
name="currentPositionId"
render={({ field }) => (
<Select
<Box display="flex" justifyContent="space-between" alignItems="center">
<Select
label={t("Current Position")}
style={{ flex: 1, marginRight: '8px' }}
{...field}
disabled
error={Boolean(errors.currentPositionId)}
>
{combos.position.map((position, index) => (
@@ -268,6 +361,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</MenuItem>
))}
</Select>
<Button variant="contained" size="small" onClick={togglePositionModal}>
{t("Position History")}
</Button>
</Box>
)}
/>
</FormControl>
@@ -275,7 +372,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel required>{t("Salary Point")}</InputLabel>
<Controller
{/* <Controller
control={control}
name="salaryId"
render={({ field }) => (
@@ -294,7 +391,35 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
))}
</Select>
)}
/>
/> */}
<Controller
control={control}
name="salaryId"
render={({ field }) => (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Select
label={t("Salary Point")}
{...field}
error={Boolean(errors.salaryId)}
style={{ flex: 1, marginRight: '8px' }}
disabled
>
{combos.salary.map((salary, index) => (
<MenuItem
key={`${salary.id}-${index}`}
value={salary.id}
>
{t(salary.label)}
</MenuItem>
))}
</Select>
{/* <Button variant="contained" size="small" onClick={() => toggleModal("salary")}> */}
<Button variant="contained" size="small" onClick={toggleSeModal}>
{t("Edit Salary")}
</Button>
</Box>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>
@@ -512,6 +637,36 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</Grid>
</Box>
</CardContent>
{state.seModal &&
<SalaryEffectiveModel
open={state.seModal}
onClose={toggleSeModal}
combos={combos}
// columns={salaryCols}
/>
}
{state.teamModal &&
<TeamHistoryModal
open={state.teamModal}
onClose={toggleTeamModal}
combos={combos}
/>
}
{state.gradeModal &&
<GradeHistoryModal
open={state.gradeModal}
onClose={toggleGradeModal}
combos={combos}
/>
}
{state.positionModal &&
<PositionHistoryModal
open={state.positionModal}
onClose={togglePositionModal}
combos={combos}
/>
}
{/* {tableKey && <TableModal tableKey={tableKey} isOpen={isOpen} onClose={() => toggleModal("team")} combos={combos}/>} */}
</Card>
);
};


+ 12
- 7
src/components/EditStaff/EditStaff.tsx View File

@@ -88,7 +88,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In
id: item.id,
team: item.team.name,
from: dayjs(item.from.join()).toDate(),
to: item.to ? dayjs(item.to.join()).toDate() : "",
to: item.to ? dayjs(item.to.join()).toDate() : undefined,
})
}) : [],
delTeamHistory: [],
@@ -97,7 +97,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In
id: item.id,
grade: item.grade.name,
from: dayjs(item.from.join()).toDate(),
to: item.to ? dayjs(item.to.join()).toDate() : "",
to: item.to ? dayjs(item.to.join()).toDate() : undefined,
})
}) : [],
delGradeHistory: [],
@@ -106,7 +106,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In
id: item.id,
position: item.position.name,
from: dayjs(item.from.join()).toDate(),
to: item.to ? dayjs(item.to.join()).toDate() : "",
to: item.to ? dayjs(item.to.join()).toDate() : undefined,
})
}) : [],
delPositionHistory: [],
@@ -180,21 +180,26 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In
id: item.id,
team: combos.team.filter(team => team.label === item.team)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())

const gradeHistory = data.gradeHistory.map((item) => ({
id: item.id,
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())

const positionHistory = data.positionHistory.map((item) => ({
id: item.id,
position: combos.position.filter(position => position.label === item.position)[0].id,
from: dayjs(item.from).format('YYYY-MM-DD'),
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined,
}))
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime())

const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({
id: item.id,
salaryPoint: chopSalaryPoints(item.salaryPoint),


+ 242
- 112
src/components/EditStaff/GradeHistoryModal.tsx View File

@@ -1,20 +1,26 @@
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material"
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material"
import StyledDataGrid from "../StyledDataGrid"
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { useCallback, useEffect, useMemo, useState } from "react";
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid";
import AddIcon from '@mui/icons-material/Add';
import SaveIcon from '@mui/icons-material/Save';
import DeleteIcon from '@mui/icons-material/Delete';
import CancelIcon from '@mui/icons-material/Cancel';
import EditIcon from '@mui/icons-material/Edit';
import waitForCondition from "../utils/waitFor";
import { gradeHistory } from "@/app/api/staff/actions";
import { Add } from "@mui/icons-material";
import { comboItem } from "../CreateStaff/CreateStaff";
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates";
import { ProcessRowUpdateError } from "./TeamHistoryModal";

interface Props {
open: boolean;
onClose: () => void;
columns: any[]
combos: comboItem;
// columns: any[]
}

const modalSx: SxProps = {
@@ -31,119 +37,217 @@ const modalSx: SxProps = {
gap: 2,
};

const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => {
export type GradeModalRow = Partial<
gradeHistory & {
_isNew: boolean
_error: StaffEntryError;
}>
const thisField = "gradeHistory"
const GradeHistoryModal: React.FC<Props> = ({ open, onClose, combos }) => {
const {
t,
// i18n: { language },
} = useTranslation();
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [count, setCount] = useState(0);
const apiRef = useGridApiRef()
const originalRows = getValues(thisField)
const [_rows, setRows] = useState(() => {
const list = getValues('gradeHistory')
const list: GradeModalRow[] = getValues(thisField)
return list && list.length > 0 ? list : []
});
const [_delRows, setDelRows] = useState<number[]>([]);
const formValues = watch();

const handleClose = () => {
onClose();
};
const looping = async () => {
for (let i = 0; i < _rows.length; i++) {
const id = _rows[i].id
setRowModesModel((prevRowModesModel) => ({
const getRowId = useCallback<GridRowIdGetter<GradeModalRow>>(
(row) => row.id!!,
[],
);
const handleSave = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
}
return true;
}
const handleSaveAll = async () => {
// trigger save all
console.log(_rows)
await waitForCondition(async () => {
return looping()
})
console.log(rowModesModel)
};
},
[setRowModesModel]
);

const onCancel = useCallback(() => {
setRows(originalRows)
onClose();
}, [onClose, originalRows]);
const bigTesting = async () => {
await looping()
setTimeout(() => {
onClose()
}, 800)
}
const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
};
// handle row update here
const processRowUpdate =
// useCallback(
(newRow: GridRowModel) => {
console.log(newRow)
const updatedRow = { ...newRow, updated: true };
console.log(_rows)
if (_rows.length != 0) {
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row)));
}, [onClose]);

const isSaved = useCallback(() => {
const saved = Object.keys(rowModesModel).every(key => {
rowModesModel[key].mode === GridRowModes.Edit
})
return saved
}, [rowModesModel])

const doSave = useCallback(async () => {
try {
if (isSaved()) {
setValue(thisField, _rows)
onClose()
}
} catch (error) {
console.error(error);
}
}, [isSaved, onClose, _rows])

const addRow = useCallback(() => {
const id = Date.now()
const newEntry = { id, _isNew: true } satisfies GradeModalRow;
setRows((prev) => [...prev, newEntry])
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
fieldToFocus: "grade",
}
return updatedRow;
}))
}, []);
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<GradeModalRow>) => {
const errors = updateError.errors;
// const prevRow = updateError.prevRow;
const currRow = updateError.currRow;
// if (updateError.prevRow) {
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]);
// }
apiRef.current.updateRows([{ ...currRow, _error: errors }]);
},
[apiRef, rowModesModel],
);

const processRowUpdate = useCallback((
newRow: GridRowModel<GradeModalRow>,
originalRow: GridRowModel<GradeModalRow>
) => {
const rowIndex = _rows.findIndex((row: GradeModalRow) => row.id === newRow.id);
const prevRow: GradeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null;
const errors = validateRowAndRowBefore(prevRow, newRow)
console.log(errors)
if (errors) {
throw new ProcessRowUpdateError(
prevRow,
newRow,
"validation error",
errors
)
}
const { _isNew, _error, ...updatedRow } = newRow;

const rowToSave = {
...updatedRow,
}
console.log(_rows)
if (_rows.length != 0) {
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => {
if (!a.from || !b.from) return 0;
return new Date(a.from).getTime() - new Date(b.from).getTime();
}));
}
return rowToSave;
}
// , [_rows, setValue, setRows])
, [_rows, validateRowAndRowBefore])

const handleSaveClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
},
[setRowModesModel]
);
const handleCancelClick = useCallback(
const handleCancel = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
}));
const editedRow = _rows.find((r) => getRowId(r) === id)
if (editedRow?._isNew) {
setRows((rw) => rw.filter((r) => r.id !== id))
} else {
setRows((rw) =>
rw.map((r) =>
getRowId(r) === id
? { ...r, _error: undefined }
: r,
),
);
}
},
[setRowModesModel]
);

const handleEditClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.Edit }
}));
},
[setRowModesModel]
[setRowModesModel, _rows]
);

const handleDeleteClick = useCallback(
(id: any) => () => {
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id));
setCount((prev: number) => prev - 1);
setDelRows((prevRowsId: number[]) => [...prevRowsId, id])
const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row) => row.id !== id));
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number])
},
[setRows, setCount, setDelRows]
[]
);
useEffect(()=> {
console.log(_rows)
setValue('gradeHistory', _rows)
// setValue(thisField, _rows)
setValue('delGradeHistory', _delRows)
}, [_rows, _delRows])

const defaultCol = useMemo(
() => (
const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Add Record")}
</Button>
</Box>
)
const columns = useMemo(
() => [
{
field: 'grade',
headerName: 'grade',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.grade.map(item => item.label),
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = (
<GridEditSingleSelectCell variant="outlined" {...params} />
);
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = <GridEditDateCell {...params} />;
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'actions',
type: 'actions',
@@ -161,42 +265,29 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) =>
sx={{
color: 'primary.main'
}}
onClick={handleSaveClick(id)}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancelClick(id)}
onClick={handleCancel(id)}
/>
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
key="edit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
}}
onClick={handleDeleteClick(id)} color="inherit" key="edit" />
onClick={handleDelete(id)} color="inherit" key="edit" />
];
}
}
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick]
)
let _columns: any[] = []
if (columns) {
_columns = [...columns, defaultCol]
}
], [combos, rowModesModel, handleSave, handleCancel, handleDelete])

return (
<Modal open={open} onClose={handleClose}>
<Paper sx={{ ...modalSx }}>
@@ -204,25 +295,48 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) =>
{t('GradeHistoryModal')}
</Typography>
<StyledDataGrid
getRowId={getRowId}
apiRef={apiRef}
rows={_rows}
columns={_columns}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
// slots={{
// toolbar: EditToolbar
// }}
// slotProps={{
// toolbar: {count, setCount, setRows, setRowModesModel, _columns}
// }}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<GradeModalRow>) => {
let classname = "";
if (params.row._error) {
classname = "hasError"
}
return classname;
}}
slots={{
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
footer: { child: footer },
}}
/>
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button variant="text" onClick={handleClose}>
{t('Cancel')}
<Button variant="text" onClick={onCancel}>
{t('Close')}
</Button>
<Button variant="contained" onClick={bigTesting}>
<Button variant="contained" onClick={doSave}>
{t("Save")}
</Button>
</Box>
@@ -231,4 +345,20 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) =>
</Modal>
)
}
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};
export default GradeHistoryModal

+ 282
- 139
src/components/EditStaff/PositionHistoryModal.tsx View File

@@ -1,20 +1,26 @@
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material"
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material"
import StyledDataGrid from "../StyledDataGrid"
import { useTranslation } from "react-i18next";
import { useFormContext } from "react-hook-form";
import { useCallback, useEffect, useMemo, useState } from "react";
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid";
import AddIcon from '@mui/icons-material/Add';
import SaveIcon from '@mui/icons-material/Save';
import DeleteIcon from '@mui/icons-material/Delete';
import CancelIcon from '@mui/icons-material/Cancel';
import EditIcon from '@mui/icons-material/Edit';
import waitForCondition from "../utils/waitFor";
import { Add } from "@mui/icons-material";
import { positionHistory } from "@/app/api/staff/actions";
import { comboItem } from "../CreateStaff/CreateStaff";
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates";
import { ProcessRowUpdateError } from "./TeamHistoryModal";
import { createSearchParamsBailoutProxy } from "next/dist/client/components/searchparams-bailout-proxy";

interface Props {
open: boolean;
onClose: () => void;
columns: any[]
combos: comboItem;
}

const modalSx: SxProps = {
@@ -31,171 +37,267 @@ const modalSx: SxProps = {
gap: 2,
};

const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => {
export type PositionModalRow = Partial<
positionHistory & {
_isNew: boolean
_error: StaffEntryError;
}>

const thisField = "positionHistory"
const PositionHistoryModal: React.FC<Props> = ({
open,
onClose,
combos
}) => {
const {
t,
// i18n: { language },
} = useTranslation();
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [count, setCount] = useState(0);
const apiRef = useGridApiRef()
const originalRows = getValues(thisField)
const [_rows, setRows] = useState(() => {
const list = getValues('positionHistory')
const list: PositionModalRow[] = getValues(thisField)
return list && list.length > 0 ? list : []
});
const [_delRows, setDelRows] = useState<number[]>([]);
const formValues = watch();
const getRowId = useCallback<GridRowIdGetter<PositionModalRow>>(
(row) => row.id!!,
[],
);

const handleClose = () => {
onClose();
};
const looping = async () => {
for (let i = 0; i < _rows.length; i++) {
const id = _rows[i].id
setRowModesModel((prevRowModesModel) => ({
const handleSave = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
}
return true;
}
const handleSaveAll = async () => {
// trigger save all
console.log(_rows)
await waitForCondition(async () => {
return looping()
},
[setRowModesModel]
);

const onCancel = useCallback(() => {
console.log(originalRows)
setRows(originalRows)
onClose();
}, [onClose, originalRows]);

const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
}, [onClose]);

const isSaved = useCallback(() => {
const saved = Object.keys(rowModesModel).every(key => {
rowModesModel[key].mode === GridRowModes.Edit
})
console.log(rowModesModel)
};
const bigTesting = async () => {
await looping()
setTimeout(() => {
onClose()
}, 800)
}
const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
return saved
}, [rowModesModel])

const doSave = useCallback(async () => {
try {
if (isSaved()) {
setValue(thisField, _rows)
onClose()
}
};
} catch (error) {
console.error(error);
}
}, [isSaved, onClose, _rows]);

const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<PositionModalRow>) => {
const errors = updateError.errors;
const prevRow = updateError.prevRow;
const currRow = updateError.currRow;
// if (updateError.prevRow) {
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]);
// }
apiRef.current.updateRows([{ ...currRow, _error: errors }]);
},
[apiRef, rowModesModel],
);
// handle row update here
const processRowUpdate =
// useCallback(
(newRow: GridRowModel) => {
console.log(newRow)
const updatedRow = { ...newRow, updated: true };
const processRowUpdate = useCallback((
newRow: GridRowModel<PositionModalRow>,
originalRow: GridRowModel<PositionModalRow>
) => {
const rowIndex = _rows.findIndex((row: PositionModalRow) => row.id === newRow.id);
const prevRow: PositionModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null;
const errors = validateRowAndRowBefore(prevRow, newRow)
console.log(errors)
if (errors) {
throw new ProcessRowUpdateError(
prevRow,
newRow,
"validation error",
errors
)
}
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
}
console.log(_rows)
if (_rows.length != 0) {
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row)));
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => {
if (!a.from || !b.from) return 0;
return new Date(a.from).getTime() - new Date(b.from).getTime();
}));
}
return updatedRow;
}
// , [_rows, setValue, setRows])

const handleSaveClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
},
[setRowModesModel]
);
const handleCancelClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
}));
},
[setRowModesModel]
);
return rowToSave;
}
, [_rows, validateRowAndRowBefore])

const handleEditClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.Edit }
const addRow = useCallback(() => {
const newEntry = { id: Date.now(), _isNew: true } satisfies PositionModalRow;
setRows((prev) => [...prev, newEntry])
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
fieldToFocus: "position",
}
}))
}, [getRowId]);

const handleCancel = useCallback(
(id: GridRowId) => () => {
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
}));
const editedRow = _rows.find((row) => getRowId(row) === id);
console.log(editedRow)
if (editedRow?._isNew) {
setRows((rw) => rw.filter((r) => r.id !== id))
} else {
setRows((rw) =>
rw.map((r) =>
getRowId(r) === id
? { ...r, _error: undefined }
: r,
),
);
}
},
[setRowModesModel]
[setRowModesModel, _rows]
);

const handleDeleteClick = useCallback(
(id: any) => () => {
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id));
setCount((prev: number) => prev - 1);
setDelRows((prevRowsId: number[]) => [...prevRowsId, id])
const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row) => row.id !== id));
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number])
},
[setRows, setCount, setDelRows]
[setRows, setDelRows]
);
useEffect(()=> {
console.log(_rows)
setValue('positionHistory', _rows)
setValue(thisField, _rows)
setValue('delPositionHistory', _delRows)
}, [_rows, _delRows])

const defaultCol = useMemo(
() => (
{
field: 'actions',
type: 'actions',
headerName: 'edit',
width: 100,
cellClassName: 'actions',
getActions: ({ id }: { id: number }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
key="edit"
sx={{
color: 'primary.main'
}}
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancelClick(id)}
/>
];
}
const columns = useMemo(
() => [
{
field: 'position',
headerName: 'position',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.position.map(item => item.label),
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = (
<GridEditSingleSelectCell variant="outlined" {...params} />
);
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = <GridEditDateCell {...params} />;
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'actions',
type: 'actions',
headerName: 'edit',
width: 100,
cellClassName: 'actions',
getActions: ({ id }: { id: number }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
icon={<SaveIcon />}
label="Save"
key="edit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
color: 'primary.main'
}}
onClick={handleDeleteClick(id)} color="inherit" key="edit" />
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancel(id)}
/>
];
}
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
}}
onClick={handleDelete(id)} color="inherit" key="edit" />
];
}
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick]
)
let _columns: any[] = []
if (columns) {
_columns = [...columns, defaultCol]
}
}
], [combos, rowModesModel, handleSave, handleCancel, handleDelete])

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Add Record")}
</Button>
</Box>
)

return (
<Modal open={open} onClose={handleClose}>
<Paper sx={{ ...modalSx }}>
@@ -203,25 +305,48 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns })
{t('PositionHistoryModal')}
</Typography>
<StyledDataGrid
getRowId={getRowId}
apiRef={apiRef}
rows={_rows}
columns={_columns}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
// slots={{
// toolbar: EditToolbar
// }}
// slotProps={{
// toolbar: {count, setCount, setRows, setRowModesModel, _columns}
// }}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<PositionModalRow>) => {
let classname = "";
if (params.row._error) {
classname = "hasError"
}
return classname;
}}
slots={{
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
footer: { child: footer },
}}
/>
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button variant="text" onClick={handleClose}>
{t('Cancel')}
<Button variant="text" onClick={onCancel}>
{t('Close')}
</Button>
<Button variant="contained" onClick={bigTesting}>
<Button variant="contained" onClick={doSave}>
{t("Save")}
</Button>
</Box>
@@ -230,4 +355,22 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns })
</Modal>
)
}

const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};

export default PositionHistoryModal

+ 231
- 154
src/components/EditStaff/SalaryEffectiveModel.tsx View File

@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material';
import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps, ModalProps, Tooltip } from '@mui/material';
import { useForm, Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil';
import dayjs from 'dayjs';
import { DatePicker } from '@mui/x-date-pickers';
import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid';
import { GridRenderEditCellParams, FooterPropsOverrides, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditSingleSelectCell, GridCellParams, GridRowId } from '@mui/x-data-grid';
import StyledDataGrid from '../StyledDataGrid';
import AddIcon from '@mui/icons-material/Add';
import SaveIcon from '@mui/icons-material/Save';
@@ -14,12 +14,17 @@ import CancelIcon from '@mui/icons-material/Cancel';
import EditIcon from '@mui/icons-material/Edit';
import { GridActionsCellItem } from '@mui/x-data-grid';
import waitForCondition from '../utils/waitFor';
import { StaffEntryError, validateRowAndRowBefore } from './validateDates';
import { salaryEffectiveInfo } from '@/app/api/staff/actions';
import { comboItem } from "../CreateStaff/CreateStaff";
import { Add } from '@mui/icons-material';
import { GridEditDateCell } from '@mui/x-data-grid';

interface SalaryEffectiveModelProps {
open: boolean;
onClose: () => void;
modalSx?: SxProps;
columns: any[]
combos: comboItem;
}

const modalSx: SxProps = {
@@ -36,125 +41,133 @@ const modalSx: SxProps = {
gap: 2,
};

function EditToolbar(props: React.JSXElementConstructor<any> | null | undefined | any) {
// const intl = useIntl();
// const addRecordBtn = intl.formatMessage({ id: 'add' });
const { count, setCount, setRows, setRowModesModel, _columns } = props;
let obj: { [key: string]: string } = {};
for (let i = 0; i < _columns.length - 1; i++) {
obj[_columns[i].field as string] = '';
}
const handleClick = React.useCallback(() => {
const id = Math.random();
setRows((oldRows: any) => [...oldRows, { id, ...obj, isNew: true }]);
setRowModesModel((oldModel: any) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit,
// fieldToFocus: 'material'
}
}));
setCount((prev: number) => prev+1)
}, [count, setCount, setRowModesModel, setRows])
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
{"addRecordBtn"}
</Button>
{/* <Button color="primary" startIcon={<AddIcon />} onClick={handleSave}>
SAVE
</Button> */}
</GridToolbarContainer>
);
}
export type SeModalRow = Partial<
salaryEffectiveInfo & {
_isNew: boolean
_error: StaffEntryError;
}
>
export class ProcessRowUpdateError<T> extends Error {
public readonly prevRow: T | null;
public readonly currRow: T;
public readonly errors: StaffEntryError | undefined;
constructor(
prevRow: T | null,
currRow: T,
message?: string,
errors?: StaffEntryError,
) {
super(message);
this.prevRow = prevRow;
this.currRow = currRow;
this.errors = errors;

const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, columns }) => {
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
}
}
const thisField = "salaryEffectiveInfo"
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, combos }) => {
const {
t,
// i18n: { language },
} = useTranslation();
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [count, setCount] = useState(0);
const apiRef = useGridApiRef()
const [_rows, setRows] = useState(() => {
const list = getValues('salaryEffectiveInfo')
const list: SeModalRow[] = getValues(thisField)
return list && list.length > 0 ? list : []
});
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows])
const [_delRows, setDelRows] = useState<number[]>([]);

const formValues = watch(); // This line of code is using the watch function from react-hook-form to get the current values of the form fields.
const getRowId = useCallback<GridRowIdGetter<SeModalRow>>(
(row) => row.id!!,
[],
);

const handleClose = () => {
onClose();
};
const looping = async () => {
for (let i = 0; i < _rows.length; i++) {
const id = _rows[i].id
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
}
return true;
}
const handleSaveAll = async () => {
// trigger save all
console.log(_rows)
await waitForCondition(async () => {
return looping()
const isSaved = useCallback(() => {
const saved = Object.keys(rowModesModel).every(key => {
rowModesModel[key].mode === GridRowModes.Edit
})
console.log(rowModesModel)
};
return saved
}, [rowModesModel])

const bigTesting = async () => {
await looping()
setTimeout(() => {
onClose()
}, 800)
}
// const handleSave = async () => {
// const isValid = await trigger();
// // if (isValid) {
// // onSave();
// // onClose();
// // }
// };
const doSave = useCallback(async () => {
try {
if (isSaved()) {
setValue(thisField, _rows)
onClose()
}
} catch (error) {
console.error(error);
}
}, [isSaved, onClose, _rows]);

const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
const onCancel = useCallback(() => {
setRows(originalRows)
onClose();
}, [onClose, originalRows]);

const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
}, [onClose]);
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<SeModalRow>) => {
const errors = updateError.errors;
const currRow = updateError.currRow;
console.log(errors)
apiRef.current.updateRows([{ ...currRow, _error: errors }]);
},
[apiRef, rowModesModel],
);
const processRowUpdate = useCallback((
newRow: GridRowModel<SeModalRow>,
originalRow: GridRowModel<SeModalRow>
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
const rowIndex = _rows.findIndex((row: SeModalRow) => row.id === newRow.id);
const prevRow: SeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null;
const errors = validateRowAndRowBefore(prevRow, newRow)
console.log(errors)
if (errors) {
throw new ProcessRowUpdateError(
prevRow,
newRow,
"validation error",
errors
)
}
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
_isNew,
}
};

const processRowUpdate =
// useCallback(
(newRow: GridRowModel) => {
console.log(newRow)
const updatedRow = { ...newRow, updated: true };
console.log(_rows)
if (_rows.length != 0) {
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row)));
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(a.date).getTime() - new Date(b.date).getTime();
}));
}
return updatedRow;
return rowToSave;
}
// , [_rows, setValue, setRows])
, [validateRowAndRowBefore, _rows])

useEffect(()=> {
console.log(_rows)
setValue('salaryEffectiveInfo', _rows)
}, [_rows])
// useEffect(()=> {
// console.log(_rows)
// setValue(thisField, _rows)
// }, [_rows])

useEffect(()=> {
console.log(_delRows)
setValue('delSalaryEffectiveInfo', _delRows)
}, [_delRows])

const handleSaveClick = useCallback((id: any) => () => {
const handleSave = useCallback((id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
@@ -163,17 +176,8 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo
[setRowModesModel]
);
const handleSaveClickAsync = useCallback(async(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
},
[setRowModesModel]
);
const handleCancelClick = useCallback(
(id: any) => () => {
const handleCancel = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
@@ -182,27 +186,56 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo
[setRowModesModel]
);

const handleEditClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.Edit }
}));
const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row: any) => row.id !== id));
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number])
},
[setRowModesModel]
[]
);

const handleDeleteClick = useCallback(
(id: any) => () => {
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id));
setCount((prev: number) => prev - 1);
setDelRows((prevRowsId: number[]) => [...prevRowsId, id])
},
[setRows, setCount, setDelRows]
);

const defaultCol = useMemo(
() => (
const columns = useMemo(
() => [
{
field: 'salaryPoint',
headerName: 'salaryPoint',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.salary.map((item) => item.label),
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = (
<GridEditSingleSelectCell variant="outlined" {...params} />
);
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'date',
headerName: 'date',
flex: 1,
editable: true,
type: 'date',
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = <GridEditDateCell {...params} />;
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'actions',
type: 'actions',
@@ -220,47 +253,54 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo
sx={{
color: 'primary.main'
}}
onClick={handleSaveClick(id)}
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancelClick(id)}
onClick={handleCancel(id)}
/>
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
key="edit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
}}
onClick={handleDeleteClick(id)} color="inherit" key="edit" />
onClick={handleDelete(id)} color="inherit" key="edit" />
];
}
}
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick]
)
], [combos, rowModesModel, handleSave, handleCancel, handleDelete])

let _columns: any[] = []
if (columns) {
_columns = [...columns, defaultCol]
}
const addRow = useCallback(() => {
const newEntry = { id: Date.now(), _isNew: true } satisfies SeModalRow;
setRows((prev) => [...prev, newEntry])
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
fieldToFocus: "team",
}
}))
}, [getRowId]);

useEffect(() => {
console.log(_rows)
}, [_rows])
const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Add Record")}
</Button>
</Box>
)

return (
<Modal open={open} onClose={handleClose}>
@@ -269,33 +309,70 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo
{t('Salary Effective Date Change')}
</Typography>
<StyledDataGrid
getRowId={getRowId}
apiRef={apiRef}
rows={_rows}
columns={_columns}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleRowEditStop}
onProcessRowUpdateError={onProcessRowUpdateError}
processRowUpdate={processRowUpdate}
getCellClassName={(params: GridCellParams<SeModalRow>) => {
let classname = "";
if (params.row._error) {
classname = "hasError"
}
return classname;
}}
slots={{
toolbar: EditToolbar
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
toolbar: {count, setCount, setRows, setRowModesModel, _columns}
footer: { child: footer },
}}
/>
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button variant="text" onClick={handleClose}>
<Button variant="text" onClick={onCancel}>
{t('Cancel')}
</Button>
<Button variant="contained" onClick={bigTesting}>
{/* <Button variant="contained" onClick={handleSaveAll}> */}
{t("Save")}
</Button>
<Button variant="contained" onClick={doSave}>
{t("Save")}
</Button>
</Box>
{/* </FormControl> */}
</Paper>
</Modal>
);
};

const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};
export default SalaryEffectiveModel;

+ 14
- 120
src/components/EditStaff/StaffInfo.tsx View File

@@ -113,10 +113,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
useEffect(() => {
resetStaff()
}, [defaultValues]);
// useEffect(() => {
// console.log(state)
// }, [state]);

const joinDate = watch("joinDate");
const departDate = watch("departDate");
@@ -126,112 +122,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
if (departDate) clearErrors("departDate");
}, [joinDate, departDate]);

const salaryCols = useMemo(
() => [
{
field: 'salaryPoint',
headerName: 'salaryPoint',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.salary.map((item) => item.label),
// valueOptions: [],
// width: 150
},
{
field: 'date',
headerName: 'date',
flex: 1,
editable: true,
type: 'date',
// width: 150
},
], [combos])

const teamHistoryCols = useMemo(
() => [
{
field: 'team',
headerName: 'team',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.team.map(item => item.label),
// valueOptions: [],
// width: 150
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
},
{
field: 'to',
headerName: 'to',
flex: 1,
editable: true,
type: 'date',
},
], [combos])

const gradeHistoryCols = useMemo(
() => [
{
field: 'grade',
headerName: 'grade',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.grade.map(item => item.label),
// valueOptions: [],
// width: 150
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
},
{
field: 'to',
headerName: 'to',
flex: 1,
editable: true,
type: 'date',
},
], [combos])

const positionHistoryCols = useMemo(
() => [
{
field: 'position',
headerName: 'position',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.position.map(item => item.label),
// valueOptions: [],
// width: 150
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
},
{
field: 'to',
headerName: 'to',
flex: 1,
editable: true,
type: 'date',
},
], [combos])

return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
@@ -311,6 +201,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
label={t("Team")}
style={{ flex: 1, marginRight: '8px' }}
{...field}
disabled
// error={Boolean(errors.teamId)}
>
{combos.team.map((team, index) => (
@@ -320,7 +211,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
))}
</Select>
<Button variant="contained" size="small" onClick={toggleTeamModal}
disabled={getValues("teamHistory").length == 0}
// disabled={getValues("teamHistory").length == 0}
>
{t("Team History")}
</Button>
@@ -367,6 +258,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
style={{ flex: 1, marginRight: '8px' }}
{...field}
error={Boolean(errors.gradeId)}
disabled
>
{combos.grade.map((grade, index) => (
<MenuItem key={`${grade.id}-${index}`} value={grade.id}>
@@ -374,8 +266,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</MenuItem>
))}
</Select>
<Button variant="contained" size="small" onClick={toggleGradeModal}
disabled={getValues("gradeHistory").length == 0}>
<Button variant="contained" size="small" onClick={toggleGradeModal}>
{t("Grade History")}
</Button>
</Box>
@@ -432,6 +323,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
style={{ flex: 1, marginRight: '8px' }}
{...field}
error={Boolean(errors.currentPositionId)}
disabled
>
{combos.position.map((position, index) => (
<MenuItem
@@ -442,8 +334,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</MenuItem>
))}
</Select>
<Button variant="contained" size="small" onClick={togglePositionModal}
disabled={getValues("positionHistory").length == 0}>
<Button variant="contained" size="small" onClick={togglePositionModal}>
{t("Position History")}
</Button>
</Box>
@@ -656,7 +547,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
label={t("Depart Date")}
value={departDate ? dayjs(departDate) : null}
onChange={(date) => {
if (!date) return;
if (!date) {
setValue("departDate", null);
return
};
dayjs(date).add(1, 'month')
setValue("departDate", date.format(INPUT_DATE_FORMAT));
}}
@@ -707,28 +601,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
<SalaryEffectiveModel
open={state.seModal}
onClose={toggleSeModal}
columns={salaryCols}
combos={combos}
/>
}
{state.teamModal &&
<TeamHistoryModal
open={state.teamModal}
onClose={toggleTeamModal}
columns={teamHistoryCols}
combos={combos}
/>
}
{state.gradeModal &&
<GradeHistoryModal
open={state.gradeModal}
onClose={toggleGradeModal}
columns={gradeHistoryCols}
combos={combos}
/>
}
{state.positionModal &&
<PositionHistoryModal
open={state.positionModal}
onClose={togglePositionModal}
columns={positionHistoryCols}
combos={combos}
/>
}
</Card>


+ 291
- 143
src/components/EditStaff/TeamHistoryModal.tsx View File

@@ -1,20 +1,22 @@
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material";
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useEffect, useMemo, useState } from "react";
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from "@mui/x-data-grid";
import AddIcon from '@mui/icons-material/Add';
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditInputCell, GridColDef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid";
import SaveIcon from '@mui/icons-material/Save';
import DeleteIcon from '@mui/icons-material/Delete';
import CancelIcon from '@mui/icons-material/Cancel';
import EditIcon from '@mui/icons-material/Edit';
import { useFormContext } from "react-hook-form";
import { useForm, useFormContext } from "react-hook-form";
import { Add } from "@mui/icons-material";
import { CreateStaffInputs, teamHistory } from "@/app/api/staff/actions";
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates";
import { comboItem } from "../CreateStaff/CreateStaff";
import waitForCondition from "../utils/waitFor";

interface Props {
open: boolean;
onClose: () => void;
columns: any[]
combos: comboItem;
}

const modalSx: SxProps = {
@@ -31,172 +33,279 @@ interface Props {
gap: 2,
};

const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => {
export type TeamModalRow = Partial<
teamHistory & {
_isNew: boolean
_error: StaffEntryError;
}>

export class ProcessRowUpdateError<T> extends Error {
public readonly prevRow: T | null;
public readonly currRow: T;
public readonly errors: StaffEntryError | undefined;
constructor(
prevRow: T | null,
currRow: T,
message?: string,
errors?: StaffEntryError,
) {
super(message);
this.prevRow = prevRow;
this.currRow = currRow;
this.errors = errors;
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
}
}

const thisField = "teamHistory"
const TeamHistoryModal: React.FC<Props> = ({
open,
onClose,
combos,
}) => {
const {
t,
// i18n: { language },
} = useTranslation();
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext();
const { setValue, getValues } = useFormContext();
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [count, setCount] = useState(0);
const apiRef = useGridApiRef()
const [_rows, setRows] = useState(() => {
const list = getValues('teamHistory')
const list: TeamModalRow[] = getValues(thisField)
return list && list.length > 0 ? list : []
});
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows])
const [_delRows, setDelRows] = useState<number[]>([]);
const formValues = watch();
const getRowId = useCallback<GridRowIdGetter<TeamModalRow>>(
(row) => row.id!!,
[],
);

const handleClose = () => {
onClose();
};
const looping = async () => {
for (let i = 0; i < _rows.length; i++) {
const id = _rows[i].id
setRowModesModel((prevRowModesModel) => ({
const handleSave = useCallback(
(id: GridRowId) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
},
[setRowModesModel]
);

const onCancel = useCallback(() => {
setRows(originalRows)
onClose();
}, [onClose, originalRows]);

const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>(
(_, reason) => {
if (reason !== "backdropClick") {
onClose();
}
}, [onClose]);

const isSaved = useCallback(() => {
const saved = Object.keys(rowModesModel).every(key => {
rowModesModel[key].mode === GridRowModes.Edit
})
return saved
}, [rowModesModel])

const doSave = useCallback(async () => {
try {
if (isSaved()) {
setValue(thisField, _rows)
onClose()
}
} catch (error) {
console.error(error);
}
}, [isSaved, onClose, _rows]);
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError<TeamModalRow>) => {
const errors = updateError.errors;
const currRow = updateError.currRow;
console.log(errors)
apiRef.current.updateRows([{ ...currRow, _error: errors }]);
},
[apiRef, rowModesModel],
);
const processRowUpdate = useCallback((
newRow: GridRowModel<TeamModalRow>,
originalRow: GridRowModel<TeamModalRow>
) => {
const rowIndex = _rows.findIndex((row: TeamModalRow) => row.id === newRow.id);
const prevRow: TeamModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null;
const errors = validateRowAndRowBefore(prevRow, newRow)
console.log(errors)
if (errors) {
throw new ProcessRowUpdateError(
prevRow,
newRow,
"validation error",
errors
)
}
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
}
return true;
}
const handleSaveAll = async () => {
// trigger save all
console.log(_rows)
await waitForCondition(async () => {
return looping()
})
console.log(rowModesModel)
};
const bigTesting = async () => {
await looping()
setTimeout(() => {
onClose()
}, 800)
if (_rows.length != 0) {
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => {
if (!a.from || !b.from) return 0;
return new Date(a.from).getTime() - new Date(b.from).getTime();
}));
}
return rowToSave;
}
const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
}
};
// handle row update here
const processRowUpdate =
// useCallback(
(newRow: GridRowModel) => {
console.log(newRow)
const updatedRow = { ...newRow, updated: true };
console.log(_rows)
if (_rows.length != 0) {
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row)));
, [validateRowAndRowBefore, _rows])

const addRow = useCallback(() => {
const newEntry = { id: Date.now(), _isNew: true } satisfies TeamModalRow;
setRows((prev) => [...prev, newEntry])
setRowModesModel((model) => ({
...model,
[getRowId(newEntry)]: {
mode: GridRowModes.Edit,
fieldToFocus: "team",
}
return updatedRow;
}
// , [_rows, setValue, setRows])
}))
}, [getRowId]);

const handleSaveClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.View }
}));
},
[setRowModesModel]
);
const handleCancelClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
const handleCancel = useCallback(
(id: GridRowId) => () => {
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.View, ignoreModifications: true }
}));
const editedRow = _rows.find((row) => getRowId(row) === id);
console.log(editedRow)
if (editedRow?._isNew) {
setRows((rw) => rw.filter((r) => r.id !== id))
} else {
setRows((rw) =>
rw.map((r) =>
getRowId(r) === id
? { ...r, _error: undefined }
: r,
),
);
}
},
[setRowModesModel]
);

const handleEditClick = useCallback(
(id: any) => () => {
setRowModesModel((prevRowModesModel) => ({
...prevRowModesModel,
[id]: { mode: GridRowModes.Edit }
}));
},
[setRowModesModel]
[setRowModesModel, _rows]
);

const handleDeleteClick = useCallback(
(id: any) => () => {
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id));
setCount((prev: number) => prev - 1);
setDelRows((prevRowsId: number[]) => [...prevRowsId, id])
const handleDelete = useCallback(
(id: GridRowId) => () => {
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id));
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number])
},
[setRows, setCount, setDelRows]
[]
);
useEffect(()=> {
console.log(_rows)
setValue('teamHistory', _rows)
// setValue(thisField, _rows)
setValue('delTeamHistory', _delRows)
}, [_rows, _delRows])

const defaultCol = useMemo(
() => (
{
field: 'actions',
type: 'actions',
headerName: 'edit',
width: 100,
cellClassName: 'actions',
getActions: ({ id }: { id: number }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
key="edit"
sx={{
color: 'primary.main'
}}
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancelClick(id)}
/>
];
}
const columns = useMemo<GridColDef[]>(
() => [
{
field: 'team',
headerName: 'team',
flex: 1,
editable: true,
type: 'singleSelect',
valueOptions: combos.team.map(item => item.label),
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = (
<GridEditSingleSelectCell variant="outlined" {...params} />
);
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'from',
headerName: 'from',
flex: 1,
editable: true,
type: 'date',
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) {
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError]
const content = <GridEditDateCell {...params} />;
return errorMessage ? (
<Tooltip title={errorMessage}>
<Box width="100%">{content}</Box>
</Tooltip>
) : (
content
);
}
},
{
field: 'actions',
type: 'actions',
headerName: 'edit',
width: 100,
cellClassName: 'actions',
getActions: ({ id }: { id: number }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
icon={<SaveIcon />}
label="Save"
key="edit"
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
color: 'primary.main'
}}
onClick={handleDeleteClick(id)} color="inherit" key="edit" />
onClick={handleSave(id)}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
key="edit"
onClick={handleCancel(id)}
/>
];
}
return [
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
sx={{
color: 'error.main'
}}
onClick={handleDelete(id)} color="inherit" key="edit" />
];
}
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick]
)
let _columns: any[] = []
if (columns) {
_columns = [...columns, defaultCol]
}
}
], [combos, rowModesModel, handleSave, handleCancel, handleDelete])

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Add Record")}
</Button>
</Box>
)
return (
<Modal open={open} onClose={handleClose}>
<Paper sx={{ ...modalSx }}>
@@ -204,25 +313,48 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) =>
{t('TeamHistoryModal')}
</Typography>
<StyledDataGrid
getRowId={getRowId}
apiRef={apiRef}
rows={_rows}
columns={_columns}
columns={columns}
editMode="row"
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}}
disableColumnMenu
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
// slots={{
// toolbar: EditToolbar
// }}
// slotProps={{
// toolbar: {count, setCount, setRows, setRowModesModel, _columns}
// }}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<TeamModalRow>) => {
let classname = "";
if (params.row._error) {
classname = "hasError"
}
return classname;
}}
slots={{
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
footer: { child: footer },
}}
/>
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button variant="text" onClick={handleClose}>
{t('Cancel')}
<Button variant="text" onClick={onCancel}>
{t('Close')}
</Button>
<Button variant="contained" onClick={bigTesting}>
<Button variant="contained" onClick={doSave}>
{t("Save")}
</Button>
</Box>
@@ -231,4 +363,20 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) =>
</Modal>
)
}
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};
const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some entries!")}</Typography>
</Box>
);
};
export default TeamHistoryModal

+ 61
- 0
src/components/EditStaff/validateDates.ts View File

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

}

Loading…
Cancel
Save