Kaynağa Gözat

Merge branch 'develop'

develop
MSI\2Fi 5 ay önce
ebeveyn
işleme
8c016fe0a5
30 değiştirilmiş dosya ile 1763 ekleme ve 759 silme
  1. +34
    -3
      src/app/api/staff/actions.ts
  2. +31
    -6
      src/components/CreateProject/CreateProject.tsx
  3. +1
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  4. +15
    -0
      src/components/CreateProject/MilestoneSection.tsx
  5. +6
    -4
      src/components/CreateProject/ProjectClientDetails.tsx
  6. +89
    -12
      src/components/CreateStaff/CreateStaff.tsx
  7. +171
    -16
      src/components/CreateStaff/StaffInfo.tsx
  8. +8
    -3
      src/components/DateHoursTable/DateHoursTable.tsx
  9. +12
    -7
      src/components/EditStaff/EditStaff.tsx
  10. +242
    -112
      src/components/EditStaff/GradeHistoryModal.tsx
  11. +282
    -139
      src/components/EditStaff/PositionHistoryModal.tsx
  12. +231
    -154
      src/components/EditStaff/SalaryEffectiveModel.tsx
  13. +14
    -120
      src/components/EditStaff/StaffInfo.tsx
  14. +291
    -143
      src/components/EditStaff/TeamHistoryModal.tsx
  15. +61
    -0
      src/components/EditStaff/validateDates.ts
  16. +58
    -2
      src/components/ExpenseSearch/ExpenseSearch.tsx
  17. +61
    -4
      src/components/InvoiceSearch/InvoiceSearch.tsx
  18. +8
    -3
      src/components/LeaveModal/LeaveCalendar.tsx
  19. +3
    -0
      src/components/LeaveModal/LeaveModal.tsx
  20. +54
    -2
      src/components/ProjectSearch/ProjectSearch.tsx
  21. +77
    -13
      src/components/SearchResults/SearchResults.tsx
  22. +3
    -0
      src/components/TimeLeaveModal/TimeLeaveModal.tsx
  23. +1
    -1
      src/components/TimesheetAmendment/TimesheetAmendment.tsx
  24. +1
    -1
      src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx
  25. +1
    -1
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  26. +1
    -1
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx
  27. +0
    -8
      src/config/authConfig.ts
  28. +1
    -0
      src/i18n/en/common.json
  29. +2
    -1
      src/i18n/zh/common.json
  30. +4
    -2
      src/middleware.ts

+ 34
- 3
src/app/api/staff/actions.ts Dosyayı Görüntüle

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


+ 31
- 6
src/components/CreateProject/CreateProject.tsx Dosyayı Görüntüle

