From 9ff4921b6f6758df96fd77de8895915fcebd0893 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 13 Feb 2025 16:42:38 +0800 Subject: [PATCH 01/16] update invoice search money format --- .../InvoiceSearch/InvoiceSearch.tsx | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index f7c3abe..1e2a517 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -13,7 +13,7 @@ import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoic import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { GridCellParams, GridColDef, GridEventListener, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid"; import { useGridApiRef } from "@mui/x-data-grid"; import StyledDataGrid from "../StyledDataGrid"; @@ -22,8 +22,11 @@ import CreateInvoiceModal from "./CreateInvoiceModal"; import { ProjectResult } from "@/app/api/projects"; import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; import InvoiceSearchLoading from "./InvoiceSearchLoading"; +import { NumberFormatValues, NumericFormat } from "react-number-format"; - +interface CustomMoneyComponentProps { + params: GridRenderEditCellParams; +} interface Props { invoices: invoiceList[]; @@ -287,6 +290,48 @@ const InvoiceSearch: React.FC & SubComponents = ({ invoices, projects, ab // setSelectedRow([]); }; + // Money format : 000,000,000.00 + const CustomMoneyFormat = useCallback((value: number) => { + if (value) { + return moneyFormatter.format(value); + } else { + return "" + } + }, []) + + const CustomMoneyComponent: React.FC = ({ params }) => { + const { id, value, field } = params; + const ref = React.useRef(); + + const handleValueChange = (newValue: NumberFormatValues) => { + apiRef.current.setEditCellValue({ id, field, value: newValue.value }); + }; + + return { + 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[]>( () => [ { @@ -329,7 +374,13 @@ const InvoiceSearch: React.FC & SubComponents = ({ invoices, projects, ab headerName: t("Amount (HKD)"), editable: true, flex: 0.5, - type: 'number' + type: 'number', + renderEditCell: (params: GridRenderEditCellParams) => { + return + }, + valueFormatter: (params: GridValueFormatterParams) => { + return CustomMoneyFormat(params.value as number) + } }, { field: "receiptDate", @@ -351,7 +402,13 @@ const InvoiceSearch: React.FC & SubComponents = ({ invoices, projects, ab headerName: t("Actual Received Amount (HKD)"), editable: true, flex: 0.5, - type: 'number' + type: 'number', + renderEditCell: (params: GridRenderEditCellParams) => { + return + }, + valueFormatter: (params: GridValueFormatterParams) => { + return CustomMoneyFormat(params.value as number) + } }, ], [t] From 3311747d4aef4ecc2c055199aefb7468d4f1892d Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Thu, 13 Feb 2025 16:50:20 +0800 Subject: [PATCH 02/16] update money format (expense & project) --- .../CreateProject/ProjectClientDetails.tsx | 4 +- .../ExpenseSearch/ExpenseSearch.tsx | 60 ++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 90f4472..b206e34 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -477,7 +477,7 @@ const ProjectClientDetails: React.FC = ({ { // console.log(values) onChange(values.floatValue) @@ -513,7 +513,7 @@ const ProjectClientDetails: React.FC = ({ { // console.log(values) onChange(values.floatValue) diff --git a/src/components/ExpenseSearch/ExpenseSearch.tsx b/src/components/ExpenseSearch/ExpenseSearch.tsx index c7807a3..c587ade 100644 --- a/src/components/ExpenseSearch/ExpenseSearch.tsx +++ b/src/components/ExpenseSearch/ExpenseSearch.tsx @@ -18,6 +18,7 @@ import { Divider, Grid, Stack, + TextField, Typography, } from "@mui/material"; import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; @@ -27,12 +28,18 @@ import { uniq } from "lodash"; import CreateExpenseModal from "./CreateExpenseModal"; import { ProjectResult } from "@/app/api/projects"; import StyledDataGrid from "../StyledDataGrid"; -import { GridCellParams, GridColDef, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { GridCellParams, GridColDef, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid"; import { useGridApiRef } from "@mui/x-data-grid"; import { GridEventListener } from "@mui/x-data-grid"; import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; import { deleteProjectExpense, updateProjectExpense } from "@/app/api/projectExpenses/actions"; import dayjs from "dayjs"; +import React from "react"; +import { NumberFormatValues, NumericFormat } from "react-number-format"; + +interface CustomMoneyComponentProps { + params: GridRenderEditCellParams; +} interface Props { expenses: ProjectExpensesResultFormatted[] @@ -86,6 +93,8 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { [] ); + + const columns = useMemo[]>( () => [ { @@ -215,6 +224,47 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { [validateRow], ); + // Money format : 000,000,000.00 + const CustomMoneyFormat = useCallback((value: number) => { + if (value) { + return moneyFormatter.format(value); + } else { + return "" + } +}, []) + +const CustomMoneyComponent: React.FC = ({ params }) => { + const { id, value, field } = params; + const ref = React.useRef(); + + const handleValueChange = (newValue: NumberFormatValues) => { + apiRef.current.setEditCellValue({ id, field, value: newValue.value }); + }; + + return { + 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( () => [ { @@ -234,7 +284,13 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { headerName: t("Amount (HKD)"), editable: true, flex: 0.5, - type: 'number' + type: 'number', + renderEditCell: (params: GridRenderEditCellParams) => { + return + }, + valueFormatter: (params: GridValueFormatterParams) => { + return CustomMoneyFormat(params.value as number) + } }, { field: "issuedDate", headerName: t("Issue Date"), From 534c906782eef7e0d0d38138fe33a6407b3a0ded Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 17 Feb 2025 18:46:33 +0800 Subject: [PATCH 03/16] 1. timesheet amendment - staff selection - sort by staffId --- src/components/TimesheetAmendment/TimesheetAmendment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index 9d1f5c2..e0bb4ab 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -117,7 +117,7 @@ const TimesheetAmendment: React.FC = ({ }); }, [], - ); + ).sort((a, b) => a.staffId.localeCompare(b.staffId)); }, [localTeamLeaves, localTeamTimesheets]); const [selectedStaff, setSelectedStaff] = useState( From 214247f126e933d7321817d7564bb1271cd54305 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 18 Feb 2025 14:43:41 +0800 Subject: [PATCH 04/16] 1. remove client & subsidiary code --- src/components/CreateProject/ProjectClientDetails.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index b206e34..4414949 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -575,7 +575,8 @@ const ProjectClientDetails: React.FC = ({ control={control} options={allCustomers.map((customer) => ({ ...customer, - label: `${customer.code} - ${customer.name}`, + label: `${customer.name}`, + // label: `${customer.code} - ${customer.name}`, }))} name="clientId" label={t("Client")} @@ -624,7 +625,8 @@ const ProjectClientDetails: React.FC = ({ const subsidiary = subsidiaryMap[subsidiaryId]; return { id: subsidiary.id, - label: `${subsidiary.code} - ${subsidiary.name}`, + label: `${subsidiary.name}`, + // label: `${subsidiary.code} - ${subsidiary.name}`, }; }), ]} From fd69b6fb2b3c4efa510c7edad7643e9a699810af Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 19 Feb 2025 14:54:36 +0800 Subject: [PATCH 05/16] save draft without routing to project search page --- .../CreateProject/CreateProject.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index f4452a9..10eca78 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -118,7 +118,7 @@ const hasErrorsInTab = ( const CreateProject: React.FC = ({ isEditMode, isCopyMode, - draftId, + draftId: initDraftId, isSubProject, mainProjects, defaultInputs, @@ -139,6 +139,7 @@ const CreateProject: React.FC = ({ customerTypes, abilities, }) => { + const [draftId, setDraftId] = useState(initDraftId) const [serverError, setServerError] = useState(""); const [tabIndex, setTabIndex] = useState(0); const { t } = useTranslation(); @@ -562,9 +563,17 @@ const CreateProject: React.FC = ({ formProps.reset(draftInputs); }, [draftId, formProps]); - const saveDraft = useCallback(() => { - saveToLocalStorage(draftId || Date.now(), formProps.getValues()); - router.replace("/projects"); + const saveDraft = useCallback(async () => { + const currentTimestamp = Date.now() + + saveToLocalStorage(draftId || currentTimestamp, formProps.getValues()); + + const success = await successDialog("Save Success", t) + + if (success && !draftId) { + setDraftId(currentTimestamp) + } + // router.replace("/projects"); }, [draftId, formProps, router]); const handleDeleteDraft = useCallback(() => { @@ -757,7 +766,7 @@ const CreateProject: React.FC = ({ > {t("Save Draft")} - {draftId && + {draftId && + ); From aa19b8599b8933225cc5722508b93a76b76ab13e Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 27 Feb 2025 16:56:37 +0800 Subject: [PATCH 10/16] update item 147 --- src/app/api/staff/actions.ts | 37 +- src/components/CreateStaff/CreateStaff.tsx | 101 +++- src/components/CreateStaff/StaffInfo.tsx | 187 +++++++- src/components/EditStaff/EditStaff.tsx | 19 +- .../EditStaff/GradeHistoryModal.tsx | 354 +++++++++----- .../EditStaff/PositionHistoryModal.tsx | 421 +++++++++++------ .../EditStaff/SalaryEffectiveModel.tsx | 385 +++++++++------- src/components/EditStaff/StaffInfo.tsx | 134 +----- src/components/EditStaff/TeamHistoryModal.tsx | 434 ++++++++++++------ src/components/EditStaff/validateDates.ts | 61 +++ 10 files changed, 1427 insertions(+), 706 deletions(-) create mode 100644 src/components/EditStaff/validateDates.ts diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 9f22094..d105d68 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -17,21 +17,18 @@ export type teamHistory = { id: number, team: string | number, from: Date | string, - to?: Date | string } export type gradeHistory = { id: number, grade: string | number, from: Date | string, - to?: Date | string } export type positionHistory = { id: number, position: string | number, from: Date | string, - to?: Date | string } export interface CreateStaffInputs { id?: number @@ -62,6 +59,11 @@ export interface CreateStaffInputs { delGradeHistory: number[]; positionHistory: positionHistory[]; delPositionHistory: number[]; + // new modal + salary: salary[]; + team: team[]; + grade: grade[]; + position: position[]; } export interface records { @@ -69,6 +71,35 @@ export interface CreateStaffInputs { name: string; // team: Team[]; } + export type DataLog = + | (salary & {id: number; type: "salary"}) + | (team & {id: number; type: "team"}) + | (grade & {id: number; type: "grade"}) + | (position & {id: number; type: "position"}) + + export type salary = { + from: string; + to: string; + salaryPoint: number; + } + + export type team = { + from: string; + to: string; + teamId: number; + } + + export type grade = { + from: string; + to: string; + gradeId: number; + } + + export type position = { + from: string; + to: string; + positionId: number; + } export interface salaryEffectiveInfo { id: number; diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 5343a71..d960cc0 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -10,9 +10,11 @@ import { } from "react-hook-form"; import { CreateStaffInputs, saveStaff, testing } from "@/app/api/staff/actions"; import { Button, Stack, Typography } from "@mui/material"; -import { comboProp} from "@/app/api/companys/actions"; +import { comboProp } from "@/app/api/companys/actions"; import StaffInfo from "./StaffInfo"; import { Check, Close } from "@mui/icons-material"; +import dayjs from "dayjs"; +import { SalaryEffectiveInfo } from "@/app/api/staff"; interface Field { id: string; @@ -45,15 +47,29 @@ const CreateStaff: React.FC = ({ combos }) => { const { t } = useTranslation(); const formProps = useForm(); const [serverError, setServerError] = useState(""); + const [errorMsg, setErrorMsg] = useState("An error has occurred. Please try again later.") const router = useRouter(); - const [tabIndex, setTabIndex] = useState(0); + // const [tabIndex, setTabIndex] = useState(0); const errors = formProps.formState.errors; + function chopSalaryPoints(input: string | number): number | null { + if (typeof input === 'string') { + const match = input.match(/(\d+) \((\d+) - (\d+)\)/); + if (match) { + return parseInt(match[1], 10); + } + } else if (typeof input === 'number') { + return input; + } + return null; + } + const onSubmit = useCallback>( async (data) => { try { console.log(data); + formProps.clearErrors() let haveError = false; const regex_email = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/ const regex_phone = /^\d{8}$/ @@ -96,20 +112,71 @@ const CreateStaff: React.FC = ({ combos }) => { haveError = true formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) } - if (!data.salaryId) { + if (data.joinDate && data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { haveError = true - formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) + formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) } - if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { + if (!data.salaryEffectiveInfo || data.salaryEffectiveInfo.length < 1) { haveError = true - formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) + formProps.setError("salaryId", { message: t("Please Enter Salary"), type: "required" }) + } + if (!data.gradeHistory || data.gradeHistory.length < 1) { + haveError = true + formProps.setError("gradeId", { message: t("Please Enter Grade"), type: "required" }) + } + if (!data.positionHistory || data.positionHistory.length < 1) { + console.log("asdadsasd") + haveError = true + formProps.setError("currentPositionId", { message: t("Please Enter Current Position"), type: "required" }) } if (haveError) { return } + const teamHistory = data.teamHistory + .map((item) => ({ + id: item.id, + team: combos.team.filter(team => team.label === item.team)[0].id, + from: dayjs(item.from).format('YYYY-MM-DD'), + })) + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + + const gradeHistory = data.gradeHistory + .map((item) => ({ + id: item.id, + grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, + from: dayjs(item.from).format('YYYY-MM-DD'), + })) + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + + console.log(data.positionHistory) + const positionHistory = data.positionHistory + .map((item) => ({ + id: item.id, + position: combos.position.filter(position => position.label === item.position)[0].id, + from: dayjs(item.from).format('YYYY-MM-DD'), + })) + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + + const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ + id: item.id, + salaryPoint: chopSalaryPoints(item.salaryPoint), + date: dayjs(item.date).format('YYYY-MM-DD').toString() + })) // backend sort + const postData: CreateStaffInputs = { + // id: id, + ...data, + salaryEffectiveInfo: salaryEffectiveInfo, + teamHistory: teamHistory ?? [], + gradeHistory: gradeHistory ?? [], + positionHistory: positionHistory ?? [], + delTeamHistory: data.delTeamHistory ? data.delTeamHistory : [], + delGradeHistory: data.delGradeHistory ? data.delGradeHistory : [], + delPositionHistory: data.delPositionHistory ? data.delPositionHistory : [], + } console.log("passed") - console.log(data) - await saveStaff(data) + console.log(postData) + // return + await saveStaff(postData) router.replace("/settings/staff") } catch (e: any) { console.log(e); @@ -118,15 +185,19 @@ const CreateStaff: React.FC = ({ combos }) => { if (e.message === "Duplicated StaffId Found") { msg = t("Duplicated StaffId Found") } - setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `); + setServerError(`${t(errorMsg)} ${msg} `); } }, - [router] + [errorMsg, router] ); - const handleCancel = () => { + + const errorKey = Object.keys(formProps.formState.errors)[0] + const err = errors[errorKey as keyof CreateStaffInputs] + + const handleCancel = useCallback(() => { router.back(); - }; + }, [router]); return ( <> @@ -141,6 +212,12 @@ const CreateStaff: React.FC = ({ combos }) => { {serverError} )} + {err && ( + + {err.message?.toString()} + + ) + } + )} /> @@ -191,7 +257,7 @@ const StaffInfo: React.FC = ({ combos }) => { {t("Grade")} - ( @@ -207,6 +273,30 @@ const StaffInfo: React.FC = ({ combos }) => { ))} )} + /> */} + ( + + + + + )} /> @@ -254,9 +344,12 @@ const StaffInfo: React.FC = ({ combos }) => { control={control} name="currentPositionId" render={({ field }) => ( - {combos.position.map((position, index) => ( @@ -268,6 +361,10 @@ const StaffInfo: React.FC = ({ combos }) => { ))} + + )} /> @@ -275,7 +372,7 @@ const StaffInfo: React.FC = ({ combos }) => { {t("Salary Point")} - ( @@ -294,7 +391,35 @@ const StaffInfo: React.FC = ({ combos }) => { ))} )} - /> + /> */} + ( + + + {/* + + )} + /> @@ -512,6 +637,36 @@ const StaffInfo: React.FC = ({ combos }) => { + {state.seModal && + + } + {state.teamModal && + + } + {state.gradeModal && + + } + {state.positionModal && + + } + {/* {tableKey && toggleModal("team")} combos={combos}/>} */} ); }; diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index c66abfc..4a52ea6 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -88,7 +88,7 @@ const EditStaff: React.FC = ({ Staff, combos, SalaryEffectiveInfo, In id: item.id, team: item.team.name, from: dayjs(item.from.join()).toDate(), - to: item.to ? dayjs(item.to.join()).toDate() : "", + to: item.to ? dayjs(item.to.join()).toDate() : undefined, }) }) : [], delTeamHistory: [], @@ -97,7 +97,7 @@ const EditStaff: React.FC = ({ Staff, combos, SalaryEffectiveInfo, In id: item.id, grade: item.grade.name, from: dayjs(item.from.join()).toDate(), - to: item.to ? dayjs(item.to.join()).toDate() : "", + to: item.to ? dayjs(item.to.join()).toDate() : undefined, }) }) : [], delGradeHistory: [], @@ -106,7 +106,7 @@ const EditStaff: React.FC = ({ Staff, combos, SalaryEffectiveInfo, In id: item.id, position: item.position.name, from: dayjs(item.from.join()).toDate(), - to: item.to ? dayjs(item.to.join()).toDate() : "", + to: item.to ? dayjs(item.to.join()).toDate() : undefined, }) }) : [], delPositionHistory: [], @@ -180,21 +180,26 @@ const EditStaff: React.FC = ({ Staff, combos, SalaryEffectiveInfo, In id: item.id, team: combos.team.filter(team => team.label === item.team)[0].id, from: dayjs(item.from).format('YYYY-MM-DD'), - to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, + // to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, })) + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + const gradeHistory = data.gradeHistory.map((item) => ({ id: item.id, grade: combos.grade.filter(grade => grade.label === item.grade)[0].id, from: dayjs(item.from).format('YYYY-MM-DD'), - to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, + // to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, })) + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + const positionHistory = data.positionHistory.map((item) => ({ id: item.id, position: combos.position.filter(position => position.label === item.position)[0].id, from: dayjs(item.from).format('YYYY-MM-DD'), - to: (item.to as string).length != 0 ? dayjs(item.to).format('YYYY-MM-DD') : undefined, + // to: item.to != undefined ? dayjs(item.to).format('YYYY-MM-DD') : undefined, })) - + .sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()) + const salaryEffectiveInfo = data.salaryEffectiveInfo.map((item: SalaryEffectiveInfo) => ({ id: item.id, salaryPoint: chopSalaryPoints(item.salaryPoint), diff --git a/src/components/EditStaff/GradeHistoryModal.tsx b/src/components/EditStaff/GradeHistoryModal.tsx index 7461708..2f65d80 100644 --- a/src/components/EditStaff/GradeHistoryModal.tsx +++ b/src/components/EditStaff/GradeHistoryModal.tsx @@ -1,20 +1,26 @@ -import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" +import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" import StyledDataGrid from "../StyledDataGrid" import { useTranslation } from "react-i18next"; import { useFormContext } from "react-hook-form"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; import AddIcon from '@mui/icons-material/Add'; import SaveIcon from '@mui/icons-material/Save'; import DeleteIcon from '@mui/icons-material/Delete'; import CancelIcon from '@mui/icons-material/Cancel'; import EditIcon from '@mui/icons-material/Edit'; import waitForCondition from "../utils/waitFor"; +import { gradeHistory } from "@/app/api/staff/actions"; +import { Add } from "@mui/icons-material"; +import { comboItem } from "../CreateStaff/CreateStaff"; +import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; +import { ProcessRowUpdateError } from "./TeamHistoryModal"; interface Props { open: boolean; onClose: () => void; - columns: any[] + combos: comboItem; + // columns: any[] } const modalSx: SxProps = { @@ -31,119 +37,217 @@ const modalSx: SxProps = { gap: 2, }; -const GradeHistoryModal: React.FC = async ({ open, onClose, columns }) => { +export type GradeModalRow = Partial< +gradeHistory & { + _isNew: boolean + _error: StaffEntryError; +}> +const thisField = "gradeHistory" +const GradeHistoryModal: React.FC = ({ open, onClose, combos }) => { const { t, // i18n: { language }, } = useTranslation(); - const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); + const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); - const [count, setCount] = useState(0); + const apiRef = useGridApiRef() + const originalRows = getValues(thisField) const [_rows, setRows] = useState(() => { - const list = getValues('gradeHistory') + const list: GradeModalRow[] = getValues(thisField) return list && list.length > 0 ? list : [] }); const [_delRows, setDelRows] = useState([]); - 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>( + (row) => row.id!!, + [], + ); + const handleSave = useCallback( + (id: GridRowId) => () => { + setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View } })); - } - return true; - } - const handleSaveAll = async () => { - // trigger save all - console.log(_rows) - await waitForCondition(async () => { - return looping() - }) - console.log(rowModesModel) - }; + }, + [setRowModesModel] + ); + + const onCancel = useCallback(() => { + setRows(originalRows) + onClose(); + }, [onClose, originalRows]); - const bigTesting = async () => { - await looping() - setTimeout(() => { - onClose() - }, 800) - } - - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; + const handleClose = useCallback>( + (_, 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) => { + 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, + originalRow: GridRowModel + ) => { + const rowIndex = _rows.findIndex((row: GradeModalRow) => row.id === newRow.id); + const prevRow: GradeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; + const errors = validateRowAndRowBefore(prevRow, newRow) + console.log(errors) + if (errors) { + throw new ProcessRowUpdateError( + prevRow, + newRow, + "validation error", + errors + ) + } + const { _isNew, _error, ...updatedRow } = newRow; + + const rowToSave = { + ...updatedRow, + } + console.log(_rows) + if (_rows.length != 0) { + setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { + if (!a.from || !b.from) return 0; + return new Date(a.from).getTime() - new Date(b.from).getTime(); + })); + } + return rowToSave; } - // , [_rows, setValue, setRows]) + , [_rows, validateRowAndRowBefore]) - const handleSaveClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.View } - })); - }, - [setRowModesModel] - ); - const handleCancelClick = useCallback( + const handleCancel = useCallback( (id: any) => () => { setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View, ignoreModifications: true } })); + const editedRow = _rows.find((r) => getRowId(r) === id) + if (editedRow?._isNew) { + setRows((rw) => rw.filter((r) => r.id !== id)) + } else { + setRows((rw) => + rw.map((r) => + getRowId(r) === id + ? { ...r, _error: undefined } + : r, + ), + ); + } }, - [setRowModesModel] - ); - - const handleEditClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.Edit } - })); - }, - [setRowModesModel] + [setRowModesModel, _rows] ); - const handleDeleteClick = useCallback( - (id: any) => () => { - setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); - setCount((prev: number) => prev - 1); - setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) + const handleDelete = useCallback( + (id: GridRowId) => () => { + setRows((prevRows) => prevRows.filter((row) => row.id !== id)); + setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) }, - [setRows, setCount, setDelRows] + [] ); useEffect(()=> { console.log(_rows) - setValue('gradeHistory', _rows) + // setValue(thisField, _rows) setValue('delGradeHistory', _delRows) }, [_rows, _delRows]) - const defaultCol = useMemo( - () => ( + const footer = ( + + + + ) + const columns = useMemo( + () => [ + { + field: 'grade', + headerName: 'grade', + flex: 1, + editable: true, + type: 'singleSelect', + valueOptions: combos.grade.map(item => item.label), + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ( + + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, + { + field: 'from', + headerName: 'from', + flex: 1, + editable: true, + type: 'date', + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, { field: 'actions', type: 'actions', @@ -161,42 +265,29 @@ const GradeHistoryModal: React.FC = async ({ open, onClose, columns }) => sx={{ color: 'primary.main' }} - onClick={handleSaveClick(id)} + onClick={handleSave(id)} />, } label="Cancel" key="edit" - onClick={handleCancelClick(id)} + onClick={handleCancel(id)} /> ]; } return [ - } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - key="edit" - />, } label="Delete" sx={{ color: 'error.main' }} - onClick={handleDeleteClick(id)} color="inherit" key="edit" /> + onClick={handleDelete(id)} color="inherit" key="edit" /> ]; } } - ), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] - ) - - let _columns: any[] = [] - if (columns) { - _columns = [...columns, defaultCol] - } + ], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) + return ( @@ -204,25 +295,48 @@ const GradeHistoryModal: React.FC = async ({ open, onClose, columns }) => {t('GradeHistoryModal')} ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError" + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} /> - - @@ -231,4 +345,20 @@ const GradeHistoryModal: React.FC = async ({ open, onClose, columns }) => ) } +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; export default GradeHistoryModal \ No newline at end of file diff --git a/src/components/EditStaff/PositionHistoryModal.tsx b/src/components/EditStaff/PositionHistoryModal.tsx index 659e5f3..cea1002 100644 --- a/src/components/EditStaff/PositionHistoryModal.tsx +++ b/src/components/EditStaff/PositionHistoryModal.tsx @@ -1,20 +1,26 @@ -import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material" +import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material" import StyledDataGrid from "../StyledDataGrid" import { useTranslation } from "react-i18next"; import { useFormContext } from "react-hook-form"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; import AddIcon from '@mui/icons-material/Add'; import SaveIcon from '@mui/icons-material/Save'; import DeleteIcon from '@mui/icons-material/Delete'; import CancelIcon from '@mui/icons-material/Cancel'; import EditIcon from '@mui/icons-material/Edit'; import waitForCondition from "../utils/waitFor"; +import { Add } from "@mui/icons-material"; +import { positionHistory } from "@/app/api/staff/actions"; +import { comboItem } from "../CreateStaff/CreateStaff"; +import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; +import { ProcessRowUpdateError } from "./TeamHistoryModal"; +import { createSearchParamsBailoutProxy } from "next/dist/client/components/searchparams-bailout-proxy"; interface Props { open: boolean; onClose: () => void; - columns: any[] + combos: comboItem; } const modalSx: SxProps = { @@ -31,171 +37,267 @@ const modalSx: SxProps = { gap: 2, }; -const PositionHistoryModal: React.FC = async ({ open, onClose, columns }) => { +export type PositionModalRow = Partial< +positionHistory & { + _isNew: boolean + _error: StaffEntryError; +}> + +const thisField = "positionHistory" +const PositionHistoryModal: React.FC = ({ + open, + onClose, + combos +}) => { const { t, // i18n: { language }, } = useTranslation(); - const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); + const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); - const [count, setCount] = useState(0); + const apiRef = useGridApiRef() + const originalRows = getValues(thisField) const [_rows, setRows] = useState(() => { - const list = getValues('positionHistory') + const list: PositionModalRow[] = getValues(thisField) return list && list.length > 0 ? list : [] }); const [_delRows, setDelRows] = useState([]); - const formValues = watch(); + const getRowId = useCallback>( + (row) => row.id!!, + [], + ); - const handleClose = () => { - onClose(); - }; - - const looping = async () => { - for (let i = 0; i < _rows.length; i++) { - const id = _rows[i].id - setRowModesModel((prevRowModesModel) => ({ + const handleSave = useCallback( + (id: GridRowId) => () => { + setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View } })); - } - return true; - } - const handleSaveAll = async () => { - // trigger save all - console.log(_rows) - await waitForCondition(async () => { - return looping() + }, + [setRowModesModel] + ); + + const onCancel = useCallback(() => { + console.log(originalRows) + setRows(originalRows) + onClose(); + }, [onClose, originalRows]); + + const handleClose = useCallback>( + (_, 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) => { + const errors = updateError.errors; + const prevRow = updateError.prevRow; + const currRow = updateError.currRow; + + // if (updateError.prevRow) { + // apiRef.current.updateRows([{ ...prevRow, _error: errors }]); + // } + apiRef.current.updateRows([{ ...currRow, _error: errors }]); + }, + [apiRef, rowModesModel], + ); // handle row update here - const processRowUpdate = - // useCallback( - (newRow: GridRowModel) => { - console.log(newRow) - const updatedRow = { ...newRow, updated: true }; + const processRowUpdate = useCallback(( + newRow: GridRowModel, + originalRow: GridRowModel + ) => { + const rowIndex = _rows.findIndex((row: PositionModalRow) => row.id === newRow.id); + const prevRow: PositionModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; + const errors = validateRowAndRowBefore(prevRow, newRow) + console.log(errors) + if (errors) { + throw new ProcessRowUpdateError( + prevRow, + newRow, + "validation error", + errors + ) + } + const { _isNew, _error, ...updatedRow } = newRow; + + const rowToSave = { + ...updatedRow, + } console.log(_rows) if (_rows.length != 0) { - setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); + setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { + if (!a.from || !b.from) return 0; + return new Date(a.from).getTime() - new Date(b.from).getTime(); + })); } - return updatedRow; - } - // , [_rows, setValue, setRows]) - - const handleSaveClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.View } - })); - }, - [setRowModesModel] - ); - const handleCancelClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.View, ignoreModifications: true } - })); - }, - [setRowModesModel] - ); + return rowToSave; + } + , [_rows, validateRowAndRowBefore]) - const handleEditClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.Edit } + const addRow = useCallback(() => { + const newEntry = { id: Date.now(), _isNew: true } satisfies PositionModalRow; + setRows((prev) => [...prev, newEntry]) + setRowModesModel((model) => ({ + ...model, + [getRowId(newEntry)]: { + mode: GridRowModes.Edit, + fieldToFocus: "position", + } + })) + }, [getRowId]); + + const handleCancel = useCallback( + (id: GridRowId) => () => { + setRowModesModel((model) => ({ + ...model, + [id]: { mode: GridRowModes.View, ignoreModifications: true } })); + const editedRow = _rows.find((row) => getRowId(row) === id); + console.log(editedRow) + if (editedRow?._isNew) { + setRows((rw) => rw.filter((r) => r.id !== id)) + } else { + setRows((rw) => + rw.map((r) => + getRowId(r) === id + ? { ...r, _error: undefined } + : r, + ), + ); + } }, - [setRowModesModel] + [setRowModesModel, _rows] ); - const handleDeleteClick = useCallback( - (id: any) => () => { - setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); - setCount((prev: number) => prev - 1); - setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) + const handleDelete = useCallback( + (id: GridRowId) => () => { + setRows((prevRows) => prevRows.filter((row) => row.id !== id)); + setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) }, - [setRows, setCount, setDelRows] + [setRows, setDelRows] ); useEffect(()=> { console.log(_rows) - setValue('positionHistory', _rows) + setValue(thisField, _rows) setValue('delPositionHistory', _delRows) }, [_rows, _delRows]) - const defaultCol = useMemo( - () => ( - { - field: 'actions', - type: 'actions', - headerName: 'edit', - width: 100, - cellClassName: 'actions', - getActions: ({ id }: { id: number }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - if (isInEditMode) { - return [ - } - label="Save" - key="edit" - sx={{ - color: 'primary.main' - }} - onClick={handleSaveClick(id)} - />, - } - 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) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ( + + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, + { + field: 'from', + headerName: 'from', + flex: 1, + editable: true, + type: 'date', + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + 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 [ } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" + icon={} + label="Save" key="edit" - />, - } - label="Delete" sx={{ - color: 'error.main' + color: 'primary.main' }} - onClick={handleDeleteClick(id)} color="inherit" key="edit" /> + onClick={handleSave(id)} + />, + } + label="Cancel" + key="edit" + onClick={handleCancel(id)} + /> ]; } + return [ + } + 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 = ( + + + + ) + return ( @@ -203,25 +305,48 @@ const PositionHistoryModal: React.FC = async ({ open, onClose, columns }) {t('PositionHistoryModal')} ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError" + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} /> - - @@ -230,4 +355,22 @@ const PositionHistoryModal: React.FC = async ({ open, onClose, columns }) ) } + +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; + export default PositionHistoryModal \ No newline at end of file diff --git a/src/components/EditStaff/SalaryEffectiveModel.tsx b/src/components/EditStaff/SalaryEffectiveModel.tsx index 334d19c..f120f98 100644 --- a/src/components/EditStaff/SalaryEffectiveModel.tsx +++ b/src/components/EditStaff/SalaryEffectiveModel.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps } from '@mui/material'; +import { Modal, Box, Typography, Button, TextField, FormControl, InputLabel, Select, MenuItem, Paper, SxProps, ModalProps, Tooltip } from '@mui/material'; import { useForm, Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; import dayjs from 'dayjs'; import { DatePicker } from '@mui/x-date-pickers'; -import { DataGrid, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from '@mui/x-data-grid'; +import { GridRenderEditCellParams, FooterPropsOverrides, GridEventListener, GridRowEditStopParams, GridRowEditStopReasons, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditSingleSelectCell, GridCellParams, GridRowId } from '@mui/x-data-grid'; import StyledDataGrid from '../StyledDataGrid'; import AddIcon from '@mui/icons-material/Add'; import SaveIcon from '@mui/icons-material/Save'; @@ -14,12 +14,17 @@ import CancelIcon from '@mui/icons-material/Cancel'; import EditIcon from '@mui/icons-material/Edit'; import { GridActionsCellItem } from '@mui/x-data-grid'; import waitForCondition from '../utils/waitFor'; +import { StaffEntryError, validateRowAndRowBefore } from './validateDates'; +import { salaryEffectiveInfo } from '@/app/api/staff/actions'; +import { comboItem } from "../CreateStaff/CreateStaff"; +import { Add } from '@mui/icons-material'; +import { GridEditDateCell } from '@mui/x-data-grid'; interface SalaryEffectiveModelProps { open: boolean; onClose: () => void; modalSx?: SxProps; - columns: any[] + combos: comboItem; } const modalSx: SxProps = { @@ -36,125 +41,133 @@ const modalSx: SxProps = { gap: 2, }; - function EditToolbar(props: React.JSXElementConstructor | 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 ( - - - {/* */} - - ); - } +export type SeModalRow = Partial< +salaryEffectiveInfo & { + _isNew: boolean + _error: StaffEntryError; +} +> +export class ProcessRowUpdateError 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 = ({ open, onClose, modalSx: mSx, columns }) => { + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} +const thisField = "salaryEffectiveInfo" +const SalaryEffectiveModel: React.FC = ({ open, onClose, modalSx: mSx, combos }) => { const { t, // i18n: { language }, } = useTranslation(); - const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); + const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); - const [count, setCount] = useState(0); + const apiRef = useGridApiRef() const [_rows, setRows] = useState(() => { - const list = getValues('salaryEffectiveInfo') + const list: SeModalRow[] = getValues(thisField) return list && list.length > 0 ? list : [] }); + const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) const [_delRows, setDelRows] = useState([]); - 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>( + (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>( + (_, reason) => { + if (reason !== "backdropClick") { + onClose(); + } + }, [onClose]); + + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const currRow = updateError.currRow; + console.log(errors) + apiRef.current.updateRows([{ ...currRow, _error: errors }]); + }, + [apiRef, rowModesModel], + ); + const processRowUpdate = useCallback(( + newRow: GridRowModel, + originalRow: GridRowModel ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; + const rowIndex = _rows.findIndex((row: SeModalRow) => row.id === newRow.id); + const prevRow: SeModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; + const errors = validateRowAndRowBefore(prevRow, newRow) + console.log(errors) + if (errors) { + throw new ProcessRowUpdateError( + prevRow, + newRow, + "validation error", + errors + ) + } + const { _isNew, _error, ...updatedRow } = newRow; + const rowToSave = { + ...updatedRow, + _isNew, } - }; - - const processRowUpdate = - // useCallback( - (newRow: GridRowModel) => { - console.log(newRow) - const updatedRow = { ...newRow, updated: true }; console.log(_rows) if (_rows.length != 0) { - setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); + setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { + if (!a.date || !b.date) return 0; + return new Date(a.date).getTime() - new Date(b.date).getTime(); + })); } - return updatedRow; + return rowToSave; } - // , [_rows, setValue, setRows]) + , [validateRowAndRowBefore, _rows]) - useEffect(()=> { - console.log(_rows) - setValue('salaryEffectiveInfo', _rows) - }, [_rows]) + // useEffect(()=> { + // console.log(_rows) + // setValue(thisField, _rows) + // }, [_rows]) useEffect(()=> { console.log(_delRows) setValue('delSalaryEffectiveInfo', _delRows) }, [_delRows]) - const handleSaveClick = useCallback((id: any) => () => { + const handleSave = useCallback((id: GridRowId) => () => { setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View } @@ -163,17 +176,8 @@ const SalaryEffectiveModel: React.FC = ({ open, onClo [setRowModesModel] ); - const handleSaveClickAsync = useCallback(async(id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.View } - })); - }, - [setRowModesModel] -); - - const handleCancelClick = useCallback( - (id: any) => () => { + const handleCancel = useCallback( + (id: GridRowId) => () => { setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View, ignoreModifications: true } @@ -182,27 +186,56 @@ const SalaryEffectiveModel: React.FC = ({ open, onClo [setRowModesModel] ); - const handleEditClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.Edit } - })); + const handleDelete = useCallback( + (id: GridRowId) => () => { + setRows((prevRows) => prevRows.filter((row: any) => row.id !== id)); + setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) }, - [setRowModesModel] + [] ); - const handleDeleteClick = useCallback( - (id: any) => () => { - setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); - setCount((prev: number) => prev - 1); - setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) - }, - [setRows, setCount, setDelRows] - ); - - const defaultCol = useMemo( - () => ( + + const columns = useMemo( + () => [ + { + field: 'salaryPoint', + headerName: 'salaryPoint', + flex: 1, + editable: true, + type: 'singleSelect', + valueOptions: combos.salary.map((item) => item.label), + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ( + + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, + { + field: 'date', + headerName: 'date', + flex: 1, + editable: true, + type: 'date', + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, { field: 'actions', type: 'actions', @@ -220,47 +253,54 @@ const SalaryEffectiveModel: React.FC = ({ open, onClo sx={{ color: 'primary.main' }} - onClick={handleSaveClick(id)} + onClick={handleSave(id)} />, } label="Cancel" key="edit" - onClick={handleCancelClick(id)} + onClick={handleCancel(id)} /> ]; } return [ - } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" - key="edit" - />, } label="Delete" sx={{ color: 'error.main' }} - onClick={handleDeleteClick(id)} color="inherit" key="edit" /> + onClick={handleDelete(id)} color="inherit" key="edit" /> ]; } } - ), [rowModesModel, handleSaveClick, handleCancelClick, handleEditClick, handleDeleteClick] - ) + ], [combos, rowModesModel, handleSave, handleCancel, handleDelete]) - - let _columns: any[] = [] - if (columns) { - _columns = [...columns, defaultCol] - } + const addRow = useCallback(() => { + const newEntry = { id: Date.now(), _isNew: true } satisfies SeModalRow; + setRows((prev) => [...prev, newEntry]) + setRowModesModel((model) => ({ + ...model, + [getRowId(newEntry)]: { + mode: GridRowModes.Edit, + fieldToFocus: "team", + } + })) + }, [getRowId]); - useEffect(() => { - console.log(_rows) - }, [_rows]) + const footer = ( + + + + ) return ( @@ -269,33 +309,70 @@ const SalaryEffectiveModel: React.FC = ({ open, onClo {t('Salary Effective Date Change')} ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError" + } + return classname; + }} slots={{ - toolbar: EditToolbar + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, }} slotProps={{ - toolbar: {count, setCount, setRows, setRowModesModel, _columns} + footer: { child: footer }, }} /> - - + {/* */} ); }; - +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; export default SalaryEffectiveModel; \ No newline at end of file diff --git a/src/components/EditStaff/StaffInfo.tsx b/src/components/EditStaff/StaffInfo.tsx index 9598b27..1c3a9f4 100644 --- a/src/components/EditStaff/StaffInfo.tsx +++ b/src/components/EditStaff/StaffInfo.tsx @@ -113,10 +113,6 @@ const StaffInfo: React.FC = ({ combos }) => { useEffect(() => { resetStaff() }, [defaultValues]); - - // useEffect(() => { - // console.log(state) - // }, [state]); const joinDate = watch("joinDate"); const departDate = watch("departDate"); @@ -126,112 +122,6 @@ const StaffInfo: React.FC = ({ combos }) => { if (departDate) clearErrors("departDate"); }, [joinDate, departDate]); - const salaryCols = useMemo( - () => [ - { - field: 'salaryPoint', - headerName: 'salaryPoint', - flex: 1, - editable: true, - type: 'singleSelect', - valueOptions: combos.salary.map((item) => item.label), - // valueOptions: [], - // width: 150 - }, - { - field: 'date', - headerName: 'date', - flex: 1, - editable: true, - type: 'date', - // width: 150 - }, - ], [combos]) - - const teamHistoryCols = useMemo( - () => [ - { - field: 'team', - headerName: 'team', - flex: 1, - editable: true, - type: 'singleSelect', - valueOptions: combos.team.map(item => item.label), - // valueOptions: [], - // width: 150 - }, - { - field: 'from', - headerName: 'from', - flex: 1, - editable: true, - type: 'date', - }, - { - field: 'to', - headerName: 'to', - flex: 1, - editable: true, - type: 'date', - }, - ], [combos]) - - const gradeHistoryCols = useMemo( - () => [ - { - field: 'grade', - headerName: 'grade', - flex: 1, - editable: true, - type: 'singleSelect', - valueOptions: combos.grade.map(item => item.label), - // valueOptions: [], - // width: 150 - }, - { - field: 'from', - headerName: 'from', - flex: 1, - editable: true, - type: 'date', - }, - { - field: 'to', - headerName: 'to', - flex: 1, - editable: true, - type: 'date', - }, - ], [combos]) - - const positionHistoryCols = useMemo( - () => [ - { - field: 'position', - headerName: 'position', - flex: 1, - editable: true, - type: 'singleSelect', - valueOptions: combos.position.map(item => item.label), - // valueOptions: [], - // width: 150 - }, - { - field: 'from', - headerName: 'from', - flex: 1, - editable: true, - type: 'date', - }, - { - field: 'to', - headerName: 'to', - flex: 1, - editable: true, - type: 'date', - }, - ], [combos]) - return ( @@ -311,6 +201,7 @@ const StaffInfo: React.FC = ({ combos }) => { label={t("Team")} style={{ flex: 1, marginRight: '8px' }} {...field} + disabled // error={Boolean(errors.teamId)} > {combos.team.map((team, index) => ( @@ -320,7 +211,7 @@ const StaffInfo: React.FC = ({ combos }) => { ))} @@ -367,6 +258,7 @@ const StaffInfo: React.FC = ({ combos }) => { style={{ flex: 1, marginRight: '8px' }} {...field} error={Boolean(errors.gradeId)} + disabled > {combos.grade.map((grade, index) => ( @@ -374,8 +266,7 @@ const StaffInfo: React.FC = ({ combos }) => { ))} - @@ -432,6 +323,7 @@ const StaffInfo: React.FC = ({ combos }) => { style={{ flex: 1, marginRight: '8px' }} {...field} error={Boolean(errors.currentPositionId)} + disabled > {combos.position.map((position, index) => ( = ({ combos }) => { ))} - @@ -656,7 +547,10 @@ const StaffInfo: React.FC = ({ combos }) => { label={t("Depart Date")} value={departDate ? dayjs(departDate) : null} onChange={(date) => { - if (!date) return; + if (!date) { + setValue("departDate", null); + return + }; dayjs(date).add(1, 'month') setValue("departDate", date.format(INPUT_DATE_FORMAT)); }} @@ -707,28 +601,28 @@ const StaffInfo: React.FC = ({ combos }) => { } {state.teamModal && } {state.gradeModal && } {state.positionModal && } diff --git a/src/components/EditStaff/TeamHistoryModal.tsx b/src/components/EditStaff/TeamHistoryModal.tsx index 92e8888..247efd7 100644 --- a/src/components/EditStaff/TeamHistoryModal.tsx +++ b/src/components/EditStaff/TeamHistoryModal.tsx @@ -1,20 +1,22 @@ -import { Box, Button, Modal, Paper, SxProps, Typography } from "@mui/material"; +import { Box, Button, Modal, ModalProps, Paper, SxProps, Tooltip, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { GridActionsCellItem, GridEventListener, GridRowEditStopReasons, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer } from "@mui/x-data-grid"; -import AddIcon from '@mui/icons-material/Add'; +import { GridRenderEditCellParams, FooterPropsOverrides, GridActionsCellItem, GridCellParams, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowIdGetter, GridRowModel, GridRowModes, GridRowModesModel, GridToolbarContainer, useGridApiRef, GridEditInputCell, GridColDef, GridEditDateCell, GridEditSingleSelectCell } from "@mui/x-data-grid"; import SaveIcon from '@mui/icons-material/Save'; import DeleteIcon from '@mui/icons-material/Delete'; import CancelIcon from '@mui/icons-material/Cancel'; -import EditIcon from '@mui/icons-material/Edit'; -import { useFormContext } from "react-hook-form"; +import { useForm, useFormContext } from "react-hook-form"; +import { Add } from "@mui/icons-material"; +import { CreateStaffInputs, teamHistory } from "@/app/api/staff/actions"; +import { StaffEntryError, validateRowAndRowBefore } from "./validateDates"; +import { comboItem } from "../CreateStaff/CreateStaff"; import waitForCondition from "../utils/waitFor"; interface Props { open: boolean; onClose: () => void; - columns: any[] + combos: comboItem; } const modalSx: SxProps = { @@ -31,172 +33,279 @@ interface Props { gap: 2, }; -const TeamHistoryModal: React.FC = async ({ open, onClose, columns }) => { + export type TeamModalRow = Partial< + teamHistory & { + _isNew: boolean + _error: StaffEntryError; + }> + + export class ProcessRowUpdateError 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 = ({ + open, + onClose, + combos, +}) => { const { t, // i18n: { language }, } = useTranslation(); - const { control, register, formState, trigger, watch, setValue, getValues } = useFormContext(); + const { setValue, getValues } = useFormContext(); const [rowModesModel, setRowModesModel] = useState({}); - const [count, setCount] = useState(0); + const apiRef = useGridApiRef() const [_rows, setRows] = useState(() => { - const list = getValues('teamHistory') + const list: TeamModalRow[] = getValues(thisField) return list && list.length > 0 ? list : [] }); + const originalRows = useMemo(() => _rows.filter(rw => rw._isNew !== true), [_rows]) const [_delRows, setDelRows] = useState([]); - const formValues = watch(); + const getRowId = useCallback>( + (row) => row.id!!, + [], + ); - const handleClose = () => { - onClose(); - }; - - const looping = async () => { - for (let i = 0; i < _rows.length; i++) { - const id = _rows[i].id - setRowModesModel((prevRowModesModel) => ({ + const handleSave = useCallback( + (id: GridRowId) => () => { + setRowModesModel((prevRowModesModel) => ({ ...prevRowModesModel, [id]: { mode: GridRowModes.View } })); + }, + [setRowModesModel] + ); + + const onCancel = useCallback(() => { + setRows(originalRows) + onClose(); + }, [onClose, originalRows]); + + const handleClose = useCallback>( + (_, 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) => { + const errors = updateError.errors; + const currRow = updateError.currRow; + console.log(errors) + apiRef.current.updateRows([{ ...currRow, _error: errors }]); + }, + [apiRef, rowModesModel], + ); + + const processRowUpdate = useCallback(( + newRow: GridRowModel, + originalRow: GridRowModel + ) => { + const rowIndex = _rows.findIndex((row: TeamModalRow) => row.id === newRow.id); + const prevRow: TeamModalRow | null = rowIndex > 0 ? _rows[rowIndex - 1] : null; + const errors = validateRowAndRowBefore(prevRow, newRow) + console.log(errors) + if (errors) { + throw new ProcessRowUpdateError( + prevRow, + newRow, + "validation error", + errors + ) + } + const { _isNew, _error, ...updatedRow } = newRow; + const rowToSave = { + ...updatedRow, } - return true; - } - const handleSaveAll = async () => { - // trigger save all console.log(_rows) - await waitForCondition(async () => { - return looping() - }) - console.log(rowModesModel) - }; - - const bigTesting = async () => { - await looping() - setTimeout(() => { - onClose() - }, 800) + if (_rows.length != 0) { + setRows((prev) => prev?.map((row) => (row.id === newRow.id ? rowToSave : row)).sort((a, b) => { + if (!a.from || !b.from) return 0; + return new Date(a.from).getTime() - new Date(b.from).getTime(); + })); + } + return rowToSave; } - const handleRowEditStop: GridEventListener<"rowEditStop"> = ( - params, - event, - ) => { - if (params.reason === GridRowEditStopReasons.rowFocusOut) { - event.defaultMuiPrevented = true; - } - }; - // handle row update here - const processRowUpdate = - // useCallback( - (newRow: GridRowModel) => { - console.log(newRow) - const updatedRow = { ...newRow, updated: true }; - console.log(_rows) - if (_rows.length != 0) { - setRows((prev: any[]) => prev?.map((row: any) => (row.id === newRow.id ? updatedRow : row))); + , [validateRowAndRowBefore, _rows]) + + const addRow = useCallback(() => { + const newEntry = { id: Date.now(), _isNew: true } satisfies TeamModalRow; + setRows((prev) => [...prev, newEntry]) + setRowModesModel((model) => ({ + ...model, + [getRowId(newEntry)]: { + mode: GridRowModes.Edit, + fieldToFocus: "team", } - return updatedRow; - } - // , [_rows, setValue, setRows]) + })) + }, [getRowId]); - const handleSaveClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.View } - })); - }, - [setRowModesModel] - ); - const handleCancelClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, + const handleCancel = useCallback( + (id: GridRowId) => () => { + setRowModesModel((model) => ({ + ...model, [id]: { mode: GridRowModes.View, ignoreModifications: true } })); + const editedRow = _rows.find((row) => getRowId(row) === id); + console.log(editedRow) + if (editedRow?._isNew) { + setRows((rw) => rw.filter((r) => r.id !== id)) + } else { + setRows((rw) => + rw.map((r) => + getRowId(r) === id + ? { ...r, _error: undefined } + : r, + ), + ); + } }, - [setRowModesModel] - ); - - const handleEditClick = useCallback( - (id: any) => () => { - setRowModesModel((prevRowModesModel) => ({ - ...prevRowModesModel, - [id]: { mode: GridRowModes.Edit } - })); - }, - [setRowModesModel] + [setRowModesModel, _rows] ); - const handleDeleteClick = useCallback( - (id: any) => () => { - setRows((prevRows: any) => prevRows.filter((row: any) => row.id !== id)); - setCount((prev: number) => prev - 1); - setDelRows((prevRowsId: number[]) => [...prevRowsId, id]) + const handleDelete = useCallback( + (id: GridRowId) => () => { + setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); + setDelRows((prevRowsId: number[]) => [...prevRowsId, id as number]) }, - [setRows, setCount, setDelRows] + [] ); useEffect(()=> { console.log(_rows) - setValue('teamHistory', _rows) + // setValue(thisField, _rows) setValue('delTeamHistory', _delRows) }, [_rows, _delRows]) - const defaultCol = useMemo( - () => ( - { - field: 'actions', - type: 'actions', - headerName: 'edit', - width: 100, - cellClassName: 'actions', - getActions: ({ id }: { id: number }) => { - const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; - if (isInEditMode) { - return [ - } - label="Save" - key="edit" - sx={{ - color: 'primary.main' - }} - onClick={handleSaveClick(id)} - />, - } - label="Cancel" - key="edit" - onClick={handleCancelClick(id)} - /> - ]; - } + const columns = useMemo( + () => [ + { + field: 'team', + headerName: 'team', + flex: 1, + editable: true, + type: 'singleSelect', + valueOptions: combos.team.map(item => item.label), + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ( + + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + } + }, + { + field: 'from', + headerName: 'from', + flex: 1, + editable: true, + type: 'date', + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = params.row._error?.[params.field as keyof StaffEntryError] + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + 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 [ } - label="Edit" - className="textPrimary" - onClick={handleEditClick(id)} - color="inherit" + icon={} + label="Save" key="edit" - />, - } - label="Delete" sx={{ - color: 'error.main' + color: 'primary.main' }} - onClick={handleDeleteClick(id)} color="inherit" key="edit" /> + onClick={handleSave(id)} + />, + } + label="Cancel" + key="edit" + onClick={handleCancel(id)} + /> ]; } + return [ + } + 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 = ( + + + + ) return ( @@ -204,25 +313,48 @@ const TeamHistoryModal: React.FC = async ({ open, onClose, columns }) => {t('TeamHistoryModal')} ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError" + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} /> - - @@ -231,4 +363,20 @@ const TeamHistoryModal: React.FC = async ({ open, onClose, columns }) => ) } +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; export default TeamHistoryModal \ No newline at end of file diff --git a/src/components/EditStaff/validateDates.ts b/src/components/EditStaff/validateDates.ts new file mode 100644 index 0000000..29db6dd --- /dev/null +++ b/src/components/EditStaff/validateDates.ts @@ -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 + +type AllFields = Partial + +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; + +} \ No newline at end of file From 388cd135f51e426c030d5a4bea75aa679db2c134 Mon Sep 17 00:00:00 2001 From: "DESKTOP\\derek" Date: Mon, 17 Feb 2025 16:05:58 +0800 Subject: [PATCH 11/16] saturday normal hr (cherry picked from commit 7d983ad5a99ee353e48ce5bd67a549403ba371c3) (cherry picked from commit 7929cfd7a059f91b96c14d78a955a394e995ad18) --- src/components/DateHoursTable/DateHoursTable.tsx | 11 ++++++++--- src/components/TimeLeaveModal/TimeLeaveModal.tsx | 3 +++ .../UserWorkspacePage/UserWorkspacePage.tsx | 4 ++++ .../UserWorkspacePage/UserWorkspaceWrapper.tsx | 4 ++++ src/config/authConfig.ts | 8 -------- src/middleware.ts | 6 ++++-- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/DateHoursTable/DateHoursTable.tsx b/src/components/DateHoursTable/DateHoursTable.tsx index f6d18e2..69a2b15 100644 --- a/src/components/DateHoursTable/DateHoursTable.tsx +++ b/src/components/DateHoursTable/DateHoursTable.tsx @@ -36,6 +36,7 @@ interface Props { EntryTableProps & { day: string; isHoliday: boolean } >; entryTableProps: EntryTableProps; + isSaturdayWorker: boolean } function DateHoursTable({ @@ -45,9 +46,9 @@ function DateHoursTable({ leaveEntries, timesheetEntries, companyHolidays, + isSaturdayWorker, }: Props) { const { t } = useTranslation("home"); - return ( @@ -71,6 +72,7 @@ function DateHoursTable({ timesheetEntries={timesheetEntries} EntryTableComponent={EntryTableComponent} entryTableProps={entryTableProps} + isSaturdayWorker={isSaturdayWorker} /> ); })} @@ -87,6 +89,7 @@ function DayRow({ entryTableProps, EntryTableComponent, companyHolidays, + isSaturdayWorker, }: { day: string; companyHolidays: HolidaysResult[]; @@ -96,16 +99,18 @@ function DayRow({ EntryTableProps & { day: string; isHoliday: boolean } >; entryTableProps: EntryTableProps; + isSaturdayWorker: boolean }) { const { t, i18n: { language }, } = useTranslation("home"); const dayJsObj = dayjs(day); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); const holiday = getHolidayForDate(day, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; const leaves = leaveEntries[day]; const leaveHours = diff --git a/src/components/TimeLeaveModal/TimeLeaveModal.tsx b/src/components/TimeLeaveModal/TimeLeaveModal.tsx index 36aaab9..7aa61ad 100644 --- a/src/components/TimeLeaveModal/TimeLeaveModal.tsx +++ b/src/components/TimeLeaveModal/TimeLeaveModal.tsx @@ -54,6 +54,7 @@ interface Props { isFullTime: boolean; joinDate: Dayjs; miscTasks: Task[]; + isSaturdayWorker: boolean } const modalSx: SxProps = { @@ -81,6 +82,7 @@ const TimeLeaveModal: React.FC = ({ isFullTime, joinDate, miscTasks, + isSaturdayWorker, }) => { const { t } = useTranslation("home"); @@ -227,6 +229,7 @@ const TimeLeaveModal: React.FC = ({ leaveTypes, miscTasks, }} + isSaturdayWorker={isSaturdayWorker} /> {errorComponent} diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 80a233b..c81d078 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -43,6 +43,7 @@ export interface Props { isFullTime: boolean; joinDate?: number | null; miscTasks: Task[]; + isSaturdayWorker: boolean } const menuItemSx: SxProps = { @@ -65,6 +66,7 @@ const UserWorkspacePage: React.FC = ({ isFullTime, joinDate, miscTasks, + isSaturdayWorker }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -183,6 +185,7 @@ const UserWorkspacePage: React.FC = ({ allProjects={allProjects} leaveTypes={leaveTypes} companyHolidays={holidays} + isSaturdayWorker={isSaturdayWorker} /> = ({ isFullTime={isFullTime} joinDate={dayjs(joinDate)} miscTasks={miscTasks} + isSaturdayWorker={isSaturdayWorker} /> { const miscTasks = allTasks.filter((t) => t.taskGroup.id === 5); + const isSaturdayWorker = abilities.includes(SATURDAY_WORKERS) + return ( { maintainManagementStaffWorkspaceAbility={ maintainManagementStaffWorkspaceAbility } + isSaturdayWorker={isSaturdayWorker} /> ); }; diff --git a/src/config/authConfig.ts b/src/config/authConfig.ts index 5e12b22..53e9fba 100644 --- a/src/config/authConfig.ts +++ b/src/config/authConfig.ts @@ -58,14 +58,6 @@ export const authOptions: AuthOptions = { jwt(params) { // Add the data from user to the token const { token, user, account, trigger, session } = params; - // console.log("--------------------------") - // console.log("%c [ token ]:", 'font-size:13px; background:#A888B5; color:#bf2c9f;', token) - // console.log("%c [ user ]:", 'font-size:13px; background:pink; color:#bf2c9f;', user) - // console.log("%c [ account ]:", 'font-size:13px; background:pink; color:#bf2c9f;', account) - // console.log("%c [ session ]:", 'font-size:13px; background:#FFD2A0; color:#bf2c9f;', session) - // console.log("%c [ trigger ]:", 'font-size:13px; background:#EFB6C8; color:#bf2c9f;', trigger) - // console.log(params) - // console.log("--------------------------") if (trigger === "update" && session?.accessToken && session?.refreshToken) { token.accessToken = session.accessToken diff --git a/src/middleware.ts b/src/middleware.ts index 2277652..c2519f5 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -71,7 +71,8 @@ export const [ GENERATE_PROJECT_CASH_FLOW_REPORT, GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, GENERATE_CROSS_TEAM_CHARGE_REPORT, - VIEW_ALL_PROJECTS + VIEW_ALL_PROJECTS, + SATURDAY_WORKERS ] = [ 'MAINTAIN_USER', 'MAINTAIN_TIMESHEET', @@ -124,7 +125,8 @@ export const [ 'G_PROJECT_CASH_FLOW_REPORT', 'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', 'G_CROSS_TEAM_CHARGE_REPORT', - 'VIEW_ALL_PROJECTS' + 'VIEW_ALL_PROJECTS', + 'SATURDAY_WORKERS' ] const PRIVATE_ROUTES = [ From fe2db352b781818933897c212673a1d7cd56ba73 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 27 Feb 2025 17:24:14 +0800 Subject: [PATCH 12/16] update row item 145 --- src/components/LeaveModal/LeaveCalendar.tsx | 11 ++++++++--- src/components/LeaveModal/LeaveModal.tsx | 3 +++ .../TimesheetAmendment/TimesheetAmendment.tsx | 11 ++++++++--- .../TimesheetAmendment/TimesheetAmendmentModal.tsx | 3 +++ .../UserWorkspacePage/UserWorkspacePage.tsx | 3 ++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/LeaveModal/LeaveCalendar.tsx b/src/components/LeaveModal/LeaveCalendar.tsx index e0dcaeb..36be589 100644 --- a/src/components/LeaveModal/LeaveCalendar.tsx +++ b/src/components/LeaveModal/LeaveCalendar.tsx @@ -36,6 +36,7 @@ export interface Props { timesheetRecords: RecordTimesheetInput; isFullTime: boolean; joinDate: Dayjs; + isSaturdayWorker: boolean } interface EventClickArg { @@ -57,6 +58,7 @@ const LeaveCalendar: React.FC = ({ leaveRecords, isFullTime, joinDate, + isSaturdayWorker }) => { const { t, @@ -190,7 +192,8 @@ const LeaveCalendar: React.FC = ({ ({ event }: EventClickArg) => { const dayJsObj = dayjs(event.startStr); const holiday = getHolidayForDate(event.startStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; if ( event.extendedProps.calendar === "leaveEntry" && @@ -210,7 +213,8 @@ const LeaveCalendar: React.FC = ({ (e: { dateStr: string; dayEl: HTMLElement }) => { const dayJsObj = dayjs(e.dateStr); const holiday = getHolidayForDate(e.dateStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); }, @@ -224,7 +228,8 @@ const LeaveCalendar: React.FC = ({ } const dayJsObj = dayjs(date); const holiday = getHolidayForDate(date, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; const leaves = localLeaveRecords[date] || []; const timesheets = timesheetRecords[date] || []; diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 0bd5687..f715e60 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -25,6 +25,7 @@ const modalSx: SxProps = { interface Props extends LeaveCalendarProps { open: boolean; onClose: () => void; + isSaturdayWorker: boolean } const LeaveModal: React.FC = ({ @@ -37,6 +38,7 @@ const LeaveModal: React.FC = ({ timesheetRecords, isFullTime, joinDate, + isSaturdayWorker }) => { const { t } = useTranslation("home"); const isMobile = useIsMobile(); @@ -51,6 +53,7 @@ const LeaveModal: React.FC = ({ allProjects={allProjects} leaveRecords={leaveRecords} timesheetRecords={timesheetRecords} + isSaturdayWorker={isSaturdayWorker} /> ); diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index e0bb4ab..24dbad5 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -51,6 +51,7 @@ export interface Props { companyHolidays: HolidaysResult[]; allProjects: ProjectWithTasks[]; miscTasks: Task[]; + isSaturdayWorker: boolean } type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string }; @@ -79,6 +80,7 @@ const TimesheetAmendment: React.FC = ({ allProjects, leaveTypes, miscTasks, + isSaturdayWorker }) => { const { t, i18n:{language}} = useTranslation(["home", "common"]); const locale = language === "zh" ? "zh-tw" : "en"; @@ -306,7 +308,8 @@ const TimesheetAmendment: React.FC = ({ ({ event }: EventClickArg) => { const dayJsObj = dayjs(event.startStr); const holiday = getHolidayForDate(event.startStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; if ( event.extendedProps.calendar === "timeEntry" && @@ -335,7 +338,8 @@ const TimesheetAmendment: React.FC = ({ (e: { dateStr: string; dayEl: HTMLElement }) => { const dayJsObj = dayjs(e.dateStr); const holiday = getHolidayForDate(e.dateStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; setSelectedDateInfo({ dateStr: e.dateStr, isHoliday: Boolean(isHoliday), @@ -352,7 +356,8 @@ const TimesheetAmendment: React.FC = ({ } const dayJsObj = dayjs(date); const holiday = getHolidayForDate(date, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; const intStaffId = parseInt(selectedStaff.id); const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; diff --git a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx index 65725ed..e62c9f2 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx @@ -27,6 +27,7 @@ const modalSx: SxProps = { interface Props extends TimesheetAmendmentProps { open: boolean; onClose: () => void; + isSaturdayWorker: boolean } export const TimesheetAmendmentModal: React.FC = ({ @@ -38,6 +39,7 @@ export const TimesheetAmendmentModal: React.FC = ({ companyHolidays, allProjects, miscTasks, + isSaturdayWorker }) => { const { t } = useTranslation("home"); const isMobile = useIsMobile(); @@ -51,6 +53,7 @@ export const TimesheetAmendmentModal: React.FC = ({ teamTimesheets={teamTimesheets} allProjects={allProjects} miscTasks={miscTasks} + isSaturdayWorker={isSaturdayWorker} /> ); diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index c81d078..0f3611f 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -185,7 +185,6 @@ const UserWorkspacePage: React.FC = ({ allProjects={allProjects} leaveTypes={leaveTypes} companyHolidays={holidays} - isSaturdayWorker={isSaturdayWorker} /> = ({ timesheetRecords={defaultTimesheets} isFullTime={isFullTime} joinDate={dayjs(joinDate)} + isSaturdayWorker={isSaturdayWorker} /> {assignedProjects.length > 0 ? ( = ({ open={isTimesheetAmendmentVisible} onClose={handleAmendmentClose} miscTasks={miscTasks} + isSaturdayWorker={isSaturdayWorker} /> )} From ea3f12b9b5b9e669a11e25f71e510f8e486caf38 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 3 Mar 2025 15:49:59 +0800 Subject: [PATCH 13/16] update status button in project search --- .../ProjectSearch/ProjectSearch.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 6a9865b..6a7ecf0 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -14,6 +14,7 @@ import { loadDrafts } from "@/app/utils/draftUtils"; import { TeamResult } from "@/app/api/team"; import { Customer } from "@/app/api/customer"; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'; type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean }; @@ -194,11 +195,29 @@ const ProjectSearch: React.FC = ({ { name: "status", label: t("Status"), - type: "link", + // 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, - underlines: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "none"}), {}), - colors: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "inherit"}), {}), - } + buttonIcon: , + disabled: !abilities.includes(MAINTAIN_PROJECT), + disabledRows: { + status: ignoreStatusList + } + }, ], [t, onProjectClick], ); From b90aa150edaaf1a60d37e2e39686bc43fd69473e Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 11 Mar 2025 13:45:56 +0800 Subject: [PATCH 14/16] no message --- .../TimesheetAmendment/TimesheetAmendment.tsx | 22 ++++++++++--------- .../TimesheetAmendmentModal.tsx | 5 +++-- .../UserWorkspacePage/UserWorkspacePage.tsx | 9 ++++---- .../UserWorkspaceWrapper.tsx | 4 +++- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index 24dbad5..c224486 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -51,7 +51,8 @@ export interface Props { companyHolidays: HolidaysResult[]; allProjects: ProjectWithTasks[]; miscTasks: Task[]; - isSaturdayWorker: boolean + isSaturdayWorker: boolean; + userId: number; } type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string }; @@ -80,7 +81,8 @@ const TimesheetAmendment: React.FC = ({ allProjects, leaveTypes, miscTasks, - isSaturdayWorker + isSaturdayWorker, + userId }) => { const { t, i18n:{language}} = useTranslation(["home", "common"]); const locale = language === "zh" ? "zh-tw" : "en"; @@ -119,16 +121,19 @@ const TimesheetAmendment: React.FC = ({ }); }, [], - ).sort((a, b) => a.staffId.localeCompare(b.staffId)); + ); }, [localTeamLeaves, localTeamTimesheets]); const [selectedStaff, setSelectedStaff] = useState( - allMembers[0], + allMembers.find((member) => parseInt(member.id) === userId) ?? allMembers[0] ); useEffect(() => { + console.log(allMembers) + console.log(userId) setSelectedStaff( (currentStaff) => allMembers.find((member) => member.id === currentStaff.id) || + allMembers.find((member) => parseInt(member.id) === userId) || allMembers[0], ); }, [allMembers]); @@ -308,8 +313,7 @@ const TimesheetAmendment: React.FC = ({ ({ event }: EventClickArg) => { const dayJsObj = dayjs(event.startStr); const holiday = getHolidayForDate(event.startStr, companyHolidays); - const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; - const isHoliday = holiday || isWeekend; + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; if ( event.extendedProps.calendar === "timeEntry" && @@ -338,8 +342,7 @@ const TimesheetAmendment: React.FC = ({ (e: { dateStr: string; dayEl: HTMLElement }) => { const dayJsObj = dayjs(e.dateStr); const holiday = getHolidayForDate(e.dateStr, companyHolidays); - const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; - const isHoliday = holiday || isWeekend; + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; setSelectedDateInfo({ dateStr: e.dateStr, isHoliday: Boolean(isHoliday), @@ -356,8 +359,7 @@ const TimesheetAmendment: React.FC = ({ } const dayJsObj = dayjs(date); const holiday = getHolidayForDate(date, companyHolidays); - const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; - const isHoliday = holiday || isWeekend; + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; const intStaffId = parseInt(selectedStaff.id); const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; diff --git a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx index e62c9f2..0018419 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx @@ -27,7 +27,6 @@ const modalSx: SxProps = { interface Props extends TimesheetAmendmentProps { open: boolean; onClose: () => void; - isSaturdayWorker: boolean } export const TimesheetAmendmentModal: React.FC = ({ @@ -39,7 +38,8 @@ export const TimesheetAmendmentModal: React.FC = ({ companyHolidays, allProjects, miscTasks, - isSaturdayWorker + isSaturdayWorker, + userId }) => { const { t } = useTranslation("home"); const isMobile = useIsMobile(); @@ -54,6 +54,7 @@ export const TimesheetAmendmentModal: React.FC = ({ allProjects={allProjects} miscTasks={miscTasks} isSaturdayWorker={isSaturdayWorker} + userId={userId} /> ); diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 0f3611f..029129b 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -43,7 +43,8 @@ export interface Props { isFullTime: boolean; joinDate?: number | null; miscTasks: Task[]; - isSaturdayWorker: boolean + isSaturdayWorker: boolean; + userId: number; } const menuItemSx: SxProps = { @@ -66,7 +67,8 @@ const UserWorkspacePage: React.FC = ({ isFullTime, joinDate, miscTasks, - isSaturdayWorker + isSaturdayWorker, + userId }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -199,7 +201,6 @@ const UserWorkspacePage: React.FC = ({ isFullTime={isFullTime} joinDate={dayjs(joinDate)} miscTasks={miscTasks} - isSaturdayWorker={isSaturdayWorker} /> = ({ timesheetRecords={defaultTimesheets} isFullTime={isFullTime} joinDate={dayjs(joinDate)} - isSaturdayWorker={isSaturdayWorker} /> {assignedProjects.length > 0 ? ( = ({ onClose={handleAmendmentClose} miscTasks={miscTasks} isSaturdayWorker={isSaturdayWorker} + userId={userId} /> )} diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index 8779079..467863a 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -16,7 +16,6 @@ import { MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, - SATURDAY_WORKERS } from "@/middleware"; import { fetchAllTasks } from "@/app/api/tasks"; @@ -62,6 +61,8 @@ const UserWorkspaceWrapper: React.FC = async () => { const isSaturdayWorker = abilities.includes(SATURDAY_WORKERS) + console.log(userStaff) + return ( { maintainManagementStaffWorkspaceAbility } isSaturdayWorker={isSaturdayWorker} + userId={userStaff?.id} /> ); }; From 1dfaf4dd4fdfaa57422fa89c0aed743533742378 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Tue, 11 Mar 2025 14:26:34 +0800 Subject: [PATCH 15/16] align with main branch --- src/components/TimesheetAmendment/TimesheetAmendment.tsx | 9 ++++++--- .../TimesheetAmendment/TimesheetAmendmentModal.tsx | 1 + src/components/UserWorkspacePage/UserWorkspacePage.tsx | 2 ++ .../UserWorkspacePage/UserWorkspaceWrapper.tsx | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index c224486..d12999c 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -313,7 +313,8 @@ const TimesheetAmendment: React.FC = ({ ({ event }: EventClickArg) => { const dayJsObj = dayjs(event.startStr); const holiday = getHolidayForDate(event.startStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; if ( event.extendedProps.calendar === "timeEntry" && @@ -342,7 +343,8 @@ const TimesheetAmendment: React.FC = ({ (e: { dateStr: string; dayEl: HTMLElement }) => { const dayJsObj = dayjs(e.dateStr); const holiday = getHolidayForDate(e.dateStr, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; setSelectedDateInfo({ dateStr: e.dateStr, isHoliday: Boolean(isHoliday), @@ -359,7 +361,8 @@ const TimesheetAmendment: React.FC = ({ } const dayJsObj = dayjs(date); const holiday = getHolidayForDate(date, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0; + const isHoliday = holiday || isWeekend; const intStaffId = parseInt(selectedStaff.id); const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; diff --git a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx index 0018419..4c7b0a3 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx @@ -27,6 +27,7 @@ const modalSx: SxProps = { interface Props extends TimesheetAmendmentProps { open: boolean; onClose: () => void; + isSaturdayWorker: boolean; } export const TimesheetAmendmentModal: React.FC = ({ diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 029129b..07c21d2 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -200,6 +200,7 @@ const UserWorkspacePage: React.FC = ({ leaveRecords={defaultLeaveRecords} isFullTime={isFullTime} joinDate={dayjs(joinDate)} + isSaturdayWorker={isSaturdayWorker} miscTasks={miscTasks} /> = ({ timesheetRecords={defaultTimesheets} isFullTime={isFullTime} joinDate={dayjs(joinDate)} + isSaturdayWorker={isSaturdayWorker} /> {assignedProjects.length > 0 ? ( Date: Tue, 11 Mar 2025 12:10:17 +0800 Subject: [PATCH 16/16] no newline --- src/components/TimesheetAmendment/TimesheetAmendment.tsx | 2 +- src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx | 2 +- src/components/UserWorkspacePage/UserWorkspacePage.tsx | 2 +- src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index d12999c..95561a9 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -512,4 +512,4 @@ const TimesheetAmendment: React.FC = ({ ); }; -export default TimesheetAmendment; +export default TimesheetAmendment; \ No newline at end of file diff --git a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx index 4c7b0a3..6cfd193 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx @@ -82,4 +82,4 @@ export const TimesheetAmendmentModal: React.FC = ({ ); -}; +}; \ No newline at end of file diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 07c21d2..32d3a25 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -248,4 +248,4 @@ const UserWorkspacePage: React.FC = ({ ); }; -export default UserWorkspacePage; +export default UserWorkspacePage; \ No newline at end of file diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index aaa7a4e..17c865b 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -89,4 +89,4 @@ const UserWorkspaceWrapper: React.FC = async () => { ); }; -export default UserWorkspaceWrapper; +export default UserWorkspaceWrapper; \ No newline at end of file