| @@ -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 = [ | ||||