@@ -8,7 +8,7 @@ import Button, { ButtonProps } from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import React, {
useCallback,
useEffect,
@@ -118,7 +118,7 @@ const hasErrorsInTab = (
const CreateProject: React.FC<Props> = ({
isEditMode,
isCopyMode,
draftId,
draftId: initDraftId,
isSubProject,
mainProjects,
defaultInputs,
@@ -139,6 +139,7 @@ const CreateProject: React.FC<Props> = ({
customerTypes,
abilities,
}) => {
const [draftId, setDraftId] = useState(initDraftId)
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
@@ -575,9 +576,17 @@ const CreateProject: React.FC<Props> = ({
formProps.reset(draftInputs);
}, [draftId, formProps]);

const saveDraft = useCallback(() => {
saveToLocalStorage(draftId || Date.now(), formProps.getValues());
router.replace("/projects");
const saveDraft = useCallback(async () => {
const currentTimestamp = Date.now()

saveToLocalStorage(draftId || currentTimestamp, formProps.getValues());
const success = await successDialog("Save Success", t)

if (success && !draftId) {
setDraftId(currentTimestamp)
}
// router.replace("/projects");
}, [draftId, formProps, router]);

const handleDeleteDraft = useCallback(() => {
@@ -592,6 +601,21 @@ const CreateProject: React.FC<Props> = ({
}, t);
}, [draftId, router]);

// Auto click the button
const buttonRef = useRef<HTMLButtonElement>(null)
const searchParams = useSearchParams()

useEffect(() => {
if (buttonRef) {
const autoClickButton = searchParams.get("autoClick")
// const autoClickList = ["start", "complete", "reopen"]
if(autoClickButton && autoClickButton === "true") {
buttonRef.current?.click()
}
}
}, [buttonRef])

return (
<>
<FormProvider {...formProps}>
@@ -633,6 +657,7 @@ const CreateProject: React.FC<Props> = ({
<Stack direction="row" gap={1}>
{/* {!formProps.getValues("projectActualStart") && ( */}
<Button
ref={buttonRef}
name={buttonData.buttonName}
type="submit"
variant="contained"
@@ -770,7 +795,7 @@ const CreateProject: React.FC<Props> = ({
>
{t("Save Draft")}
</Button>
{draftId &&
{draftId &&
<Button
variant="outlined"
color="error"


+ 1
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Dosyayı Görüntüle

@@ -100,7 +100,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
projectInfo.projectId = null
projectInfo.projectCode = projectInfo.projectCode + "-copy"
projectInfo.projectName = projectInfo.projectName + "-copy"
projectInfo.projectStatus = ""
projectInfo.projectStatus = "" // backend will assign "Pending To Start" status
Object.entries(projectInfo.milestones).forEach(([key, value]) => {
projectInfo.milestones[Number(key)].payments.forEach(({ ...rest}, idx, orig) => {
orig[idx] = { ...rest, id: rest.id * -1 }


+ 15
- 0
src/components/CreateProject/MilestoneSection.tsx Dosyayı Görüntüle

@@ -39,6 +39,7 @@ import {
} from "@/app/utils/formatUtil";
import isDate from "lodash/isDate";
import BulkAddPaymentModal from "./BulkAddPaymentModal";
import { deleteDialog } from "../Swal/CustomAlerts";

interface Props {
taskGroupId: TaskGroup["id"];
@@ -292,6 +293,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
setPayments((currentPayments) => [...currentPayments, ...entries]);
setBulkAddModalOpen(false);
}, []);
const onBulkDelete = useCallback(() => {
deleteDialog(() => {
setPayments([])
}, t)
}, []);

const footer = (
<Box display="flex" gap={2} alignItems="center">
@@ -311,6 +317,15 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
>
{t("Bulk Add Payment Milestones")}
</Button>
<Button
variant="outlined"
startIcon={<Delete />}
color={"error"}
onClick={onBulkDelete}
size="small"
>
{t("Delete Payment Milestones")}
</Button>
</Box>
);



+ 6
- 4
src/components/CreateProject/ProjectClientDetails.tsx Dosyayı Görüntüle

@@ -477,7 +477,7 @@ const ProjectClientDetails: React.FC<Props> = ({
<NumericFormat
label={t("Expected Total Project Fee")}
fullWidth
prefix="$"
prefix="HK$"
onValueChange={(values) => {
// console.log(values)
onChange(values.floatValue)
@@ -513,7 +513,7 @@ const ProjectClientDetails: React.FC<Props> = ({
<NumericFormat
label={t("Sub-Contract Fee")}
fullWidth
prefix="$"
prefix="HK$"
onValueChange={(values) => {
// console.log(values)
onChange(values.floatValue)
@@ -575,7 +575,8 @@ const ProjectClientDetails: React.FC<Props> = ({
control={control}
options={allCustomers.map((customer) => ({
...customer,
label: `${customer.code} - ${customer.name}`,
label: `${customer.name}`,
// label: `${customer.code} - ${customer.name}`,
}))}
name="clientId"
label={t("Client")}
@@ -624,7 +625,8 @@ const ProjectClientDetails: React.FC<Props> = ({
const subsidiary = subsidiaryMap[subsidiaryId];
return {
id: subsidiary.id,
label: `${subsidiary.code} - ${subsidiary.name}`,
label: `${subsidiary.name}`,
// label: `${subsidiary.code} - ${subsidiary.name}`,
};
}),
]}


+ 89
- 12
src/components/CreateStaff/CreateStaff.tsx Dosyayı Görüntüle

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

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

const errors = formProps.formState.errors;

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

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

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

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


+ 171
- 16
src/components/CreateStaff/StaffInfo.tsx Dosyayı Görüntüle

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

interface Props {
combos: comboItem;
}

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

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

type ReducerAction = {
type: REDUCER_ACTION_TYPE
}

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

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

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

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

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

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


+ 8
- 3
src/components/DateHoursTable/DateHoursTable.tsx Dosyayı Görüntüle

@@ -36,6 +36,7 @@ interface Props<EntryTableProps = object> {
EntryTableProps & { day: string; isHoliday: boolean }
>;
entryTableProps: EntryTableProps;
isSaturdayWorker: boolean
}

function DateHoursTable<EntryTableProps>({
@@ -45,9 +46,9 @@ function DateHoursTable<EntryTableProps>({
leaveEntries,
timesheetEntries,
companyHolidays,
isSaturdayWorker,
}: Props<EntryTableProps>) {
const { t } = useTranslation("home");

return (
<TableContainer sx={{ maxHeight: 400 }}>
<Table stickyHeader>
@@ -71,6 +72,7 @@ function DateHoursTable<EntryTableProps>({
timesheetEntries={timesheetEntries}
EntryTableComponent={EntryTableComponent}
entryTableProps={entryTableProps}
isSaturdayWorker={isSaturdayWorker}
/>
);
})}
@@ -87,6 +89,7 @@ function DayRow<EntryTableProps>({
entryTableProps,
EntryTableComponent,
companyHolidays,
isSaturdayWorker,
}: {
day: string;
companyHolidays: HolidaysResult[];
@@ -96,16 +99,18 @@ function DayRow<EntryTableProps>({
EntryTableProps & { day: string; isHoliday: boolean }
>;
entryTableProps: EntryTableProps;
isSaturdayWorker: boolean
}) {
const {
t,
i18n: { language },
} = useTranslation("home");
const dayJsObj = dayjs(day);
const [open, setOpen] = useState(false);

const [open, setOpen] = useState(false);
const holiday = getHolidayForDate(day, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0;
const isHoliday = holiday || isWeekend;

const leaves = leaveEntries[day];
const leaveHours =


+ 12
- 7
src/components/EditStaff/EditStaff.tsx Dosyayı Görüntüle

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

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

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

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


+ 242
- 112
src/components/EditStaff/GradeHistoryModal.tsx Dosyayı Görüntüle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 282
- 139
src/components/EditStaff/PositionHistoryModal.tsx Dosyayı Görüntüle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

export default PositionHistoryModal

+ 231
- 154
src/components/EditStaff/SalaryEffectiveModel.tsx Dosyayı Görüntüle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 14
- 120
src/components/EditStaff/StaffInfo.tsx Dosyayı Görüntüle

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

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

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

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

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

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

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


+ 291
- 143
src/components/EditStaff/TeamHistoryModal.tsx Dosyayı Görüntüle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 61
- 0
src/components/EditStaff/validateDates.ts Dosyayı Görüntüle

@@ -0,0 +1,61 @@
import { gradeHistory, positionHistory, salaryEffectiveInfo, teamHistory } from "@/app/api/staff/actions";
import { GridRowId, GridRowModel } from "@mui/x-data-grid";
import dayjs, { Dayjs } from "dayjs";
import { TeamModalRow } from "./TeamHistoryModal";
import { GradeModalRow } from "./GradeHistoryModal";
import { PositionModalRow } from "./PositionHistoryModal";
import { SeModalRow } from "./SalaryEffectiveModel";
export type ValidateError = {
from: number[]
to: number[]
}

type RowModel = Partial<TeamModalRow & GradeModalRow & PositionModalRow & SeModalRow>

type AllFields = Partial<teamHistory & gradeHistory & positionHistory & salaryEffectiveInfo>

export type StaffEntryError = {
[field in keyof AllFields]?: string;
};

export const validateRowAndRowBefore = (
prevRow: RowModel | null,
currRow: RowModel
): StaffEntryError | undefined => {
const error: StaffEntryError = {}

if (prevRow) {
if ('from' in currRow && currRow.from !== undefined) {
if (dayjs(prevRow.from).diff(dayjs(currRow.from)) == 0) {
error.from = "The date should not be the same as last entry"
}
} else if ('date' in currRow && currRow.date !== undefined) {
if (dayjs(prevRow.date).diff(dayjs(currRow.date)) == 0) {
error.date = "The date should not be the same as last entry"
}
}
}
console.log(currRow)
if ('from' in currRow && !currRow.from) {
error.from = "The date cannot be empty"
}
if ('date' in currRow && !currRow.date) {
error.date = "The date cannot be empty"
}
// Check specific fields based on row type
if ('grade' in currRow && !currRow.grade) {
error.grade = "Grade cannot be empty"
}
if ('position' in currRow && !currRow.position) {
error.position = "Position cannot be empty"
}
if ('team' in currRow && !currRow.team) {
error.team = "Team cannot be empty"
}
console.log("error")
console.log(error)
console.log(currRow.from)

return Object.keys(error).length > 0 ? error : undefined;

}

+ 58
- 2
src/components/ExpenseSearch/ExpenseSearch.tsx Dosyayı Görüntüle

@@ -18,6 +18,7 @@ import {
Divider,
Grid,
Stack,
TextField,
Typography,
} from "@mui/material";
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil";
@@ -27,12 +28,18 @@ import { uniq } from "lodash";
import CreateExpenseModal from "./CreateExpenseModal";
import { ProjectResult } from "@/app/api/projects";
import StyledDataGrid from "../StyledDataGrid";
import { GridCellParams, GridColDef, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { GridCellParams, GridColDef, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid";
import { useGridApiRef } from "@mui/x-data-grid";
import { GridEventListener } from "@mui/x-data-grid";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { deleteProjectExpense, updateProjectExpense } from "@/app/api/projectExpenses/actions";
import dayjs from "dayjs";
import React from "react";
import { NumberFormatValues, NumericFormat } from "react-number-format";

interface CustomMoneyComponentProps {
params: GridRenderEditCellParams;
}

interface Props {
expenses: ProjectExpensesResultFormatted[]
@@ -86,6 +93,8 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => {
[]
);



const columns = useMemo<Column<ProjectExpensesResultFormatted>[]>(
() => [
{
@@ -215,6 +224,47 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => {
[validateRow],
);

// Money format : 000,000,000.00
const CustomMoneyFormat = useCallback((value: number) => {
if (value) {
return moneyFormatter.format(value);
} else {
return ""
}
}, [])

const CustomMoneyComponent: React.FC<CustomMoneyComponentProps> = ({ params }) => {
const { id, value, field } = params;
const ref = React.useRef();

const handleValueChange = (newValue: NumberFormatValues) => {
apiRef.current.setEditCellValue({ id, field, value: newValue.value });
};

return <NumericFormat
fullWidth
prefix="HK$"
onValueChange={(values) => {
console.log(values)
handleValueChange(values)
}}
customInput={TextField}
thousandSeparator
valueIsNumericString
decimalScale={2}
fixedDecimalScale
value={value}
inputRef={ref}
InputProps={{
sx: {
'& .MuiInputBase-input': {
textAlign: 'right',
mb: 2
}
}
}}
/>;
}
const editColumn = useMemo<GridColDef[]>(
() => [
{
@@ -234,7 +284,13 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => {
headerName: t("Amount (HKD)"),
editable: true,
flex: 0.5,
type: 'number'
type: 'number',
renderEditCell: (params: GridRenderEditCellParams) => {
return <CustomMoneyComponent params={params} />
},
valueFormatter: (params: GridValueFormatterParams) => {
return CustomMoneyFormat(params.value as number)
}
},
{ field: "issuedDate",
headerName: t("Issue Date"),


+ 61
- 4
src/components/InvoiceSearch/InvoiceSearch.tsx Dosyayı Görüntüle

@@ -13,7 +13,7 @@ import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoic
import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts";
import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { GridCellParams, GridColDef, GridEventListener, GridRenderEditCellParams, GridRowId, GridRowModes, GridRowModesModel, GridValueFormatterParams } from "@mui/x-data-grid";
import { useGridApiRef } from "@mui/x-data-grid";
import StyledDataGrid from "../StyledDataGrid";

@@ -22,8 +22,11 @@ import CreateInvoiceModal from "./CreateInvoiceModal";
import { ProjectResult } from "@/app/api/projects";
import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware";
import InvoiceSearchLoading from "./InvoiceSearchLoading";
import { NumberFormatValues, NumericFormat } from "react-number-format";


interface CustomMoneyComponentProps {
params: GridRenderEditCellParams;
}

interface Props {
invoices: invoiceList[];
@@ -287,6 +290,48 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab
// setSelectedRow([]);
};

// Money format : 000,000,000.00
const CustomMoneyFormat = useCallback((value: number) => {
if (value) {
return moneyFormatter.format(value);
} else {
return ""
}
}, [])

const CustomMoneyComponent: React.FC<CustomMoneyComponentProps> = ({ params }) => {
const { id, value, field } = params;
const ref = React.useRef();

const handleValueChange = (newValue: NumberFormatValues) => {
apiRef.current.setEditCellValue({ id, field, value: newValue.value });
};

return <NumericFormat
fullWidth
prefix="HK$"
onValueChange={(values) => {
console.log(values)
handleValueChange(values)
}}
customInput={TextField}
thousandSeparator
valueIsNumericString
decimalScale={2}
fixedDecimalScale
value={value}
inputRef={ref}
InputProps={{
sx: {
'& .MuiInputBase-input': {
textAlign: 'right',
mb: 2
}
}
}}
/>;
}

const combinedColumns = useMemo<Column<invoiceList>[]>(
() => [
{
@@ -329,7 +374,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab
headerName: t("Amount (HKD)"),
editable: true,
flex: 0.5,
type: 'number'
type: 'number',
renderEditCell: (params: GridRenderEditCellParams) => {
return <CustomMoneyComponent params={params} />
},
valueFormatter: (params: GridValueFormatterParams) => {
return CustomMoneyFormat(params.value as number)
}
},
{
field: "receiptDate",
@@ -351,7 +402,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab
headerName: t("Actual Received Amount (HKD)"),
editable: true,
flex: 0.5,
type: 'number'
type: 'number',
renderEditCell: (params: GridRenderEditCellParams) => {
return <CustomMoneyComponent params={params} />
},
valueFormatter: (params: GridValueFormatterParams) => {
return CustomMoneyFormat(params.value as number)
}
},
],
[t]


+ 8
- 3
src/components/LeaveModal/LeaveCalendar.tsx Dosyayı Görüntüle

@@ -36,6 +36,7 @@ export interface Props {
timesheetRecords: RecordTimesheetInput;
isFullTime: boolean;
joinDate: Dayjs;
isSaturdayWorker: boolean
}

interface EventClickArg {
@@ -57,6 +58,7 @@ const LeaveCalendar: React.FC<Props> = ({
leaveRecords,
isFullTime,
joinDate,
isSaturdayWorker
}) => {
const {
t,
@@ -190,7 +192,8 @@ const LeaveCalendar: React.FC<Props> = ({
({ event }: EventClickArg) => {
const dayJsObj = dayjs(event.startStr);
const holiday = getHolidayForDate(event.startStr, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0;
const isHoliday = holiday || isWeekend;

if (
event.extendedProps.calendar === "leaveEntry" &&
@@ -210,7 +213,8 @@ const LeaveCalendar: React.FC<Props> = ({
(e: { dateStr: string; dayEl: HTMLElement }) => {
const dayJsObj = dayjs(e.dateStr);
const holiday = getHolidayForDate(e.dateStr, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0;
const isHoliday = holiday || isWeekend;

openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday));
},
@@ -224,7 +228,8 @@ const LeaveCalendar: React.FC<Props> = ({
}
const dayJsObj = dayjs(date);
const holiday = getHolidayForDate(date, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
const isWeekend = !isSaturdayWorker ? dayJsObj.day() === 0 || dayJsObj.day() === 6 : dayJsObj.day() === 0;
const isHoliday = holiday || isWeekend;

const leaves = localLeaveRecords[date] || [];
const timesheets = timesheetRecords[date] || [];


+ 3
- 0
src/components/LeaveModal/LeaveModal.tsx Dosyayı Görüntüle

@@ -25,6 +25,7 @@ const modalSx: SxProps = {
interface Props extends LeaveCalendarProps {
open: boolean;
onClose: () => void;
isSaturdayWorker: boolean
}

const LeaveModal: React.FC<Props> = ({
@@ -37,6 +38,7 @@ const LeaveModal: React.FC<Props> = ({
timesheetRecords,
isFullTime,
joinDate,
isSaturdayWorker
}) => {
const { t } = useTranslation("home");
const isMobile = useIsMobile();
@@ -51,6 +53,7 @@ const LeaveModal: React.FC<Props> = ({
allProjects={allProjects}
leaveRecords={leaveRecords}
timesheetRecords={timesheetRecords}
isSaturdayWorker={isSaturdayWorker}
/>
);



+ 54
- 2
src/components/ProjectSearch/ProjectSearch.tsx Dosyayı Görüntüle

@@ -7,13 +7,14 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from "lodash/uniq";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { MAINTAIN_PROJECT } from "@/middleware";
import { reverse, uniqBy } from "lodash";
import { loadDrafts } from "@/app/utils/draftUtils";
import { TeamResult } from "@/app/api/team";
import { Customer } from "@/app/api/customer";
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';

type ProjectResultOrDraft = ProjectResult & { isDraft?: boolean };

@@ -141,6 +142,32 @@ const ProjectSearch: React.FC<Props> = ({
[router],
);

const onProjectStatusClick = useCallback(
(project: ProjectResultOrDraft) => {
const status = project.status.toLocaleLowerCase()
console.log(status)

if (status && statusList.includes(status)) {
/* switch (status) {
case "pending to start":
router.push(`/projects/edit?id=${project.id}&autoClick=start`);
break;
case "on-going":
router.push(`/projects/edit?id=${project.id}&autoClick=complete`);
break;
case "completed":
router.push(`/projects/edit?id=${project.id}&autoClick=reopen`);
break;
} */
router.push(`/projects/edit?id=${project.id}&autoClick=true`);
}
},
[router],
);

const statusList = ["pending to start", "on-going","completed"]
const ignoreStatusList = ["draft", "deleted"]

const columns = useMemo<Column<ProjectResult>[]>(
() => [
{
@@ -165,7 +192,32 @@ const ProjectSearch: React.FC<Props> = ({
{ name: "category", label: t("Project Category") },
{ name: "team", label: t("Team") },
{ name: "client", label: t("Client") },
{ name: "status", label: t("Status") },
{
name: "status",
label: t("Status"),
// type: "link",
// onClick: onProjectStatusClick,
// underlines: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "none"}), {}),
// colors: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "inherit"}), {}),
},
// {
// name: "status",
// label: t("Status"),
// type: "button",
// onClick: onProjectStatusClick,
// variants: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "text"}), {}),
// colors: ignoreStatusList.reduce((acc, cur) => ({...acc, [cur]: "inherit"}), {}),
// }
{
name: "status",
label: t(""),
onClick: onProjectStatusClick,
buttonIcon: <PlayCircleOutlineIcon />,
disabled: !abilities.includes(MAINTAIN_PROJECT),
disabledRows: {
status: ignoreStatusList
}
},
],
[t, onProjectClick],
);


+ 77
- 13
src/components/SearchResults/SearchResults.tsx Dosyayı Görüntüle

@@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
import { convertDateArrayToString, moneyFormatter } from "@/app/utils/formatUtil";
import DoneIcon from '@mui/icons-material/Done';
import CloseIcon from '@mui/icons-material/Close';
import { Button, Link, LinkOwnProps } from "@mui/material";

export interface ResultWithId {
id: string | number;
@@ -25,7 +26,6 @@ export interface ResultWithId {
interface BaseColumn<T extends ResultWithId> {
name: keyof T;
label: string;
color?: IconButtonOwnProps["color"];
needTranslation?: boolean;
type?: string;
isHidden?: boolean;
@@ -34,13 +34,25 @@ interface BaseColumn<T extends ResultWithId> {
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
color?: IconButtonOwnProps["color"];
disabled?: boolean;
disabledRows?: { [columnName in keyof T]: string[] }; // Filter the row which is going to be disabled
disabledRows?: { [columnValue in keyof T]: string[] }; // Filter the row which is going to be disabled
}

interface LinkColumn<T extends ResultWithId> extends BaseColumn<T> {
// href: string;
onClick: (item: T) => void;
underline: LinkOwnProps["underline"];
underlines: { [columnValue in keyof T]: LinkOwnProps["underline"] };
color: LinkOwnProps["color"];
colors: { [columnValue in keyof T]: LinkOwnProps["color"] };
}

export type Column<T extends ResultWithId> =
| BaseColumn<T>
| ColumnWithAction<T>;
| ColumnWithAction<T>
| LinkColumn<T>
;

interface Props<T extends ResultWithId> {
items: T[];
@@ -55,6 +67,12 @@ function isActionColumn<T extends ResultWithId>(
return Boolean((column as ColumnWithAction<T>).onClick);
}

function isLinkColumn<T extends ResultWithId>(
column: Column<T>,
): column is LinkColumn<T> {
return column.type === "link";
}

function SearchResults<T extends ResultWithId>({
items,
columns,
@@ -101,6 +119,43 @@ function SearchResults<T extends ResultWithId>({
return false;
};

function convertObjectKeysToLowercase<T extends object>(obj: T): object | undefined {
return obj ? Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])
) : undefined;
}

// Link Component Functions
function handleLinkColors<T extends ResultWithId>(
column: LinkColumn<T>,
value: T[keyof T],
): LinkOwnProps["color"] {
const colors = convertObjectKeysToLowercase(column.colors);
console.log(colors)
const valueKey = String(value).toLowerCase() as keyof typeof colors;

if (colors && valueKey in colors) {
return colors[valueKey];
}

return column.color ?? "primary";
};

function handleLinkUnderlines<T extends ResultWithId>(
column: LinkColumn<T>,
value: T[keyof T],
): LinkOwnProps["underline"] {
const underlines = convertObjectKeysToLowercase(column.underlines);
console.log(underlines)
const valueKey = String(value).toLowerCase() as keyof typeof underlines;

if (underlines && valueKey in underlines) {
return underlines[valueKey];
}

return column.underline ?? "always";
};

const table = (
<>
<TableContainer sx={{ maxHeight: 440 }}>
@@ -125,15 +180,15 @@ function SearchResults<T extends ResultWithId>({

return (
<TableCell key={`${columnName.toString()}-${idx}`}>
{isActionColumn(column) ? (
<IconButton
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))}
>
{column.buttonIcon}
</IconButton>
) :
{isActionColumn(column) && !column?.type ? (
<IconButton
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
disabled={Boolean(column.disabled) || Boolean(disabledRows(column, item))}
>
{column.buttonIcon}
</IconButton>
) :
column?.type === "date" ? (
<>{convertDateArrayToString(item[columnName] as number[])}</>
) :
@@ -143,9 +198,18 @@ function SearchResults<T extends ResultWithId>({
column?.type === "checkbox" ? (
Boolean(item[columnName]) ?
<DoneIcon color="primary" /> : <CloseIcon color="error"/>
) :
isLinkColumn(column) ? (
<Link
onClick={() => column.onClick(item)}
underline={handleLinkUnderlines(column, item[columnName])}
color={handleLinkColors(column, item[columnName])}
>
{item[columnName] as string}
</Link>
) :
(
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName]}</>
<>{column?.needTranslation ? t(item[columnName] as string) : item[columnName] as string}</>
)}
</TableCell>
);


+ 3
- 0
src/components/TimeLeaveModal/TimeLeaveModal.tsx Dosyayı Görüntüle

@@ -54,6 +54,7 @@ interface Props {
isFullTime: boolean;
joinDate: Dayjs;
miscTasks: Task[];
isSaturdayWorker: boolean
}

const modalSx: SxProps = {
@@ -81,6 +82,7 @@ const TimeLeaveModal: React.FC<Props> = ({
isFullTime,
joinDate,
miscTasks,
isSaturdayWorker,
}) => {
const { t } = useTranslation("home");

@@ -227,6 +229,7 @@ const TimeLeaveModal: React.FC<Props> = ({
leaveTypes,
miscTasks,
}}
isSaturdayWorker={isSaturdayWorker}
/>
</Box>
{errorComponent}


+ 1
- 1
src/components/TimesheetAmendment/TimesheetAmendment.tsx Dosyayı Görüntüle

@@ -512,4 +512,4 @@ const TimesheetAmendment: React.FC<Props> = ({
);
};

export default TimesheetAmendment;
export default TimesheetAmendment;

+ 1
- 1
src/components/TimesheetAmendment/TimesheetAmendmentModal.tsx Dosyayı Görüntüle

@@ -82,4 +82,4 @@ export const TimesheetAmendmentModal: React.FC<Props> = ({
</Card>
</Modal>
);
};
};

+ 1
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx Dosyayı Görüntüle

@@ -248,4 +248,4 @@ const UserWorkspacePage: React.FC<Props> = ({
);
};

export default UserWorkspacePage;
export default UserWorkspacePage;

+ 1
- 1
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx Dosyayı Görüntüle

@@ -89,4 +89,4 @@ const UserWorkspaceWrapper: React.FC = async () => {
);
};

export default UserWorkspaceWrapper;
export default UserWorkspaceWrapper;

+ 0
- 8
src/config/authConfig.ts Dosyayı Görüntüle

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


+ 1
- 0
src/i18n/en/common.json Dosyayı Görüntüle

@@ -21,6 +21,7 @@
"Submit Fail": "Submit Fail",
"Do you want to delete?": "Do you want to delete",
"Delete Success": "Delete Success",
"Save Success": "Save Success",
"Details": "Details",
"Delete": "Delete",


+ 2
- 1
src/i18n/zh/common.json Dosyayı Görüntüle

@@ -16,7 +16,8 @@
"Submit Fail": "提交失敗",
"Do you want to delete?": "你是否確認要刪除?",
"Delete Success": "刪除成功",
"Save Success": "儲存成功",

"Date": "日期",
"Month": "月份",



+ 4
- 2
src/middleware.ts Dosyayı Görüntüle

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


Yükleniyor…
İptal
Kaydet