@@ -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; | |||
@@ -8,7 +8,7 @@ import Button, { ButtonProps } from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Tab from "@mui/material/Tab"; | |||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||
import { useRouter } from "next/navigation"; | |||
import { useRouter, useSearchParams } from "next/navigation"; | |||
import React, { | |||
useCallback, | |||
useEffect, | |||
@@ -118,7 +118,7 @@ const hasErrorsInTab = ( | |||
const CreateProject: React.FC<Props> = ({ | |||
isEditMode, | |||
isCopyMode, | |||
draftId, | |||
draftId: initDraftId, | |||
isSubProject, | |||
mainProjects, | |||
defaultInputs, | |||
@@ -139,6 +139,7 @@ const CreateProject: React.FC<Props> = ({ | |||
customerTypes, | |||
abilities, | |||
}) => { | |||
const [draftId, setDraftId] = useState(initDraftId) | |||
const [serverError, setServerError] = useState(""); | |||
const [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation(); | |||
@@ -575,9 +576,17 @@ const CreateProject: React.FC<Props> = ({ | |||
formProps.reset(draftInputs); | |||
}, [draftId, formProps]); | |||
const saveDraft = useCallback(() => { | |||
saveToLocalStorage(draftId || Date.now(), formProps.getValues()); | |||
router.replace("/projects"); | |||
const saveDraft = useCallback(async () => { | |||
const currentTimestamp = Date.now() | |||
saveToLocalStorage(draftId || currentTimestamp, formProps.getValues()); | |||
const success = await successDialog("Save Success", t) | |||
if (success && !draftId) { | |||
setDraftId(currentTimestamp) | |||
} | |||
// router.replace("/projects"); | |||
}, [draftId, formProps, router]); | |||
const handleDeleteDraft = useCallback(() => { | |||
@@ -592,6 +601,21 @@ const CreateProject: React.FC<Props> = ({ | |||
}, t); | |||
}, [draftId, router]); | |||
// Auto click the button | |||
const buttonRef = useRef<HTMLButtonElement>(null) | |||
const searchParams = useSearchParams() | |||
useEffect(() => { | |||
if (buttonRef) { | |||
const autoClickButton = searchParams.get("autoClick") | |||
// const autoClickList = ["start", "complete", "reopen"] | |||
if(autoClickButton && autoClickButton === "true") { | |||
buttonRef.current?.click() | |||
} | |||
} | |||
}, [buttonRef]) | |||
return ( | |||
<> | |||
<FormProvider {...formProps}> | |||
@@ -633,6 +657,7 @@ const CreateProject: React.FC<Props> = ({ | |||
<Stack direction="row" gap={1}> | |||
{/* {!formProps.getValues("projectActualStart") && ( */} | |||
<Button | |||
ref={buttonRef} | |||
name={buttonData.buttonName} | |||
type="submit" | |||
variant="contained" | |||
@@ -770,7 +795,7 @@ const CreateProject: React.FC<Props> = ({ | |||
> | |||
{t("Save Draft")} | |||
</Button> | |||
{draftId && | |||
{draftId && | |||
<Button | |||
variant="outlined" | |||
color="error" | |||
@@ -100,7 +100,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
projectInfo.projectId = null | |||
projectInfo.projectCode = projectInfo.projectCode + "-copy" | |||
projectInfo.projectName = projectInfo.projectName + "-copy" | |||
projectInfo.projectStatus = "" | |||
projectInfo.projectStatus = "" // backend will assign "Pending To Start" status | |||
Object.entries(projectInfo.milestones).forEach(([key, value]) => { | |||
projectInfo.milestones[Number(key)].payments.forEach(({ ...rest}, idx, orig) => { | |||
orig[idx] = { ...rest, id: rest.id * -1 } | |||
@@ -39,6 +39,7 @@ import { | |||
} from "@/app/utils/formatUtil"; | |||
import isDate from "lodash/isDate"; | |||
import BulkAddPaymentModal from "./BulkAddPaymentModal"; | |||
import { deleteDialog } from "../Swal/CustomAlerts"; | |||
interface Props { | |||
taskGroupId: TaskGroup["id"]; | |||
@@ -292,6 +293,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
setPayments((currentPayments) => [...currentPayments, ...entries]); | |||
setBulkAddModalOpen(false); | |||
}, []); | |||
const onBulkDelete = useCallback(() => { | |||
deleteDialog(() => { | |||
setPayments([]) | |||
}, t) | |||
}, []); | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
@@ -311,6 +317,15 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
> | |||
{t("Bulk Add Payment Milestones")} | |||
</Button> | |||
<Button | |||
variant="outlined" | |||
startIcon={<Delete />} | |||
color={"error"} | |||
onClick={onBulkDelete} | |||
size="small" | |||
> | |||
{t("Delete Payment Milestones")} | |||
</Button> | |||
</Box> | |||
); | |||
@@ -477,7 +477,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<NumericFormat | |||
label={t("Expected Total Project Fee")} | |||
fullWidth | |||
prefix="$" | |||
prefix="HK$" | |||
onValueChange={(values) => { | |||
// console.log(values) | |||
onChange(values.floatValue) | |||
@@ -513,7 +513,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<NumericFormat | |||
label={t("Sub-Contract Fee")} | |||
fullWidth | |||
prefix="$" | |||
prefix="HK$" | |||
onValueChange={(values) => { | |||
// console.log(values) | |||
onChange(values.floatValue) | |||
@@ -575,7 +575,8 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
control={control} | |||
options={allCustomers.map((customer) => ({ | |||
...customer, | |||
label: `${customer.code} - ${customer.name}`, | |||
label: `${customer.name}`, | |||
// label: `${customer.code} - ${customer.name}`, | |||
}))} | |||
name="clientId" | |||
label={t("Client")} | |||
@@ -624,7 +625,8 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
const subsidiary = subsidiaryMap[subsidiaryId]; | |||
return { | |||
id: subsidiary.id, | |||
label: `${subsidiary.code} - ${subsidiary.name}`, | |||
label: `${subsidiary.name}`, | |||
// label: `${subsidiary.code} - ${subsidiary.name}`, | |||
}; | |||
}), | |||
]} | |||
@@ -10,9 +10,11 @@ import { | |||
} from "react-hook-form"; | |||
import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; | |||
import { Button, Stack, Typography } from "@mui/material"; | |||
import { comboProp} from "@/app/api/companys/actions"; | |||
import { comboProp } from "@/app/api/companys/actions"; | |||
import StaffInfo from "./StaffInfo"; | |||
import { Check, Close } from "@mui/icons-material"; | |||
import dayjs from "dayjs"; | |||
import { SalaryEffectiveInfo } from "@/app/api/staff"; | |||
interface Field { | |||
id: string; | |||
@@ -45,15 +47,29 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
const { t } = useTranslation(); | |||
const formProps = useForm<CreateStaffInputs>(); | |||
const [serverError, setServerError] = useState(""); | |||
const [errorMsg, setErrorMsg] = useState("An error has occurred. Please try again later.") | |||
const router = useRouter(); | |||
const [tabIndex, setTabIndex] = useState(0); | |||
// const [tabIndex, setTabIndex] = useState(0); | |||
const errors = formProps.formState.errors; | |||
function chopSalaryPoints(input: string | number): number | null { | |||
if (typeof input === 'string') { | |||
const match = input.match(/(\d+) \((\d+) - (\d+)\)/); | |||
if (match) { | |||
return parseInt(match[1], 10); | |||
} | |||
} else if (typeof input === 'number') { | |||
return input; | |||
} | |||
return null; | |||
} | |||
const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>( | |||
async (data) => { | |||
try { | |||
console.log(data); | |||
formProps.clearErrors() | |||
let haveError = false; | |||
const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ | |||
const regex_phone = /^\d{8}$/ | |||
@@ -96,20 +112,71 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
haveError = true | |||
formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | |||
} | |||
if (!data.salaryId) { | |||
if (data.joinDate && data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
haveError = true | |||
formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) | |||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
} | |||
if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
if (!data.salaryEffectiveInfo || data.salaryEffectiveInfo.length < 1) { | |||
haveError = true | |||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
formProps.setError("salaryId", { message: t("Please Enter Salary"), type: "required" }) | |||
} | |||
if (!data.gradeHistory || data.gradeHistory.length < 1) { | |||
haveError = true | |||
formProps.setError("gradeId", { message: t("Please Enter Grade"), type: "required" }) | |||
} | |||
if (!data.positionHistory || data.positionHistory.length < 1) { | |||
console.log("asdadsasd") | |||
haveError = true | |||
formProps.setError("currentPositionId", { message: t("Please Enter Current Position"), type: "required" }) | |||
} | |||
if (haveError) { | |||
return | |||
} | |||
const teamHistory = data.teamHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
team: combos.team.filter(team => team.label === item.team)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const gradeHistory = data.gradeHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
console.log(data.positionHistory) | |||
const positionHistory = data.positionHistory | |||
.map((item) => ({ | |||
id: item.id, | |||
position: combos.position.filter(position => position.label === item.position)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ | |||
id: item.id, | |||
salaryPoint: chopSalaryPoints(item.salaryPoint), | |||
date: dayjs(item.date).format('YYYY-MM-DD').toString() | |||
})) // backend sort | |||
const postData: CreateStaffInputs = { | |||
// id: id, | |||
...data, | |||
salaryEffectiveInfo: salaryEffectiveInfo, | |||
teamHistory: teamHistory ?? [], | |||
gradeHistory: gradeHistory ?? [], | |||
positionHistory: positionHistory ?? [], | |||
delTeamHistory: data.delTeamHistory ? data.delTeamHistory : [], | |||
delGradeHistory: data.delGradeHistory ? data.delGradeHistory : [], | |||
delPositionHistory: data.delPositionHistory ? data.delPositionHistory : [], | |||
} | |||
console.log("passed") | |||
console.log(data) | |||
await saveStaff(data) | |||
console.log(postData) | |||
// return | |||
await saveStaff(postData) | |||
router.replace("/settings/staff") | |||
} catch (e: any) { | |||
console.log(e); | |||
@@ -118,15 +185,19 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
if (e.message === "Duplicated StaffId Found") { | |||
msg = t("Duplicated StaffId Found") | |||
} | |||
setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `); | |||
setServerError(`${t(errorMsg)} ${msg} `); | |||
} | |||
}, | |||
[router] | |||
[errorMsg, router] | |||
); | |||
const handleCancel = () => { | |||
const errorKey = Object.keys(formProps.formState.errors)[0] | |||
const err = errors[errorKey as keyof CreateStaffInputs] | |||
const handleCancel = useCallback(() => { | |||
router.back(); | |||
}; | |||
}, [router]); | |||
return ( | |||
<> | |||
@@ -141,6 +212,12 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
{serverError} | |||
</Typography> | |||
)} | |||
{err && ( | |||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||
{err.message?.toString()} | |||
</Typography> | |||
) | |||
} | |||
<StaffInfo combos={combos}/> | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
@@ -9,9 +9,10 @@ import Typography from "@mui/material/Typography"; | |||
import { CreateGroupInputs } from "@/app/api/group/actions"; | |||
import { Controller, useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import { useCallback, useEffect } from "react"; | |||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react"; | |||
import { CreateStaffInputs } from "@/app/api/staff/actions"; | |||
import { | |||
Button, | |||
Checkbox, | |||
FormControl, | |||
InputLabel, | |||
@@ -25,11 +26,52 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | |||
import dayjs from "dayjs"; | |||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
import TableModal from "./TableModal"; | |||
import { Preview } from "@mui/icons-material"; | |||
import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel"; | |||
import TeamHistoryModal from "../EditStaff/TeamHistoryModal"; | |||
import GradeHistoryModal from "../EditStaff/GradeHistoryModal"; | |||
import PositionHistoryModal from "../EditStaff/PositionHistoryModal"; | |||
interface Props { | |||
combos: comboItem; | |||
} | |||
type tableKey = "salary" | "team" | "grade" | "position"; | |||
//// | |||
const initState = { | |||
teamModal: false, | |||
seModal: false, | |||
gradeModal: false, | |||
positionModal: false, | |||
} | |||
const enum REDUCER_ACTION_TYPE { | |||
TOGGLE_TEAM_MODAL, | |||
TOGGLE_SALARY_EFFECTIVE_MODAL, | |||
TOGGLE_GRADE_MODAL, | |||
TOGGLE_POSITION_MODAL, | |||
} | |||
type ReducerAction = { | |||
type: REDUCER_ACTION_TYPE | |||
} | |||
const reducer = (state: typeof initState, action: ReducerAction): typeof initState => { | |||
switch (action.type) { | |||
case REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL: | |||
return { ...state, teamModal: !state.teamModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL: | |||
return { ...state, seModal: !state.seModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL: | |||
return { ...state, gradeModal: !state.gradeModal }; | |||
case REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL: | |||
return { ...state, positionModal: !state.positionModal }; | |||
default: | |||
return state; | |||
} | |||
} | |||
///// | |||
const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
const { | |||
t, | |||
@@ -45,6 +87,14 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
getValues, | |||
clearErrors | |||
} = useFormContext<CreateStaffInputs>(); | |||
const [tableKey, setTableKey] = useState<tableKey>() | |||
const [isOpen, setIsOpen] = useState(false) | |||
const [state, dispatch] = useReducer(reducer, initState) | |||
const toggleSeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_SALARY_EFFECTIVE_MODAL}) | |||
const toggleTeamModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_TEAM_MODAL}) | |||
const toggleGradeModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_GRADE_MODAL}) | |||
const togglePositionModal = () => dispatch({ type: REDUCER_ACTION_TYPE.TOGGLE_POSITION_MODAL}) | |||
const employType = [ | |||
{ id: 1, label: "FT" }, | |||
@@ -64,6 +114,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
} | |||
}, [defaultValues]); | |||
const toggleModal = useCallback((key: tableKey) => { | |||
setIsOpen(!isOpen) | |||
setTableKey(key) | |||
}, [isOpen]) | |||
const joinDate = getValues("joinDate"); | |||
const departDate = getValues("departDate"); | |||
@@ -148,17 +203,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
control={control} | |||
name="teamId" | |||
render={({ field }) => ( | |||
<Select | |||
label={t("Team")} | |||
{...field} | |||
// error={Boolean(errors.teamId)} | |||
> | |||
{combos.team.map((team, index) => ( | |||
<MenuItem key={`${team.id}-${index}`} value={team.id}> | |||
{t(team.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Team")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
// error={Boolean(errors.teamId)} | |||
disabled | |||
> | |||
{combos.team.map((team, index) => ( | |||
<MenuItem key={`${team.id}-${index}`} value={team.id}> | |||
{t(team.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button | |||
variant="contained" | |||
size="small" | |||
onClick={toggleTeamModal} | |||
> | |||
{t("Team History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
@@ -191,7 +257,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel required>{t("Grade")}</InputLabel> | |||
<Controller | |||
{/* <Controller | |||
control={control} | |||
name="gradeId" | |||
render={({ field }) => ( | |||
@@ -207,6 +273,30 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
)} | |||
/> */} | |||
<Controller | |||
control={control} | |||
name="gradeId" | |||
render={({ field }) => ( | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Grade")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.gradeId)} | |||
disabled | |||
> | |||
{combos.grade.map((grade, index) => ( | |||
<MenuItem key={`${grade.id}-${index}`} value={grade.id}> | |||
{t(grade.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal}> | |||
{t("Grade History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
@@ -254,9 +344,12 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
control={control} | |||
name="currentPositionId" | |||
render={({ field }) => ( | |||
<Select | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Current Position")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
disabled | |||
error={Boolean(errors.currentPositionId)} | |||
> | |||
{combos.position.map((position, index) => ( | |||
@@ -268,6 +361,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={togglePositionModal}> | |||
{t("Position History")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
@@ -275,7 +372,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
<InputLabel required>{t("Salary Point")}</InputLabel> | |||
<Controller | |||
{/* <Controller | |||
control={control} | |||
name="salaryId" | |||
render={({ field }) => ( | |||
@@ -294,7 +391,35 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
)} | |||
/> | |||
/> */} | |||
<Controller | |||
control={control} | |||
name="salaryId" | |||
render={({ field }) => ( | |||
<Box display="flex" justifyContent="space-between" alignItems="center"> | |||
<Select | |||
label={t("Salary Point")} | |||
{...field} | |||
error={Boolean(errors.salaryId)} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
disabled | |||
> | |||
{combos.salary.map((salary, index) => ( | |||
<MenuItem | |||
key={`${salary.id}-${index}`} | |||
value={salary.id} | |||
> | |||
{t(salary.label)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
{/* <Button variant="contained" size="small" onClick={() => toggleModal("salary")}> */} | |||
<Button variant="contained" size="small" onClick={toggleSeModal}> | |||
{t("Edit Salary")} | |||
</Button> | |||
</Box> | |||
)} | |||
/> | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
@@ -512,6 +637,36 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</Grid> | |||
</Box> | |||
</CardContent> | |||
{state.seModal && | |||
<SalaryEffectiveModel | |||
open={state.seModal} | |||
onClose={toggleSeModal} | |||
combos={combos} | |||
// columns={salaryCols} | |||
/> | |||
} | |||
{state.teamModal && | |||
<TeamHistoryModal | |||
open={state.teamModal} | |||
onClose={toggleTeamModal} | |||
combos={combos} | |||
/> | |||
} | |||
{state.gradeModal && | |||
<GradeHistoryModal | |||
open={state.gradeModal} | |||
onClose={toggleGradeModal} | |||
combos={combos} | |||
/> | |||
} | |||
{state.positionModal && | |||
<PositionHistoryModal | |||
open={state.positionModal} | |||
onClose={togglePositionModal} | |||
combos={combos} | |||
/> | |||
} | |||
{/* {tableKey && <TableModal tableKey={tableKey} isOpen={isOpen} onClose={() => toggleModal("team")} combos={combos}/>} */} | |||
</Card> | |||
); | |||
}; | |||
@@ -36,6 +36,7 @@ interface Props<EntryTableProps = object> { | |||
EntryTableProps & { day: string; isHoliday: boolean } | |||
>; | |||
entryTableProps: EntryTableProps; | |||
isSaturdayWorker: boolean | |||
} | |||
function DateHoursTable<EntryTableProps>({ | |||
@@ -45,9 +46,9 @@ function DateHoursTable<EntryTableProps>({ | |||
leaveEntries, | |||
timesheetEntries, | |||
companyHolidays, | |||
isSaturdayWorker, | |||
}: Props<EntryTableProps>) { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<TableContainer sx={{ maxHeight: 400 }}> | |||
<Table stickyHeader> | |||
@@ -71,6 +72,7 @@ function DateHoursTable<EntryTableProps>({ | |||
timesheetEntries={timesheetEntries} | |||
EntryTableComponent={EntryTableComponent} | |||
entryTableProps={entryTableProps} | |||
isSaturdayWorker={isSaturdayWorker} | |||
/> | |||
); | |||
})} | |||
@@ -87,6 +89,7 @@ function DayRow<EntryTableProps>({ | |||
entryTableProps, | |||
EntryTableComponent, | |||
companyHolidays, | |||
isSaturdayWorker, | |||
}: { | |||
day: string; | |||
companyHolidays: HolidaysResult[]; | |||
@@ -96,16 +99,18 @@ function DayRow<EntryTableProps>({ | |||
EntryTableProps & { day: string; isHoliday: boolean } | |||
>; | |||
entryTableProps: EntryTableProps; | |||
isSaturdayWorker: boolean | |||
}) { | |||
const { | |||
t, | |||
i18n: { language }, | |||
} = useTranslation("home"); | |||
const dayJsObj = dayjs(day); | |||
const [open, setOpen] = useState(false); | |||
const [open, setOpen] = useState(false); | |||
const holiday = getHolidayForDate(day, companyHolidays); | |||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; | |||
const isHoliday = holiday || isWeekend; | |||
const leaves = leaveEntries[day]; | |||
const leaveHours = | |||
@@ -88,7 +88,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
team: item.team.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delTeamHistory: [], | |||
@@ -97,7 +97,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
grade: item.grade.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delGradeHistory: [], | |||
@@ -106,7 +106,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
position: item.position.name, | |||
from: dayjs(item.from.join()).toDate(), | |||
to: item.to ? dayjs(item.to.join()).toDate() : "", | |||
to: item.to ? dayjs(item.to.join()).toDate() : undefined, | |||
}) | |||
}) : [], | |||
delPositionHistory: [], | |||
@@ -180,21 +180,26 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos, SalaryEffectiveInfo, In | |||
id: item.id, | |||
team: combos.team.filter(team => team.label === item.team)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const gradeHistory = data.gradeHistory.map((item) => ({ | |||
id: item.id, | |||
grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const positionHistory = data.positionHistory.map((item) => ({ | |||
id: item.id, | |||
position: combos.position.filter(position => position.label === item.position)[0].id, | |||
from: dayjs(item.from).format('YYYY-MM-DD'), | |||
to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
// to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, | |||
})) | |||
.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) | |||
const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ | |||
id: item.id, | |||
salaryPoint: chopSalaryPoints(item.salaryPoint), | |||
@@ -1,20 +1,26 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" | |||
import StyledDataGrid from "../StyledDataGrid" | |||
import { useTranslation } from "react-i18next"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
import DeleteIcon from '@mui/icons-material/Delete'; | |||
import CancelIcon from '@mui/icons-material/Cancel'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import waitForCondition from "../utils/waitFor"; | |||
import { gradeHistory } from "@/app/api/staff/actions"; | |||
import { Add } from "@mui/icons-material"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { ProcessRowUpdateError } from "./TeamHistoryModal"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
// columns: any[] | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,119 +37,217 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type GradeModalRow = Partial< | |||
gradeHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
const thisField = "gradeHistory" | |||
const GradeHistoryModal: React.FC<Props> = ({ open, onClose, combos }) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const originalRows = getValues(thisField) | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('gradeHistory') | |||
const list: GradeModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const getRowId = useCallback<GridRowIdGetter<GradeModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}; | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]) | |||
const addRow = useCallback(() => { | |||
const id = Date.now() | |||
const newEntry = { id, _isNew: true } satisfies GradeModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "grade", | |||
} | |||
return updatedRow; | |||
})) | |||
}, []); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<GradeModalRow>) => { | |||
const errors = updateError.errors; | |||
// const prevRow = updateError.prevRow; | |||
const currRow = updateError.currRow; | |||
// if (updateError.prevRow) { | |||
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]); | |||
// } | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<GradeModalRow>, | |||
originalRow: GridRowModel<GradeModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: GradeModalRow) => row.id === newRow.id); | |||
const prevRow: GradeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return rowToSave; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
, [_rows, validateRowAndRowBefore]) | |||
const handleSaveClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
const handleCancel = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((r) => getRowId(r) === id) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('gradeHistory', _rows) | |||
// setValue(thisField, _rows) | |||
setValue('delGradeHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
const defaultCol = useMemo( | |||
() => ( | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'grade', | |||
headerName: 'grade', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.grade.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<GradeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
@@ -161,42 +265,29 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -204,25 +295,48 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
{t('GradeHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<GradeModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -231,4 +345,20 @@ const GradeHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default GradeHistoryModal |
@@ -1,20 +1,26 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" | |||
import StyledDataGrid from "../StyledDataGrid" | |||
import { useTranslation } from "react-i18next"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
import DeleteIcon from '@mui/icons-material/Delete'; | |||
import CancelIcon from '@mui/icons-material/Cancel'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import waitForCondition from "../utils/waitFor"; | |||
import { Add } from "@mui/icons-material"; | |||
import { positionHistory } from "@/app/api/staff/actions"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { ProcessRowUpdateError } from "./TeamHistoryModal"; | |||
import { createSearchParamsBailoutProxy } from "next/dist/client/components/searchparams-bailout-proxy"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,171 +37,267 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type PositionModalRow = Partial< | |||
positionHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
const thisField = "positionHistory" | |||
const PositionHistoryModal: React.FC<Props> = ({ | |||
open, | |||
onClose, | |||
combos | |||
}) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const originalRows = getValues(thisField) | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('positionHistory') | |||
const list: PositionModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const getRowId = useCallback<GridRowIdGetter<PositionModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
console.log(originalRows) | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
}; | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<PositionModalRow>) => { | |||
const errors = updateError.errors; | |||
const prevRow = updateError.prevRow; | |||
const currRow = updateError.currRow; | |||
// if (updateError.prevRow) { | |||
// apiRef.current.updateRows([{ ...prevRow, _error: errors }]); | |||
// } | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<PositionModalRow>, | |||
originalRow: GridRowModel<PositionModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: PositionModalRow) => row.id === newRow.id); | |||
const prevRow: PositionModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return updatedRow; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
const handleSaveClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
return rowToSave; | |||
} | |||
, [_rows, validateRowAndRowBefore]) | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies PositionModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "position", | |||
} | |||
})) | |||
}, [getRowId]); | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((row) => getRowId(row) === id); | |||
console.log(editedRow) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[setRows, setDelRows] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('positionHistory', _rows) | |||
setValue(thisField, _rows) | |||
setValue('delPositionHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
const defaultCol = useMemo( | |||
() => ( | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: 'edit', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }: { id: number }) => { | |||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
if (isInEditMode) { | |||
return [ | |||
<GridActionsCellItem | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
/> | |||
]; | |||
} | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'position', | |||
headerName: 'position', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.position.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<PositionModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: 'edit', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }: { id: number }) => { | |||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
if (isInEditMode) { | |||
return [ | |||
<GridActionsCellItem | |||
icon={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
color: 'primary.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -203,25 +305,48 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||
{t('PositionHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<PositionModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -230,4 +355,22 @@ const PositionHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default PositionHistoryModal |
@@ -1,11 +1,11 @@ | |||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | |||
import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material'; | |||
import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps, ModalProps, Tooltip } from '@mui/material'; | |||
import { useForm, Controller, useFormContext } from 'react-hook-form'; | |||
import { useTranslation } from 'react-i18next'; | |||
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | |||
import dayjs from 'dayjs'; | |||
import { DatePicker } from '@mui/x-date-pickers'; | |||
import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid'; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditSingleSelectCell, GridCellParams, GridRowId } from '@mui/x-data-grid'; | |||
import StyledDataGrid from '../StyledDataGrid'; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
@@ -14,12 +14,17 @@ import CancelIcon from '@mui/icons-material/Cancel'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import { GridActionsCellItem } from '@mui/x-data-grid'; | |||
import waitForCondition from '../utils/waitFor'; | |||
import { StaffEntryError, validateRowAndRowBefore } from './validateDates'; | |||
import { salaryEffectiveInfo } from '@/app/api/staff/actions'; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import { Add } from '@mui/icons-material'; | |||
import { GridEditDateCell } from '@mui/x-data-grid'; | |||
interface SalaryEffectiveModelProps { | |||
open: boolean; | |||
onClose: () => void; | |||
modalSx?: SxProps; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -36,125 +41,133 @@ const modalSx: SxProps = { | |||
gap: 2, | |||
}; | |||
function EditToolbar(props: React.JSXElementConstructor<any> | null | undefined | any) { | |||
// const intl = useIntl(); | |||
// const addRecordBtn = intl.formatMessage({ id: 'add' }); | |||
const { count, setCount, setRows, setRowModesModel, _columns } = props; | |||
let obj: { [key: string]: string } = {}; | |||
for (let i = 0; i < _columns.length - 1; i++) { | |||
obj[_columns[i].field as string] = ''; | |||
} | |||
const handleClick = React.useCallback(() => { | |||
const id = Math.random(); | |||
setRows((oldRows: any) => [...oldRows, { id, ...obj, isNew: true }]); | |||
setRowModesModel((oldModel: any) => ({ | |||
...oldModel, | |||
[id]: { mode: GridRowModes.Edit, | |||
// fieldToFocus: 'material' | |||
} | |||
})); | |||
setCount((prev: number) => prev+1) | |||
}, [count, setCount, setRowModesModel, setRows]) | |||
return ( | |||
<GridToolbarContainer> | |||
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}> | |||
{"addRecordBtn"} | |||
</Button> | |||
{/* <Button color="primary" startIcon={<AddIcon />} onClick={handleSave}> | |||
SAVE | |||
</Button> */} | |||
</GridToolbarContainer> | |||
); | |||
} | |||
export type SeModalRow = Partial< | |||
salaryEffectiveInfo & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
} | |||
> | |||
export class ProcessRowUpdateError<T> extends Error { | |||
public readonly prevRow: T | null; | |||
public readonly currRow: T; | |||
public readonly errors: StaffEntryError | undefined; | |||
constructor( | |||
prevRow: T | null, | |||
currRow: T, | |||
message?: string, | |||
errors?: StaffEntryError, | |||
) { | |||
super(message); | |||
this.prevRow = prevRow; | |||
this.currRow = currRow; | |||
this.errors = errors; | |||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, columns }) => { | |||
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
} | |||
} | |||
const thisField = "salaryEffectiveInfo" | |||
const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClose, modalSx: mSx, combos }) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('salaryEffectiveInfo') | |||
const list: SeModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); // This line of code is using the watch function from react-hook-form to get the current values of the form fields. | |||
const getRowId = useCallback<GridRowIdGetter<SeModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
return saved | |||
}, [rowModesModel]) | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
} | |||
// const handleSave = async () => { | |||
// const isValid = await trigger(); | |||
// // if (isValid) { | |||
// // onSave(); | |||
// // onClose(); | |||
// // } | |||
// }; | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<SeModalRow>) => { | |||
const errors = updateError.errors; | |||
const currRow = updateError.currRow; | |||
console.log(errors) | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<SeModalRow>, | |||
originalRow: GridRowModel<SeModalRow> | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
const rowIndex = _rows.findIndex((row: SeModalRow) => row.id === newRow.id); | |||
const prevRow: SeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
_isNew, | |||
} | |||
}; | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.date || !b.date) return 0; | |||
return new Date(a.date).getTime() - new Date(b.date).getTime(); | |||
})); | |||
} | |||
return updatedRow; | |||
return rowToSave; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
, [validateRowAndRowBefore, _rows]) | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('salaryEffectiveInfo', _rows) | |||
}, [_rows]) | |||
// useEffect(()=> { | |||
// console.log(_rows) | |||
// setValue(thisField, _rows) | |||
// }, [_rows]) | |||
useEffect(()=> { | |||
console.log(_delRows) | |||
setValue('delSalaryEffectiveInfo', _delRows) | |||
}, [_delRows]) | |||
const handleSaveClick = useCallback((id: any) => () => { | |||
const handleSave = useCallback((id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
@@ -163,17 +176,8 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
[setRowModesModel] | |||
); | |||
const handleSaveClickAsync = useCallback(async(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
(id: any) => () => { | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
@@ -182,27 +186,56 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row: any) => row.id !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRowModesModel] | |||
[] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
); | |||
const defaultCol = useMemo( | |||
() => ( | |||
const columns = useMemo( | |||
() => [ | |||
{ | |||
field: 'salaryPoint', | |||
headerName: 'salaryPoint', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.salary.map((item) => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'date', | |||
headerName: 'date', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<SeModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
@@ -220,47 +253,54 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies SeModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "team", | |||
} | |||
})) | |||
}, [getRowId]); | |||
useEffect(() => { | |||
console.log(_rows) | |||
}, [_rows]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
@@ -269,33 +309,70 @@ const SalaryEffectiveModel: React.FC<SalaryEffectiveModelProps> = ({ open, onClo | |||
{t('Salary Effective Date Change')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
processRowUpdate={processRowUpdate} | |||
getCellClassName={(params: GridCellParams<SeModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
toolbar: EditToolbar | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Cancel')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
{/* <Button variant="contained" onClick={handleSaveAll}> */} | |||
{t("Save")} | |||
</Button> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
{/* </FormControl> */} | |||
</Paper> | |||
</Modal> | |||
); | |||
}; | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default SalaryEffectiveModel; |
@@ -113,10 +113,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
useEffect(() => { | |||
resetStaff() | |||
}, [defaultValues]); | |||
// useEffect(() => { | |||
// console.log(state) | |||
// }, [state]); | |||
const joinDate = watch("joinDate"); | |||
const departDate = watch("departDate"); | |||
@@ -126,112 +122,6 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
if (departDate) clearErrors("departDate"); | |||
}, [joinDate, departDate]); | |||
const salaryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'salaryPoint', | |||
headerName: 'salaryPoint', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.salary.map((item) => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'date', | |||
headerName: 'date', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
// width: 150 | |||
}, | |||
], [combos]) | |||
const teamHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'team', | |||
headerName: 'team', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.team.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
const gradeHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'grade', | |||
headerName: 'grade', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.grade.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
const positionHistoryCols = useMemo( | |||
() => [ | |||
{ | |||
field: 'position', | |||
headerName: 'position', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.position.map(item => item.label), | |||
// valueOptions: [], | |||
// width: 150 | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
{ | |||
field: 'to', | |||
headerName: 'to', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
}, | |||
], [combos]) | |||
return ( | |||
<Card sx={{ display: "block" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
@@ -311,6 +201,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
label={t("Team")} | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
disabled | |||
// error={Boolean(errors.teamId)} | |||
> | |||
{combos.team.map((team, index) => ( | |||
@@ -320,7 +211,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleTeamModal} | |||
disabled={getValues("teamHistory").length == 0} | |||
// disabled={getValues("teamHistory").length == 0} | |||
> | |||
{t("Team History")} | |||
</Button> | |||
@@ -367,6 +258,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.gradeId)} | |||
disabled | |||
> | |||
{combos.grade.map((grade, index) => ( | |||
<MenuItem key={`${grade.id}-${index}`} value={grade.id}> | |||
@@ -374,8 +266,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal} | |||
disabled={getValues("gradeHistory").length == 0}> | |||
<Button variant="contained" size="small" onClick={toggleGradeModal}> | |||
{t("Grade History")} | |||
</Button> | |||
</Box> | |||
@@ -432,6 +323,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
style={{ flex: 1, marginRight: '8px' }} | |||
{...field} | |||
error={Boolean(errors.currentPositionId)} | |||
disabled | |||
> | |||
{combos.position.map((position, index) => ( | |||
<MenuItem | |||
@@ -442,8 +334,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
</MenuItem> | |||
))} | |||
</Select> | |||
<Button variant="contained" size="small" onClick={togglePositionModal} | |||
disabled={getValues("positionHistory").length == 0}> | |||
<Button variant="contained" size="small" onClick={togglePositionModal}> | |||
{t("Position History")} | |||
</Button> | |||
</Box> | |||
@@ -656,7 +547,10 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
label={t("Depart Date")} | |||
value={departDate ? dayjs(departDate) : null} | |||
onChange={(date) => { | |||
if (!date) return; | |||
if (!date) { | |||
setValue("departDate", null); | |||
return | |||
}; | |||
dayjs(date).add(1, 'month') | |||
setValue("departDate", date.format(INPUT_DATE_FORMAT)); | |||
}} | |||
@@ -707,28 +601,28 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
<SalaryEffectiveModel | |||
open={state.seModal} | |||
onClose={toggleSeModal} | |||
columns={salaryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.teamModal && | |||
<TeamHistoryModal | |||
open={state.teamModal} | |||
onClose={toggleTeamModal} | |||
columns={teamHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.gradeModal && | |||
<GradeHistoryModal | |||
open={state.gradeModal} | |||
onClose={toggleGradeModal} | |||
columns={gradeHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
{state.positionModal && | |||
<PositionHistoryModal | |||
open={state.positionModal} | |||
onClose={togglePositionModal} | |||
columns={positionHistoryCols} | |||
combos={combos} | |||
/> | |||
} | |||
</Card> | |||
@@ -1,20 +1,22 @@ | |||
import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material"; | |||
import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material"; | |||
import { useTranslation } from "react-i18next"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from "@mui/x-data-grid"; | |||
import AddIcon from '@mui/icons-material/Add'; | |||
import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditInputCell, GridColDef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; | |||
import SaveIcon from '@mui/icons-material/Save'; | |||
import DeleteIcon from '@mui/icons-material/Delete'; | |||
import CancelIcon from '@mui/icons-material/Cancel'; | |||
import EditIcon from '@mui/icons-material/Edit'; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useForm, useFormContext } from "react-hook-form"; | |||
import { Add } from "@mui/icons-material"; | |||
import { CreateStaffInputs, teamHistory } from "@/app/api/staff/actions"; | |||
import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; | |||
import { comboItem } from "../CreateStaff/CreateStaff"; | |||
import waitForCondition from "../utils/waitFor"; | |||
interface Props { | |||
open: boolean; | |||
onClose: () => void; | |||
columns: any[] | |||
combos: comboItem; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -31,172 +33,279 @@ interface Props { | |||
gap: 2, | |||
}; | |||
const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => { | |||
export type TeamModalRow = Partial< | |||
teamHistory & { | |||
_isNew: boolean | |||
_error: StaffEntryError; | |||
}> | |||
export class ProcessRowUpdateError<T> extends Error { | |||
public readonly prevRow: T | null; | |||
public readonly currRow: T; | |||
public readonly errors: StaffEntryError | undefined; | |||
constructor( | |||
prevRow: T | null, | |||
currRow: T, | |||
message?: string, | |||
errors?: StaffEntryError, | |||
) { | |||
super(message); | |||
this.prevRow = prevRow; | |||
this.currRow = currRow; | |||
this.errors = errors; | |||
Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
} | |||
} | |||
const thisField = "teamHistory" | |||
const TeamHistoryModal: React.FC<Props> = ({ | |||
open, | |||
onClose, | |||
combos, | |||
}) => { | |||
const { | |||
t, | |||
// i18n: { language }, | |||
} = useTranslation(); | |||
const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); | |||
const { setValue, getValues } = useFormContext(); | |||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
const [count, setCount] = useState(0); | |||
const apiRef = useGridApiRef() | |||
const [_rows, setRows] = useState(() => { | |||
const list = getValues('teamHistory') | |||
const list: TeamModalRow[] = getValues(thisField) | |||
return list && list.length > 0 ? list : [] | |||
}); | |||
const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) | |||
const [_delRows, setDelRows] = useState<number[]>([]); | |||
const formValues = watch(); | |||
const getRowId = useCallback<GridRowIdGetter<TeamModalRow>>( | |||
(row) => row.id!!, | |||
[], | |||
); | |||
const handleClose = () => { | |||
onClose(); | |||
}; | |||
const looping = async () => { | |||
for (let i = 0; i < _rows.length; i++) { | |||
const id = _rows[i].id | |||
setRowModesModel((prevRowModesModel) => ({ | |||
const handleSave = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const onCancel = useCallback(() => { | |||
setRows(originalRows) | |||
onClose(); | |||
}, [onClose, originalRows]); | |||
const handleClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(_, reason) => { | |||
if (reason !== "backdropClick") { | |||
onClose(); | |||
} | |||
}, [onClose]); | |||
const isSaved = useCallback(() => { | |||
const saved = Object.keys(rowModesModel).every(key => { | |||
rowModesModel[key].mode === GridRowModes.Edit | |||
}) | |||
return saved | |||
}, [rowModesModel]) | |||
const doSave = useCallback(async () => { | |||
try { | |||
if (isSaved()) { | |||
setValue(thisField, _rows) | |||
onClose() | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
}, [isSaved, onClose, _rows]); | |||
const onProcessRowUpdateError = useCallback( | |||
(updateError: ProcessRowUpdateError<TeamModalRow>) => { | |||
const errors = updateError.errors; | |||
const currRow = updateError.currRow; | |||
console.log(errors) | |||
apiRef.current.updateRows([{ ...currRow, _error: errors }]); | |||
}, | |||
[apiRef, rowModesModel], | |||
); | |||
const processRowUpdate = useCallback(( | |||
newRow: GridRowModel<TeamModalRow>, | |||
originalRow: GridRowModel<TeamModalRow> | |||
) => { | |||
const rowIndex = _rows.findIndex((row: TeamModalRow) => row.id === newRow.id); | |||
const prevRow: TeamModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; | |||
const errors = validateRowAndRowBefore(prevRow, newRow) | |||
console.log(errors) | |||
if (errors) { | |||
throw new ProcessRowUpdateError( | |||
prevRow, | |||
newRow, | |||
"validation error", | |||
errors | |||
) | |||
} | |||
const { _isNew, _error, ...updatedRow } = newRow; | |||
const rowToSave = { | |||
...updatedRow, | |||
} | |||
return true; | |||
} | |||
const handleSaveAll = async () => { | |||
// trigger save all | |||
console.log(_rows) | |||
await waitForCondition(async () => { | |||
return looping() | |||
}) | |||
console.log(rowModesModel) | |||
}; | |||
const bigTesting = async () => { | |||
await looping() | |||
setTimeout(() => { | |||
onClose() | |||
}, 800) | |||
if (_rows.length != 0) { | |||
setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { | |||
if (!a.from || !b.from) return 0; | |||
return new Date(a.from).getTime() - new Date(b.from).getTime(); | |||
})); | |||
} | |||
return rowToSave; | |||
} | |||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
params, | |||
event, | |||
) => { | |||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
event.defaultMuiPrevented = true; | |||
} | |||
}; | |||
// handle row update here | |||
const processRowUpdate = | |||
// useCallback( | |||
(newRow: GridRowModel) => { | |||
console.log(newRow) | |||
const updatedRow = { ...newRow, updated: true }; | |||
console.log(_rows) | |||
if (_rows.length != 0) { | |||
setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); | |||
, [validateRowAndRowBefore, _rows]) | |||
const addRow = useCallback(() => { | |||
const newEntry = { id: Date.now(), _isNew: true } satisfies TeamModalRow; | |||
setRows((prev) => [...prev, newEntry]) | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[getRowId(newEntry)]: { | |||
mode: GridRowModes.Edit, | |||
fieldToFocus: "team", | |||
} | |||
return updatedRow; | |||
} | |||
// , [_rows, setValue, setRows]) | |||
})) | |||
}, [getRowId]); | |||
const handleSaveClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.View } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleCancelClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
const handleCancel = useCallback( | |||
(id: GridRowId) => () => { | |||
setRowModesModel((model) => ({ | |||
...model, | |||
[id]: { mode: GridRowModes.View, ignoreModifications: true } | |||
})); | |||
const editedRow = _rows.find((row) => getRowId(row) === id); | |||
console.log(editedRow) | |||
if (editedRow?._isNew) { | |||
setRows((rw) => rw.filter((r) => r.id !== id)) | |||
} else { | |||
setRows((rw) => | |||
rw.map((r) => | |||
getRowId(r) === id | |||
? { ...r, _error: undefined } | |||
: r, | |||
), | |||
); | |||
} | |||
}, | |||
[setRowModesModel] | |||
); | |||
const handleEditClick = useCallback( | |||
(id: any) => () => { | |||
setRowModesModel((prevRowModesModel) => ({ | |||
...prevRowModesModel, | |||
[id]: { mode: GridRowModes.Edit } | |||
})); | |||
}, | |||
[setRowModesModel] | |||
[setRowModesModel, _rows] | |||
); | |||
const handleDeleteClick = useCallback( | |||
(id: any) => () => { | |||
setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); | |||
setCount((prev: number) => prev - 1); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) | |||
const handleDelete = useCallback( | |||
(id: GridRowId) => () => { | |||
setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||
setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) | |||
}, | |||
[setRows, setCount, setDelRows] | |||
[] | |||
); | |||
useEffect(()=> { | |||
console.log(_rows) | |||
setValue('teamHistory', _rows) | |||
// setValue(thisField, _rows) | |||
setValue('delTeamHistory', _delRows) | |||
}, [_rows, _delRows]) | |||
const defaultCol = useMemo( | |||
() => ( | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: 'edit', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }: { id: number }) => { | |||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
if (isInEditMode) { | |||
return [ | |||
<GridActionsCellItem | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
sx={{ | |||
color: 'primary.main' | |||
}} | |||
onClick={handleSaveClick(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancelClick(id)} | |||
/> | |||
]; | |||
} | |||
const columns = useMemo<GridColDef[]>( | |||
() => [ | |||
{ | |||
field: 'team', | |||
headerName: 'team', | |||
flex: 1, | |||
editable: true, | |||
type: 'singleSelect', | |||
valueOptions: combos.team.map(item => item.label), | |||
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = ( | |||
<GridEditSingleSelectCell variant="outlined" {...params} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'from', | |||
headerName: 'from', | |||
flex: 1, | |||
editable: true, | |||
type: 'date', | |||
renderEditCell(params: GridRenderEditCellParams<TeamModalRow>) { | |||
const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] | |||
const content = <GridEditDateCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={errorMessage}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
} | |||
}, | |||
{ | |||
field: 'actions', | |||
type: 'actions', | |||
headerName: 'edit', | |||
width: 100, | |||
cellClassName: 'actions', | |||
getActions: ({ id }: { id: number }) => { | |||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
if (isInEditMode) { | |||
return [ | |||
<GridActionsCellItem | |||
icon={<EditIcon />} | |||
label="Edit" | |||
className="textPrimary" | |||
onClick={handleEditClick(id)} | |||
color="inherit" | |||
icon={<SaveIcon />} | |||
label="Save" | |||
key="edit" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
color: 'primary.main' | |||
}} | |||
onClick={handleDeleteClick(id)} color="inherit" key="edit" /> | |||
onClick={handleSave(id)} | |||
/>, | |||
<GridActionsCellItem | |||
icon={<CancelIcon />} | |||
label="Cancel" | |||
key="edit" | |||
onClick={handleCancel(id)} | |||
/> | |||
]; | |||
} | |||
return [ | |||
<GridActionsCellItem | |||
icon={<DeleteIcon />} | |||
label="Delete" | |||
sx={{ | |||
color: 'error.main' | |||
}} | |||
onClick={handleDelete(id)} color="inherit" key="edit" /> | |||
]; | |||
} | |||
), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] | |||
) | |||
let _columns: any[] = [] | |||
if (columns) { | |||
_columns = [...columns, defaultCol] | |||
} | |||
} | |||
], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) | |||
const footer = ( | |||
<Box display="flex" gap={2} alignItems="center"> | |||
<Button | |||
disableRipple | |||
variant="outlined" | |||
startIcon={<Add />} | |||
onClick={addRow} | |||
size="small" | |||
> | |||
{t("Add Record")} | |||
</Button> | |||
</Box> | |||
) | |||
return ( | |||
<Modal open={open} onClose={handleClose}> | |||
<Paper sx={{ ...modalSx }}> | |||
@@ -204,25 +313,48 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
{t('TeamHistoryModal')} | |||
</Typography> | |||
<StyledDataGrid | |||
getRowId={getRowId} | |||
apiRef={apiRef} | |||
rows={_rows} | |||
columns={_columns} | |||
columns={columns} | |||
editMode="row" | |||
autoHeight | |||
sx={{ | |||
"--DataGrid-overlayHeight": "100px", | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
border: "1px solid", | |||
borderColor: "error.main", | |||
}, | |||
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
border: "1px solid", | |||
borderColor: "warning.main", | |||
}, | |||
}} | |||
disableColumnMenu | |||
rowModesModel={rowModesModel} | |||
onRowModesModelChange={setRowModesModel} | |||
onRowEditStop={handleRowEditStop} | |||
processRowUpdate={processRowUpdate} | |||
// slots={{ | |||
// toolbar: EditToolbar | |||
// }} | |||
// slotProps={{ | |||
// toolbar: {count, setCount, setRows, setRowModesModel, _columns} | |||
// }} | |||
onProcessRowUpdateError={onProcessRowUpdateError} | |||
getCellClassName={(params: GridCellParams<TeamModalRow>) => { | |||
let classname = ""; | |||
if (params.row._error) { | |||
classname = "hasError" | |||
} | |||
return classname; | |||
}} | |||
slots={{ | |||
footer: FooterToolbar, | |||
noRowsOverlay: NoRowsOverlay, | |||
}} | |||
slotProps={{ | |||
footer: { child: footer }, | |||
}} | |||
/> | |||
<Box display="flex" justifyContent="flex-end" gap={2}> | |||
<Button variant="text" onClick={handleClose}> | |||
{t('Cancel')} | |||
<Button variant="text" onClick={onCancel}> | |||
{t('Close')} | |||
</Button> | |||
<Button variant="contained" onClick={bigTesting}> | |||
<Button variant="contained" onClick={doSave}> | |||
{t("Save")} | |||
</Button> | |||
</Box> | |||
@@ -231,4 +363,20 @@ const TeamHistoryModal: React.FC<Props> = async ({ open, onClose, columns }) => | |||
</Modal> | |||
) | |||
} | |||
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
}; | |||
const NoRowsOverlay: React.FC = () => { | |||
const { t } = useTranslation("home"); | |||
return ( | |||
<Box | |||
display="flex" | |||
justifyContent="center" | |||
alignItems="center" | |||
height="100%" | |||
> | |||
<Typography variant="caption">{t("Add some entries!")}</Typography> | |||
</Box> | |||
); | |||
}; | |||
export default TeamHistoryModal |
@@ -0,0 +1,61 @@ | |||
import { gradeHistory, positionHistory, salaryEffectiveInfo, teamHistory } from "@/app/api/staff/actions"; | |||
import { GridRowId, GridRowModel } from "@mui/x-data-grid"; | |||
import dayjs, { Dayjs } from "dayjs"; | |||
import { TeamModalRow } from "./TeamHistoryModal"; | |||
import { GradeModalRow } from "./GradeHistoryModal"; | |||
import { PositionModalRow } from "./PositionHistoryModal"; | |||
import { SeModalRow } from "./SalaryEffectiveModel"; | |||
export type ValidateError = { | |||
from: number[] | |||
to: number[] | |||
} | |||
type RowModel = Partial<TeamModalRow & GradeModalRow & PositionModalRow & SeModalRow> | |||
type AllFields = Partial<teamHistory & gradeHistory & positionHistory & salaryEffectiveInfo> | |||
export type StaffEntryError = { | |||
[field in keyof AllFields]?: string; | |||
}; | |||
export const validateRowAndRowBefore = ( | |||
prevRow: RowModel | null, | |||
currRow: RowModel | |||
): StaffEntryError | undefined => { | |||
const error: StaffEntryError = {} | |||
if (prevRow) { | |||
if ('from' in currRow && currRow.from !== undefined) { | |||
if (dayjs(prevRow.from).diff(dayjs(currRow.from)) == 0) { | |||
error.from = "The date should not be the same as last entry" | |||
} | |||
} else if ('date' in currRow && currRow.date !== undefined) { | |||
if (dayjs(prevRow.date).diff(dayjs(currRow.date)) == 0) { | |||
error.date = "The date should not be the same as last entry" | |||
} | |||
} | |||
} | |||
console.log(currRow) | |||
if ('from' in currRow && !currRow.from) { | |||
error.from = "The date cannot be empty" | |||
} | |||
if ('date' in currRow && !currRow.date) { | |||
error.date = "The date cannot be empty" | |||
} | |||
// Check specific fields based on row type | |||
if ('grade' in currRow && !currRow.grade) { | |||
error.grade = "Grade cannot be empty" | |||
} | |||
if ('position' in currRow && !currRow.position) { | |||
error.position = "Position cannot be empty" | |||
} | |||
if ('team' in currRow && !currRow.team) { | |||
error.team = "Team cannot be empty" | |||
} | |||
console.log("error") | |||
console.log(error) | |||
console.log(currRow.from) | |||
return Object.keys(error).length > 0 ? error : undefined; | |||
} |
@@ -18,6 +18,7 @@ import { | |||
Divider, | |||
Grid, | |||
Stack, | |||
TextField, | |||
Typography, | |||
} from "@mui/material"; | |||
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; | |||
@@ -27,12 +28,18 @@ import { uniq } from "lodash"; | |||
import CreateExpenseModal from "./CreateExpenseModal"; | |||
import { ProjectResult } from "@/app/api/projects"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { GridCellParams, GridColDef, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridCellParams, GridColDef, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
import { GridEventListener } from "@mui/x-data-grid"; | |||
import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||
import { deleteProjectExpense, updateProjectExpense } from "@/app/api/projectExpenses/actions"; | |||
import dayjs from "dayjs"; | |||
import React from "react"; | |||
import { NumberFormatValues, NumericFormat } from "react-number-format"; | |||
interface CustomMoneyComponentProps { | |||
params: GridRenderEditCellParams; | |||
} | |||
interface Props { | |||
expenses: ProjectExpensesResultFormatted[] | |||
@@ -86,6 +93,8 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
[] | |||
); | |||
const columns = useMemo<Column<ProjectExpensesResultFormatted>[]>( | |||
() => [ | |||
{ | |||
@@ -215,6 +224,47 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
[validateRow], | |||
); | |||
// Money format : 000,000,000.00 | |||
const CustomMoneyFormat = useCallback((value: number) => { | |||
if (value) { | |||
return moneyFormatter.format(value); | |||
} else { | |||
return "" | |||
} | |||
}, []) | |||
const CustomMoneyComponent: React.FC<CustomMoneyComponentProps> = ({ params }) => { | |||
const { id, value, field } = params; | |||
const ref = React.useRef(); | |||
const handleValueChange = (newValue: NumberFormatValues) => { | |||
apiRef.current.setEditCellValue({ id, field, value: newValue.value }); | |||
}; | |||
return <NumericFormat | |||
fullWidth | |||
prefix="HK$" | |||
onValueChange={(values) => { | |||
console.log(values) | |||
handleValueChange(values) | |||
}} | |||
customInput={TextField} | |||
thousandSeparator | |||
valueIsNumericString | |||
decimalScale={2} | |||
fixedDecimalScale | |||
value={value} | |||
inputRef={ref} | |||
InputProps={{ | |||
sx: { | |||
'& .MuiInputBase-input': { | |||
textAlign: 'right', | |||
mb: 2 | |||
} | |||
} | |||
}} | |||
/>; | |||
} | |||
const editColumn = useMemo<GridColDef[]>( | |||
() => [ | |||
{ | |||
@@ -234,7 +284,13 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
headerName: t("Amount (HKD)"), | |||
editable: true, | |||
flex: 0.5, | |||
type: 'number' | |||
type: 'number', | |||
renderEditCell: (params: GridRenderEditCellParams) => { | |||
return <CustomMoneyComponent params={params} /> | |||
}, | |||
valueFormatter: (params: GridValueFormatterParams) => { | |||
return CustomMoneyFormat(params.value as number) | |||
} | |||
}, | |||
{ field: "issuedDate", | |||
headerName: t("Issue Date"), | |||
@@ -13,7 +13,7 @@ import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoic | |||
import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; | |||
import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; | |||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; | |||
import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; | |||
import { GridCellParams, GridColDef, GridEventListener, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
@@ -22,8 +22,11 @@ import CreateInvoiceModal from "./CreateInvoiceModal"; | |||
import { ProjectResult } from "@/app/api/projects"; | |||
import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; | |||
import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||
import { NumberFormatValues, NumericFormat } from "react-number-format"; | |||
interface CustomMoneyComponentProps { | |||
params: GridRenderEditCellParams; | |||
} | |||
interface Props { | |||
invoices: invoiceList[]; | |||
@@ -287,6 +290,48 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab | |||
// setSelectedRow([]); | |||
}; | |||
// Money format : 000,000,000.00 | |||
const CustomMoneyFormat = useCallback((value: number) => { | |||
if (value) { | |||
return moneyFormatter.format(value); | |||
} else { | |||
return "" | |||
} | |||
}, []) | |||
const CustomMoneyComponent: React.FC<CustomMoneyComponentProps> = ({ params }) => { | |||
const { id, value, field } = params; | |||
const ref = React.useRef(); | |||
const handleValueChange = (newValue: NumberFormatValues) => { | |||
apiRef.current.setEditCellValue({ id, field, value: newValue.value }); | |||
}; | |||
return <NumericFormat | |||
fullWidth | |||
prefix="HK$" | |||
onValueChange={(values) => { | |||
console.log(values) | |||
handleValueChange(values) | |||
}} | |||
customInput={TextField} | |||
thousandSeparator | |||
valueIsNumericString | |||
decimalScale={2} | |||
fixedDecimalScale | |||
value={value} | |||
inputRef={ref} | |||
InputProps={{ | |||
sx: { | |||
'& .MuiInputBase-input': { | |||
textAlign: 'right', | |||
mb: 2 | |||
} | |||
} | |||
}} | |||
/>; | |||
} | |||
const combinedColumns = useMemo<Column<invoiceList>[]>( | |||
() => [ | |||
{ | |||
@@ -329,7 +374,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab | |||
headerName: t("Amount (HKD)"), | |||
editable: true, | |||
flex: 0.5, | |||
type: 'number' | |||
type: 'number', | |||
renderEditCell: (params: GridRenderEditCellParams) => { | |||
return <CustomMoneyComponent params={params} /> | |||
}, | |||
valueFormatter: (params: GridValueFormatterParams) => { | |||
return CustomMoneyFormat(params.value as number) | |||
} | |||
}, | |||
{ | |||
field: "receiptDate", | |||
@@ -351,7 +402,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab | |||
headerName: t("Actual Received Amount (HKD)"), | |||
editable: true, | |||
flex: 0.5, | |||
type: 'number' | |||
type: 'number', | |||
renderEditCell: (params: GridRenderEditCellParams) => { | |||
return <CustomMoneyComponent params={params} /> | |||
}, | |||
valueFormatter: (params: GridValueFormatterParams) => { | |||
return CustomMoneyFormat(params.value as number) | |||
} | |||
}, | |||
], | |||
[t] | |||
@@ -36,6 +36,7 @@ export interface Props { | |||
timesheetRecords: RecordTimesheetInput; | |||
isFullTime: boolean; | |||
joinDate: Dayjs; | |||
isSaturdayWorker: boolean | |||
} | |||
interface EventClickArg { | |||
@@ -57,6 +58,7 @@ const LeaveCalendar: React.FC<Props> = ({ | |||
leaveRecords, | |||
isFullTime, | |||
joinDate, | |||
isSaturdayWorker | |||
}) => { | |||
const { | |||
t, | |||
@@ -190,7 +192,8 @@ const LeaveCalendar: React.FC<Props> = ({ | |||
({ event }: EventClickArg) => { | |||
const dayJsObj = dayjs(event.startStr); | |||
const holiday = getHolidayForDate(event.startStr, companyHolidays); | |||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; | |||
const isHoliday = holiday || isWeekend; | |||
if ( | |||
event.extendedProps.calendar === "leaveEntry" && | |||
@@ -210,7 +213,8 @@ const LeaveCalendar: React.FC<Props> = ({ | |||
(e: { dateStr: string; dayEl: HTMLElement }) => { | |||
const dayJsObj = dayjs(e.dateStr); | |||
const holiday = getHolidayForDate(e.dateStr, companyHolidays); | |||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; | |||
const isHoliday = holiday || isWeekend; | |||
openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); | |||
}, | |||
@@ -224,7 +228,8 @@ const LeaveCalendar: React.FC<Props> = ({ | |||
} | |||
const dayJsObj = dayjs(date); | |||
const holiday = getHolidayForDate(date, companyHolidays); | |||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; | |||
const isHoliday = holiday || isWeekend; | |||
const leaves = localLeaveRecords[date] || []; | |||
const timesheets = timesheetRecords[date] || []; | |||
@@ -25,6 +25,7 @@ const modalSx: SxProps = { | |||
interface Props extends LeaveCalendarProps { | |||
open: boolean; | |||
onClose: () => void; | |||
isSaturdayWorker: boolean | |||
} | |||
const LeaveModal: React.FC<Props> = ({ | |||
@@ -37,6 +38,7 @@ const LeaveModal: React.FC<Props> = ({ | |||
timesheetRecords, | |||
isFullTime, | |||
joinDate, | |||
isSaturdayWorker | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
const isMobile = useIsMobile(); | |||
@@ -51,6 +53,7 @@ const LeaveModal: React.FC<Props> = ({ | |||
allProjects={allProjects} | |||
leaveRecords={leaveRecords} | |||
timesheetRecords={timesheetRecords} | |||
isSaturdayWorker={isSaturdayWorker} | |||
/> | |||
); | |||
@@ -7,13 +7,14 @@ import { useTranslation } from "react-i18next"; | |||
import SearchResults, { Column } from "../SearchResults"; | |||
import EditNote from "@mui/icons-material/EditNote"; | |||
import uniq from "lodash/uniq"; | |||
import { useRouter } from "next/navigation"; | |||
import { useRouter, useSearchParams } from "next/navigation"; | |||
import { MAINTAIN_PROJECT } from "@/middleware"; | |||
import { reverse, uniqBy } from "lodash"; | |||
import { loadDrafts } from "@/app/utils/draftUtils"; | |||
import { TeamResult } from "@/app/api/team"; | |||
import { Customer } from "@/app/api/customer"; | |||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | |||
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'; | |||
type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; | |||
@@ -141,6 +142,32 @@ const ProjectSearch: React.FC<Props> = ({ | |||
[router], | |||
); | |||
const onProjectStatusClick = useCallback( | |||
(project: ProjectResultOrDraft) => { | |||
const status = project.status.toLocaleLowerCase() | |||
console.log(status) | |||
if (status && statusList.includes(status)) { | |||
/* switch (status) { | |||
case "pending to start": | |||
router.push(`/projects/edit?id=${project.id}&autoClick=start`); | |||
break; | |||
case "on-going": | |||
router.push(`/projects/edit?id=${project.id}&autoClick=complete`); | |||
break; | |||
case "completed": | |||
router.push(`/projects/edit?id=${project.id}&autoClick=reopen`); | |||
break; | |||
} */ | |||
router.push(`/projects/edit?id=${project.id}&autoClick=true`); | |||
} | |||
}, | |||
[router], | |||
); | |||
const statusList = ["pending to start", "on-going","completed"] | |||
const ignoreStatusList = ["draft", "deleted"] | |||
const columns = useMemo<Column<ProjectResult>[]>( | |||
() => [ | |||
{ | |||
@@ -165,7 +192,32 @@ const ProjectSearch: React.FC<Props> = ({ | |||
{ name: "category", label: t("Project Category") }, | |||
{ name: "team", label: t("Team") }, | |||
{ name: "client", label: t("Client") }, | |||
{ name: "status", label: t("Status") }, | |||
{ | |||
name: "status", | |||
label: t("Status"), | |||
// type: "link", | |||
// onClick: onProjectStatusClick, | |||
// underlines: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "none"}), {}), | |||
// colors: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "inherit"}), {}), | |||
}, | |||
// { | |||
// name: "status", | |||
// label: t("Status"), | |||
// type: "button", | |||
// onClick: onProjectStatusClick, | |||
// variants: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "text"}), {}), | |||
// colors: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "inherit"}), {}), | |||
// } | |||
{ | |||
name: "status", | |||
label: t(""), | |||
onClick: onProjectStatusClick, | |||
buttonIcon: <PlayCircleOutlineIcon />, | |||
disabled: !abilities.includes(MAINTAIN_PROJECT), | |||
disabledRows: { | |||
status: ignoreStatusList | |||
} | |||
}, | |||
], | |||
[t, onProjectClick], | |||
); | |||
@@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next"; | |||
import { convertDateArrayToString, moneyFormatter } from "@/app/utils/formatUtil"; | |||
import DoneIcon from '@mui/icons-material/Done'; | |||
import CloseIcon from '@mui/icons-material/Close'; | |||
import { Button, Link, LinkOwnProps } from "@mui/material"; | |||
export interface ResultWithId { | |||
id: string | number; | |||
@@ -25,7 +26,6 @@ export interface ResultWithId { | |||
interface BaseColumn<T extends ResultWithId> { | |||
name: keyof T; | |||
label: string; | |||
color?: IconButtonOwnProps["color"]; | |||
needTranslation?: boolean; | |||
type?: string; | |||
isHidden?: boolean; | |||
@@ -34,13 +34,25 @@ interface BaseColumn<T extends ResultWithId> { | |||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | |||
onClick: (item: T) => void; | |||
buttonIcon: React.ReactNode; | |||
color?: IconButtonOwnProps["color"]; | |||
disabled?: boolean; | |||
disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled | |||
disabledRows?: { [columnValue in keyof T]: string[] }; // Filter the row which is going to be disabled | |||
} | |||
interface LinkColumn<T extends ResultWithId> extends BaseColumn<T> { | |||
// href: string; | |||
onClick: (item: T) => void; | |||
underline: LinkOwnProps["underline"]; | |||
underlines: { [columnValue in keyof T]: LinkOwnProps["underline"] }; | |||
color: LinkOwnProps["color"]; | |||
colors: { [columnValue in keyof T]: LinkOwnProps["color"] }; | |||
} | |||
export type Column<T extends ResultWithId> = | |||
| BaseColumn<T> | |||
| ColumnWithAction<T>; | |||
| ColumnWithAction<T> | |||
| LinkColumn<T> | |||
; | |||
interface Props<T extends ResultWithId> { | |||
items: T[]; | |||
@@ -55,6 +67,12 @@ function isActionColumn<T extends ResultWithId>( | |||
return Boolean((column as ColumnWithAction<T>).onClick); | |||
} | |||
function isLinkColumn<T extends ResultWithId>( | |||
column: Column<T>, | |||
): column is LinkColumn<T> { | |||
return column.type === "link"; | |||
} | |||
function SearchResults<T extends ResultWithId>({ | |||
items, | |||
columns, | |||
@@ -101,6 +119,43 @@ function SearchResults<T extends ResultWithId>({ | |||
return false; | |||
}; | |||
function convertObjectKeysToLowercase<T extends object>(obj: T): object | undefined { | |||
return obj ? Object.fromEntries( | |||
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]) | |||
) : undefined; | |||
} | |||
// Link Component Functions | |||
function handleLinkColors<T extends ResultWithId>( | |||
column: LinkColumn<T>, | |||
value: T[keyof T], | |||
): LinkOwnProps["color"] { | |||
const colors = convertObjectKeysToLowercase(column.colors); | |||
console.log(colors) | |||
const valueKey = String(value).toLowerCase() as keyof typeof colors; | |||
if (colors && valueKey in colors) { | |||
return colors[valueKey]; | |||
} | |||
return column.color ?? "primary"; | |||
}; | |||
function handleLinkUnderlines<T extends ResultWithId>( | |||
column: LinkColumn<T>, | |||
value: T[keyof T], | |||
): LinkOwnProps["underline"] { | |||
const underlines = convertObjectKeysToLowercase(column.underlines); | |||
console.log(underlines) | |||
const valueKey = String(value).toLowerCase() as keyof typeof underlines; | |||
if (underlines && valueKey in underlines) { | |||
return underlines[valueKey]; | |||
} | |||
return column.underline ?? "always"; | |||
}; | |||
const table = ( | |||
<> | |||
<TableContainer sx={{ maxHeight: 440 }}> | |||
@@ -125,15 +180,15 @@ function SearchResults<T extends ResultWithId>({ | |||
return ( | |||
<TableCell key={`${columnName.toString()}-${idx}`}> | |||
{isActionColumn(column) ? ( | |||
<IconButton | |||
color={column.color ?? "primary"} | |||
onClick={() => column.onClick(item)} | |||
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} | |||
> | |||
{column.buttonIcon} | |||
</IconButton> | |||
) : | |||
{isActionColumn(column) && !column?.type ? ( | |||
<IconButton | |||
color={column.color ?? "primary"} | |||
onClick={() => column.onClick(item)} | |||
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))} | |||
> | |||
{column.buttonIcon} | |||
</IconButton> | |||
) : | |||
column?.type === "date" ? ( | |||
<>{convertDateArrayToString(item[columnName] as number[])}</> | |||
) : | |||
@@ -143,9 +198,18 @@ function SearchResults<T extends ResultWithId>({ | |||
column?.type === "checkbox" ? ( | |||
Boolean(item[columnName]) ? | |||
<DoneIcon color="primary" /> : <CloseIcon color="error"/> | |||
) : | |||
isLinkColumn(column) ? ( | |||
<Link | |||
onClick={() => column.onClick(item)} | |||
underline={handleLinkUnderlines(column, item[columnName])} | |||
color={handleLinkColors(column, item[columnName])} | |||
> | |||
{item[columnName] as string} | |||
</Link> | |||
) : | |||
( | |||
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</> | |||
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName] as string}</> | |||
)} | |||
</TableCell> | |||
); | |||
@@ -54,6 +54,7 @@ interface Props { | |||
isFullTime: boolean; | |||
joinDate: Dayjs; | |||
miscTasks: Task[]; | |||
isSaturdayWorker: boolean | |||
} | |||
const modalSx: SxProps = { | |||
@@ -81,6 +82,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||
isFullTime, | |||
joinDate, | |||
miscTasks, | |||
isSaturdayWorker, | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
@@ -227,6 +229,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||
leaveTypes, | |||
miscTasks, | |||
}} | |||
isSaturdayWorker={isSaturdayWorker} | |||
/> | |||
</Box> | |||
{errorComponent} | |||
@@ -512,4 +512,4 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||
); | |||
}; | |||
export default TimesheetAmendment; | |||
export default TimesheetAmendment; |
@@ -82,4 +82,4 @@ export const TimesheetAmendmentModal: React.FC<Props> = ({ | |||
</Card> | |||
</Modal> | |||
); | |||
}; | |||
}; |
@@ -248,4 +248,4 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
); | |||
}; | |||
export default UserWorkspacePage; | |||
export default UserWorkspacePage; |
@@ -89,4 +89,4 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||
); | |||
}; | |||
export default UserWorkspaceWrapper; | |||
export default UserWorkspaceWrapper; |
@@ -58,14 +58,6 @@ export const authOptions: AuthOptions = { | |||
jwt(params) { | |||
// Add the data from user to the token | |||
const { token, user, account, trigger, session } = params; | |||
// console.log("--------------------------") | |||
// console.log("%c [ token ]:", 'font-size:13px; background:#A888B5; color:#bf2c9f;', token) | |||
// console.log("%c [ user ]:", 'font-size:13px; background:pink; color:#bf2c9f;', user) | |||
// console.log("%c [ account ]:", 'font-size:13px; background:pink; color:#bf2c9f;', account) | |||
// console.log("%c [ session ]:", 'font-size:13px; background:#FFD2A0; color:#bf2c9f;', session) | |||
// console.log("%c [ trigger ]:", 'font-size:13px; background:#EFB6C8; color:#bf2c9f;', trigger) | |||
// console.log(params) | |||
// console.log("--------------------------") | |||
if (trigger === "update" && session?.accessToken && session?.refreshToken) { | |||
token.accessToken = session.accessToken | |||
@@ -21,6 +21,7 @@ | |||
"Submit Fail": "Submit Fail", | |||
"Do you want to delete?": "Do you want to delete", | |||
"Delete Success": "Delete Success", | |||
"Save Success": "Save Success", | |||
"Details": "Details", | |||
"Delete": "Delete", | |||
@@ -16,7 +16,8 @@ | |||
"Submit Fail": "提交失敗", | |||
"Do you want to delete?": "你是否確認要刪除?", | |||
"Delete Success": "刪除成功", | |||
"Save Success": "儲存成功", | |||
"Date": "日期", | |||
"Month": "月份", | |||
@@ -71,7 +71,8 @@ export const [ | |||
GENERATE_PROJECT_CASH_FLOW_REPORT, | |||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||
VIEW_ALL_PROJECTS | |||
VIEW_ALL_PROJECTS, | |||
SATURDAY_WORKERS | |||
] = [ | |||
'MAINTAIN_USER', | |||
'MAINTAIN_TIMESHEET', | |||
@@ -124,7 +125,8 @@ export const [ | |||
'G_PROJECT_CASH_FLOW_REPORT', | |||
'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||
'G_CROSS_TEAM_CHARGE_REPORT', | |||
'VIEW_ALL_PROJECTS' | |||
'VIEW_ALL_PROJECTS', | |||
'SATURDAY_WORKERS' | |||
] | |||
const PRIVATE_ROUTES = [ | |||