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