浏览代码

Remove unused code

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1年前
父节点
当前提交
3d551315ca
共有 10 个文件被更改,包括 4 次插入1625 次删除
  1. +3
    -85
      src/app/api/timesheets/utils.ts
  2. +0
    -109
      src/components/EnterLeave/EnterLeaveModal.tsx
  3. +0
    -548
      src/components/EnterLeave/LeaveInputGrid.tsx
  4. +0
    -1
      src/components/EnterLeave/index.ts
  5. +0
    -109
      src/components/EnterTimesheet/EnterTimesheetModal.tsx
  6. +0
    -548
      src/components/EnterTimesheet/TimesheetInputGrid.tsx
  7. +0
    -1
      src/components/EnterTimesheet/index.ts
  8. +0
    -223
      src/components/TimesheetModal/TimesheetModal.tsx
  9. +0
    -1
      src/components/TimesheetModal/index.ts
  10. +1
    -0
      src/components/TimesheetTable/FastTimeEntryModal.tsx

+ 3
- 85
src/app/api/timesheets/utils.ts 查看文件

@@ -1,12 +1,6 @@
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils";
import { HolidaysResult } from "../holidays";
import {
LeaveEntry,
RecordLeaveInput,
RecordTimeLeaveInput,
RecordTimesheetInput,
TimeEntry,
} from "./actions";
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions";
import { convertDateArrayToString } from "@/app/utils/formatUtil";
import compact from "lodash/compact";

@@ -83,82 +77,6 @@ export const validateLeaveEntry = (
return Object.keys(error).length > 0 ? error : undefined;
};

export const validateTimesheet = (
timesheet: RecordTimesheetInput,
leaveRecords: RecordLeaveInput,
companyHolidays: HolidaysResult[],
): { [date: string]: string } | undefined => {
const errors: { [date: string]: string } = {};

const holidays = new Set(
compact([
...getPublicHolidaysForNYears(2).map((h) => h.date),
...companyHolidays.map((h) => convertDateArrayToString(h.date)),
]),
);

Object.keys(timesheet).forEach((date) => {
const timeEntries = timesheet[date];

// Check each entry
for (const entry of timeEntries) {
const entryErrors = validateTimeEntry(entry, holidays.has(date));

if (entryErrors) {
errors[date] = "There are errors in the entries";
return;
}
}

// Check total hours
const leaves = leaveRecords[date] || [];
const totalHourError = checkTotalHours(timeEntries, leaves);
if (totalHourError) {
errors[date] = totalHourError;
}
});

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

export const validateLeaveRecord = (
leaveRecords: RecordLeaveInput,
timesheet: RecordTimesheetInput,
companyHolidays: HolidaysResult[],
): { [date: string]: string } | undefined => {
const errors: { [date: string]: string } = {};

const holidays = new Set(
compact([
...getPublicHolidaysForNYears(2).map((h) => h.date),
...companyHolidays.map((h) => convertDateArrayToString(h.date)),
]),
);

Object.keys(leaveRecords).forEach((date) => {
const leaves = leaveRecords[date];

// Check each leave entry
for (const entry of leaves) {
const entryError = validateLeaveEntry(entry, holidays.has(date));
if (entryError) {
errors[date] = "There are errors in the entries";
return;
}
}

// Check total hours
const timeEntries = timesheet[date] || [];

const totalHourError = checkTotalHours(timeEntries, leaves);
if (totalHourError) {
errors[date] = totalHourError;
}
});

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

export const validateTimeLeaveRecord = (
records: RecordTimeLeaveInput,
companyHolidays: HolidaysResult[],
@@ -191,8 +109,8 @@ export const validateTimeLeaveRecord = (

// Check total hours
const totalHourError = checkTotalHours(
entries.filter((e) => e.type === "timeEntry"),
entries.filter((e) => e.type === "leaveEntry"),
entries.filter((e) => e.type === "timeEntry") as TimeEntry[],
entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[],
);

if (totalHourError) {


+ 0
- 109
src/components/EnterLeave/EnterLeaveModal.tsx 查看文件

@@ -1,109 +0,0 @@
"use client";

import { useState } from "react";
import Button from "@mui/material/Button";
import { Card, Modal } from "@mui/material";
import TimesheetInputGrid from "./LeaveInputGrid";

// import { fetchLeaves } from "@/app/api/leave";

interface EnterTimesheetModalProps {
isOpen: boolean;
onClose: () => void;
modalStyle?: any;
}

const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({
...props
}) => {
const [lockConfirm, setLockConfirm] = useState(false);
const columns = [
{
id: "projectCode",
field: "projectCode",
headerName: "Project Code and Name",
flex: 1,
},
{
id: "task",
field: "task",
headerName: "Task",
flex: 1,
},
];

const rows = [
{
id: 1,
projectCode: "M1001",
task: "1.2",
},
{
id: 2,
projectCode: "M1301",
task: "1.1",
},
];

const fetchTimesheet = async () => {
// fetchLeaves();
// const res = await fetch(`http://localhost:8090/api/timesheets`, {
// // const res = await fetch(`${BASE_API_URL}/timesheets`, {
// method: "GET",
// mode: 'no-cors',
// });

// console.log(res.json);
};

return (
<Modal open={props.isOpen} onClose={props.onClose}>
<div>
{/* <Typography variant="h5" id="modal-title" sx={{flex:1}}>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
Record Leave
</div>
</Typography> */}

<Card style={{
flex: 10,
marginBottom: "20px",
width: "90%",
// height: "80%",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<TimesheetInputGrid setLockConfirm={setLockConfirm}/>
<div
style={{
display: "flex",
justifyContent: "space-between",
width: "100%",
flex: 1,
padding: "20px",
}}
>
<Button
disabled={lockConfirm}
variant="contained"
onClick={props.onClose}
>
Confirm
</Button>
<Button
variant="contained"
onClick={props.onClose}
sx={{ "background-color": "#F890A5" }}
>
Cancel
</Button>
</div>
</Card>
</div>
</Modal>
);
};

export default EnterTimesheetModal;

+ 0
- 548
src/components/EnterLeave/LeaveInputGrid.tsx 查看文件

@@ -1,548 +0,0 @@
"use client";
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import PageTitle from "../PageTitle/PageTitle";
import { Suspense } from "react";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Link from "next/link";
import { t } from "i18next";
import {
Box,
Container,
Modal,
Select,
SelectChangeEvent,
Typography,
} from "@mui/material";
import { Close } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Close";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Swal from "sweetalert2";
import { msg } from "../Swal/CustomAlerts";
import React from "react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import {
GridRowsProp,
GridRowModesModel,
GridRowModes,
DataGrid,
GridColDef,
GridToolbarContainer,
GridFooterContainer,
GridActionsCellItem,
GridEventListener,
GridRowId,
GridRowModel,
GridRowEditStopReasons,
GridEditInputCell,
GridValueSetterParams,
} from "@mui/x-data-grid";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import { Props } from "react-intl/src/components/relative";

const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];

interface BottomBarProps {
getHoursTotal: (column: string) => number;
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

interface EditToolbarProps {
// setDay: (newDay : dayjs.Dayjs) => void;
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

interface EditFooterProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

const EditToolbar = (props: EditToolbarProps) => {
const { setDay } = props;
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs());

const handleClickLeft = () => {
if (selectedDate) {
const newDate = selectedDate.add(-7, "day");
setSelectedDate(newDate);
}
};
const handleClickRight = () => {
if (selectedDate) {
const newDate =
selectedDate.add(7, "day") > dayjs()
? dayjs()
: selectedDate.add(7, "day");
setSelectedDate(newDate);
}
};

const handleDateChange = (date: dayjs.Dayjs | Date | null) => {
const newDate = dayjs(date);
setSelectedDate(newDate);
};

useEffect(() => {
setDay((oldDay) => selectedDate);
}, [selectedDate]);

return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<div
style={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
paddingBottom: "20px",
}}
>
<Typography variant="h5" id="modal-title" sx={{ flex: 1 }}>
Record Leave
</Typography>
<Button
sx={{ "border-radius": "30%", marginRight: "20px" }}
variant="contained"
onClick={handleClickLeft}
>
<ArrowBackIcon />
</Button>
<DatePicker
value={selectedDate}
onChange={handleDateChange}
disableFuture={true}
/>
<Button
sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }}
variant="contained"
onClick={handleClickRight}
>
<ArrowForwardIcon />
</Button>
</div>
</LocalizationProvider>
);
};

const BottomBar = (props: BottomBarProps) => {
const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props;
// const getHoursTotal = props.getHoursTotal;
const [newId, setNewId] = useState(-1);
const [invalidDays, setInvalidDays] = useState(0);

const handleAddClick = () => {
const id = newId;
setNewId(newId - 1);
setRows((oldRows) => [
...oldRows,
{ id, projectCode: "", task: "", isNew: true },
]);
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" },
}));
};

const totalColDef = {
flex: 1,
// style: {color:getHoursTotal('mon')>24?"red":"black"}
};

const TotalCell = ({ value }: Props) => {
const [invalid, setInvalid] = useState(false);

useEffect(() => {
const newInvalid = (value ?? 0) > 24;
setInvalid(newInvalid);
}, [value]);

return (
<Box flex={1} style={{ color: invalid ? "red" : "black" }}>
{value}
</Box>
);
};

const checkUnlockConfirmBtn = () => {
// setLockConfirm((oldLock)=> valid);
setLockConfirm((oldLock) =>
weekdays.every((weekday) => {
getHoursTotal(weekday) <= 24;
}),
);
};

return (
<div>
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
<Box flex={5.7} textAlign={"right"} marginRight="4rem">
<b>Total:</b>
</Box>
<TotalCell value={getHoursTotal("mon")} />
<TotalCell value={getHoursTotal("tue")} />
<TotalCell value={getHoursTotal("wed")} />
<TotalCell value={getHoursTotal("thu")} />
<TotalCell value={getHoursTotal("fri")} />
<TotalCell value={getHoursTotal("sat")} />
<TotalCell value={getHoursTotal("sun")} />
</div>
<Button
variant="outlined"
color="primary"
startIcon={<AddIcon />}
onClick={handleAddClick}
>
Add record
</Button>
</div>
);
};

const EditFooter = (props: EditFooterProps) => {
return (
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
<Box flex={1}>
<b>Total: </b>
</Box>
<Box flex={2}>ssss</Box>
</div>
);
};

interface TimesheetInputGridProps {
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void;
onClose?: () => void;
}

const initialRows: GridRowsProp = [
{
id: 1,
projectCode: "M1001",
task: "1.2",
mon: 2.5,
},
{
id: 2,
projectCode: "M1002",
task: "1.3",
mon: 3.25,
},
];

const options = ["M1001", "M1301", "M1354", "M1973"];
const options2 = [
"1.1 - Preparation of preliminary Cost Estimate / Cost Plan",
"1.2 - Cash flow forecast",
"1.3 - Cost studies fo alterative design solutions",
"1.4 = Attend design co-ordination / project review meetings",
"1.5 - Prepare / Review RIC",
];

const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => {
if (date.day() == 0) {
return date.add(weekday - date.day() - 7, "day").format("DD MMM");
} else {
return date.add(weekday - date.day(), "day").format("DD MMM");
}
};

const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({
...props
}) => {
const [rows, setRows] = useState(initialRows);
const [day, setDay] = useState(dayjs());
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
{},
);
const { setLockConfirm } = props;

const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
}
};

const handleEditClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
};

const handleSaveClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
};

const handleDeleteClick = (id: GridRowId) => () => {
setRows(rows.filter((row) => row.id !== id));
};

const handleCancelClick = (id: GridRowId) => () => {
setRowModesModel({
...rowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
});

const editedRow = rows.find((row) => row.id === id);
if (editedRow!.isNew) {
setRows(rows.filter((row) => row.id !== id));
}
};

const processRowUpdate = (newRow: GridRowModel) => {
const updatedRow = { ...newRow, isNew: false };
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
return updatedRow;
};

const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
setRowModesModel(newRowModesModel);
};

const getHoursTotal = (column: any) => {
let sum = 0;
rows.forEach((row) => {
sum += row[column] ?? 0;
});
return sum;
};

const weekdayColConfig: any = {
type: "number",
// sortable: false,
//width: 100,
flex: 1,
align: "left",
headerAlign: "left",
editable: true,
renderEditCell: (value: any) => (
<GridEditInputCell
{...value}
inputProps={{
max: 24,
min: 0,
step: 0.25,
}}
/>
),
};

const columns: GridColDef[] = [
{
field: "actions",
type: "actions",
headerName: "Actions",
width: 100,
cellClassName: "actions",
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;

if (isInEditMode) {
return [
<GridActionsCellItem
key={`actions-${id}-save`}
icon={<SaveIcon />}
title="Save"
label="Save"
sx={{
color: "primary.main",
}}
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
key={`actions-${id}-cancel`}
icon={<CancelIcon />}
title="Cancel"
label="Cancel"
className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
/>,
];
}

return [
<GridActionsCellItem
key={`actions-${id}-edit`}
icon={<EditIcon />}
title="Edit"
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
/>,
<GridActionsCellItem
key={`actions-${id}-delete`}
title="Delete"
label="Delete"
icon={<DeleteIcon />}
onClick={handleDeleteClick(id)}
sx={{ color: "red" }}
/>,
];
},
},
{
field: "projectCode",
headerName: "Project Code",
// width: 220,
flex: 2,
editable: true,
type: "singleSelect",
valueOptions: options,
},
{
field: "task",
headerName: "Task",
// width: 220,
flex: 3,
editable: true,
type: "singleSelect",
valueOptions: options2,
},
{
// Mon
field: "mon",
...weekdayColConfig,
renderHeader: () => {
return <div>Mon - {getDateForHeader(day, 1)}</div>;
},
},
{
// Tue
field: "tue",
...weekdayColConfig,
renderHeader: () => {
return <div>Tue - {getDateForHeader(day, 2)}</div>;
},
},
{
// Wed
field: "wed",
...weekdayColConfig,
renderHeader: () => {
return <div>Wed - {getDateForHeader(day, 3)}</div>;
},
},
{
// Thu
field: "thu",
...weekdayColConfig,
renderHeader: () => {
return <div>Thu - {getDateForHeader(day, 4)}</div>;
},
},
{
// Fri
field: "fri",
...weekdayColConfig,
renderHeader: () => {
return <div>Fri - {getDateForHeader(day, 5)}</div>;
},
},
{
// Sat
field: "sat",
...weekdayColConfig,
renderHeader: () => {
return <div>Sat - {getDateForHeader(day, 6)}</div>;
},
},
{
// Sun
field: "sun",
...weekdayColConfig,
renderHeader: () => {
return (
<div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div>
);
},
},
// {
// field: 'joinDate',
// headerName: 'Join date',
// type: 'date',
// width: 180,
// editable: true,
// },
];

return (
<Box
sx={{
// marginBottom: '-5px',
display: "flex",
"flex-direction": "column",
// 'justify-content': 'flex-end',
padding: "20px",
height: "100%", //'30rem',
width: "100%",
"& .actions": {
color: "text.secondary",
},
"& .header": {
// border: 1,
// 'border-width': '1px',
// 'border-color': 'grey',
},
"& .textPrimary": {
color: "text.primary",
},
}}
>
<DataGrid
rows={rows}
columns={columns}
editMode="row"
rowModesModel={rowModesModel}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
disableRowSelectionOnClick={true}
disableColumnMenu={true}
hideFooterPagination={true}
slots={{
toolbar: EditToolbar,
// footer: EditFooter,
}}
slotProps={{
toolbar: { setDay, setRows, setRowModesModel },
// footer: { setDay, setRows, setRowModesModel },
}}
initialState={{
pagination: { paginationModel: { pageSize: 100 } },
}}
sx={{ flex: 1 }}
/>

<BottomBar
getHoursTotal={getHoursTotal}
setRows={setRows}
setRowModesModel={setRowModesModel}
setLockConfirm={setLockConfirm}
// sx={{flex:3}}
/>
</Box>
);
};

export default TimesheetInputGrid;

+ 0
- 1
src/components/EnterLeave/index.ts 查看文件

@@ -1 +0,0 @@
export { default } from "./EnterLeaveModal";

+ 0
- 109
src/components/EnterTimesheet/EnterTimesheetModal.tsx 查看文件

@@ -1,109 +0,0 @@
"use client";

import { useState } from "react";
import Button from "@mui/material/Button";
import { Card, Modal } from "@mui/material";
import TimesheetInputGrid from "./TimesheetInputGrid";

// import { fetchTimesheets } from "@/app/api/timesheets";

interface EnterTimesheetModalProps {
isOpen: boolean;
onClose: () => void;
modalStyle?: any;
}

const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({
...props
}) => {
const [lockConfirm, setLockConfirm] = useState(false);
const columns = [
{
id: "projectCode",
field: "projectCode",
headerName: "Project Code and Name",
flex: 1,
},
{
id: "task",
field: "task",
headerName: "Task",
flex: 1,
},
];

const rows = [
{
id: 1,
projectCode: "M1001",
task: "1.2",
},
{
id: 2,
projectCode: "M1301",
task: "1.1",
},
];

const fetchTimesheet = async () => {
// fetchTimesheets();
// const res = await fetch(`http://localhost:8090/api/timesheets`, {
// // const res = await fetch(`${BASE_API_URL}/timesheets`, {
// method: "GET",
// mode: 'no-cors',
// });

// console.log(res.json);
};

return (
<Modal open={props.isOpen} onClose={props.onClose}>
<div>
{/* <Typography variant="h5" id="modal-title" sx={{flex:1}}>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
Timesheet Input
</div>
</Typography> */}

<Card style={{
flex: 10,
marginBottom: "20px",
width: "90%",
// height: "80%",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}>
<TimesheetInputGrid setLockConfirm={setLockConfirm}/>
<div
style={{
display: "flex",
justifyContent: "space-between",
width: "100%",
flex: 1,
padding: "20px",
}}
>
<Button
disabled={lockConfirm}
variant="contained"
onClick={props.onClose}
>
Confirm
</Button>
<Button
variant="contained"
onClick={props.onClose}
sx={{ "background-color": "#F890A5" }}
>
Cancel
</Button>
</div>
</Card>
</div>
</Modal>
);
};

export default EnterTimesheetModal;

+ 0
- 548
src/components/EnterTimesheet/TimesheetInputGrid.tsx 查看文件

@@ -1,548 +0,0 @@
"use client";
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import PageTitle from "../PageTitle/PageTitle";
import { Suspense } from "react";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Link from "next/link";
import { t } from "i18next";
import {
Box,
Container,
Modal,
Select,
SelectChangeEvent,
Typography,
} from "@mui/material";
import { Close } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Close";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Swal from "sweetalert2";
import { msg } from "../Swal/CustomAlerts";
import React from "react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import {
GridRowsProp,
GridRowModesModel,
GridRowModes,
DataGrid,
GridColDef,
GridToolbarContainer,
GridFooterContainer,
GridActionsCellItem,
GridEventListener,
GridRowId,
GridRowModel,
GridRowEditStopReasons,
GridEditInputCell,
GridValueSetterParams,
} from "@mui/x-data-grid";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
import { Props } from "react-intl/src/components/relative";

const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];

interface BottomBarProps {
getHoursTotal: (column: string) => number;
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

interface EditToolbarProps {
// setDay: (newDay : dayjs.Dayjs) => void;
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void;
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

interface EditFooterProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
setRowModesModel: (
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
}

const EditToolbar = (props: EditToolbarProps) => {
const { setDay } = props;
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs());

const handleClickLeft = () => {
if (selectedDate) {
const newDate = selectedDate.add(-7, "day");
setSelectedDate(newDate);
}
};
const handleClickRight = () => {
if (selectedDate) {
const newDate =
selectedDate.add(7, "day") > dayjs()
? dayjs()
: selectedDate.add(7, "day");
setSelectedDate(newDate);
}
};

const handleDateChange = (date: dayjs.Dayjs | Date | null) => {
const newDate = dayjs(date);
setSelectedDate(newDate);
};

useEffect(() => {
setDay((oldDay) => selectedDate);
}, [selectedDate]);

return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<div
style={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
paddingBottom: "20px",
}}
>
<Typography variant="h5" id="modal-title" sx={{ flex: 1 }}>
Timesheet Input
</Typography>
<Button
sx={{ "border-radius": "30%", marginRight: "20px" }}
variant="contained"
onClick={handleClickLeft}
>
<ArrowBackIcon />
</Button>
<DatePicker
value={selectedDate}
onChange={handleDateChange}
disableFuture={true}
/>
<Button
sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }}
variant="contained"
onClick={handleClickRight}
>
<ArrowForwardIcon />
</Button>
</div>
</LocalizationProvider>
);
};

const BottomBar = (props: BottomBarProps) => {
const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props;
// const getHoursTotal = props.getHoursTotal;
const [newId, setNewId] = useState(-1);
const [invalidDays, setInvalidDays] = useState(0);

const handleAddClick = () => {
const id = newId;
setNewId(newId - 1);
setRows((oldRows) => [
...oldRows,
{ id, projectCode: "", task: "", isNew: true },
]);
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" },
}));
};

const totalColDef = {
flex: 1,
// style: {color:getHoursTotal('mon')>24?"red":"black"}
};

const TotalCell = ({ value }: Props) => {
const [invalid, setInvalid] = useState(false);

useEffect(() => {
const newInvalid = (value ?? 0) > 24;
setInvalid(newInvalid);
}, [value]);

return (
<Box flex={1} style={{ color: invalid ? "red" : "black" }}>
{value}
</Box>
);
};

const checkUnlockConfirmBtn = () => {
// setLockConfirm((oldLock)=> valid);
setLockConfirm((oldLock) =>
weekdays.every((weekday) => {
getHoursTotal(weekday) <= 24;
}),
);
};

return (
<div>
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
<Box flex={5.7} textAlign={"right"} marginRight="4rem">
<b>Total:</b>
</Box>
<TotalCell value={getHoursTotal("mon")} />
<TotalCell value={getHoursTotal("tue")} />
<TotalCell value={getHoursTotal("wed")} />
<TotalCell value={getHoursTotal("thu")} />
<TotalCell value={getHoursTotal("fri")} />
<TotalCell value={getHoursTotal("sat")} />
<TotalCell value={getHoursTotal("sun")} />
</div>
<Button
variant="outlined"
color="primary"
startIcon={<AddIcon />}
onClick={handleAddClick}
>
Add record
</Button>
</div>
);
};

const EditFooter = (props: EditFooterProps) => {
return (
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}>
<Box flex={1}>
<b>Total: </b>
</Box>
<Box flex={2}>ssss</Box>
</div>
);
};

interface TimesheetInputGridProps {
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void;
onClose?: () => void;
}

const initialRows: GridRowsProp = [
{
id: 1,
projectCode: "M1001",
task: "1.2",
mon: 2.5,
},
{
id: 2,
projectCode: "M1002",
task: "1.3",
mon: 3.25,
},
];

const options = ["M1001", "M1301", "M1354", "M1973"];
const options2 = [
"1.1 - Preparation of preliminary Cost Estimate / Cost Plan",
"1.2 - Cash flow forecast",
"1.3 - Cost studies fo alterative design solutions",
"1.4 = Attend design co-ordination / project review meetings",
"1.5 - Prepare / Review RIC",
];

const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => {
if (date.day() == 0) {
return date.add(weekday - date.day() - 7, "day").format("DD MMM");
} else {
return date.add(weekday - date.day(), "day").format("DD MMM");
}
};

const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({
...props
}) => {
const [rows, setRows] = useState(initialRows);
const [day, setDay] = useState(dayjs());
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>(
{},
);
const { setLockConfirm } = props;

const handleRowEditStop: GridEventListener<"rowEditStop"> = (
params,
event,
) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
}
};

const handleEditClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
};

const handleSaveClick = (id: GridRowId) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
};

const handleDeleteClick = (id: GridRowId) => () => {
setRows(rows.filter((row) => row.id !== id));
};

const handleCancelClick = (id: GridRowId) => () => {
setRowModesModel({
...rowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
});

const editedRow = rows.find((row) => row.id === id);
if (editedRow!.isNew) {
setRows(rows.filter((row) => row.id !== id));
}
};

const processRowUpdate = (newRow: GridRowModel) => {
const updatedRow = { ...newRow, isNew: false };
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
return updatedRow;
};

const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
setRowModesModel(newRowModesModel);
};

const getHoursTotal = (column: any) => {
let sum = 0;
rows.forEach((row) => {
sum += row[column] ?? 0;
});
return sum;
};

const weekdayColConfig: any = {
type: "number",
// sortable: false,
//width: 100,
flex: 1,
align: "left",
headerAlign: "left",
editable: true,
renderEditCell: (value: any) => (
<GridEditInputCell
{...value}
inputProps={{
max: 24,
min: 0,
step: 0.25,
}}
/>
),
};

const columns: GridColDef[] = [
{
field: "actions",
type: "actions",
headerName: "Actions",
width: 100,
cellClassName: "actions",
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;

if (isInEditMode) {
return [
<GridActionsCellItem
key={`actions-${id}-save`}
icon={<SaveIcon />}
title="Save"
label="Save"
sx={{
color: "primary.main",
}}
onClick={handleSaveClick(id)}
/>,
<GridActionsCellItem
key={`actions-${id}-cancel`}
icon={<CancelIcon />}
title="Cancel"
label="Cancel"
className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
/>,
];
}

return [
<GridActionsCellItem
key={`actions-${id}-edit`}
icon={<EditIcon />}
title="Edit"
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
/>,
<GridActionsCellItem
key={`actions-${id}-delete`}
title="Delete"
label="Delete"
icon={<DeleteIcon />}
onClick={handleDeleteClick(id)}
sx={{ color: "red" }}
/>,
];
},
},
{
field: "projectCode",
headerName: "Project Code",
// width: 220,
flex: 2,
editable: true,
type: "singleSelect",
valueOptions: options,
},
{
field: "task",
headerName: "Task",
// width: 220,
flex: 3,
editable: true,
type: "singleSelect",
valueOptions: options2,
},
{
// Mon
field: "mon",
...weekdayColConfig,
renderHeader: () => {
return <div>Mon - {getDateForHeader(day, 1)}</div>;
},
},
{
// Tue
field: "tue",
...weekdayColConfig,
renderHeader: () => {
return <div>Tue - {getDateForHeader(day, 2)}</div>;
},
},
{
// Wed
field: "wed",
...weekdayColConfig,
renderHeader: () => {
return <div>Wed - {getDateForHeader(day, 3)}</div>;
},
},
{
// Thu
field: "thu",
...weekdayColConfig,
renderHeader: () => {
return <div>Thu - {getDateForHeader(day, 4)}</div>;
},
},
{
// Fri
field: "fri",
...weekdayColConfig,
renderHeader: () => {
return <div>Fri - {getDateForHeader(day, 5)}</div>;
},
},
{
// Sat
field: "sat",
...weekdayColConfig,
renderHeader: () => {
return <div>Sat - {getDateForHeader(day, 6)}</div>;
},
},
{
// Sun
field: "sun",
...weekdayColConfig,
renderHeader: () => {
return (
<div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div>
);
},
},
// {
// field: 'joinDate',
// headerName: 'Join date',
// type: 'date',
// width: 180,
// editable: true,
// },
];

return (
<Box
sx={{
// marginBottom: '-5px',
display: "flex",
"flex-direction": "column",
// 'justify-content': 'flex-end',
padding: "20px",
height: "100%", //'30rem',
width: "100%",
"& .actions": {
color: "text.secondary",
},
"& .header": {
// border: 1,
// 'border-width': '1px',
// 'border-color': 'grey',
},
"& .textPrimary": {
color: "text.primary",
},
}}
>
<DataGrid
rows={rows}
columns={columns}
editMode="row"
rowModesModel={rowModesModel}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
disableRowSelectionOnClick={true}
disableColumnMenu={true}
hideFooterPagination={true}
slots={{
toolbar: EditToolbar,
// footer: EditFooter,
}}
slotProps={{
toolbar: { setDay, setRows, setRowModesModel },
// footer: { setDay, setRows, setRowModesModel },
}}
initialState={{
pagination: { paginationModel: { pageSize: 100 } },
}}
sx={{ flex: 1 }}
/>

<BottomBar
getHoursTotal={getHoursTotal}
setRows={setRows}
setRowModesModel={setRowModesModel}
setLockConfirm={setLockConfirm}
// sx={{flex:3}}
/>
</Box>
);
};

export default TimesheetInputGrid;

+ 0
- 1
src/components/EnterTimesheet/index.ts 查看文件

@@ -1 +0,0 @@
export { default } from "./EnterTimesheetModal";

+ 0
- 223
src/components/TimesheetModal/TimesheetModal.tsx 查看文件

@@ -1,223 +0,0 @@
import React, { useCallback, useEffect, useMemo } from "react";
import {
Box,
Button,
Card,
CardActions,
CardContent,
Modal,
ModalProps,
SxProps,
Typography,
} from "@mui/material";
import TimesheetTable from "../TimesheetTable";
import { useTranslation } from "react-i18next";
import { Check, Close } from "@mui/icons-material";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import {
RecordLeaveInput,
RecordTimesheetInput,
saveTimesheet,
} from "@/app/api/timesheets/actions";
import dayjs from "dayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import FullscreenModal from "../FullscreenModal";
import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable";
import useIsMobile from "@/app/utils/useIsMobile";
import { HolidaysResult } from "@/app/api/holidays";
import {
DAILY_NORMAL_MAX_HOURS,
TIMESHEET_DAILY_MAX_HOURS,
validateTimesheet,
} from "@/app/api/timesheets/utils";
import ErrorAlert from "../ErrorAlert";

interface Props {
isOpen: boolean;
onClose: () => void;
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
defaultTimesheets?: RecordTimesheetInput;
leaveRecords: RecordLeaveInput;
companyHolidays: HolidaysResult[];
fastEntryEnabled?: boolean;
}

const modalSx: SxProps = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: { xs: "calc(100% - 2rem)", sm: "90%" },
maxHeight: "90%",
maxWidth: 1400,
};

const TimesheetModal: React.FC<Props> = ({
isOpen,
onClose,
allProjects,
assignedProjects,
defaultTimesheets,
leaveRecords,
companyHolidays,
fastEntryEnabled,
}) => {
const { t } = useTranslation("home");

const defaultValues = useMemo(() => {
const today = dayjs();
return Array(7)
.fill(undefined)
.reduce<RecordTimesheetInput>((acc, _, index) => {
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
return {
...acc,
[date]: defaultTimesheets?.[date] ?? [],
};
}, {});
}, [defaultTimesheets]);

const formProps = useForm<RecordTimesheetInput>({ defaultValues });
useEffect(() => {
formProps.reset(defaultValues);
}, [defaultValues, formProps]);

const onSubmit = useCallback<SubmitHandler<RecordTimesheetInput>>(
async (data) => {
const errors = validateTimesheet(data, leaveRecords, companyHolidays);
if (errors) {
Object.keys(errors).forEach((date) =>
formProps.setError(date, {
message: errors[date],
}),
);
return;
}
const savedRecords = await saveTimesheet(data);

const today = dayjs();
const newFormValues = Array(7)
.fill(undefined)
.reduce<RecordTimesheetInput>((acc, _, index) => {
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT);
return {
...acc,
[date]: savedRecords[date] ?? [],
};
}, {});

formProps.reset(newFormValues);
onClose();
},
[companyHolidays, formProps, leaveRecords, onClose],
);

const onCancel = useCallback(() => {
formProps.reset(defaultValues);
onClose();
}, [defaultValues, formProps, onClose]);

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

const errorComponent = (
<ErrorAlert
errors={Object.keys(formProps.formState.errors).map((date) => {
const error = formProps.formState.errors[date]?.message;
return error
? `${date}: ${t(error, {
TIMESHEET_DAILY_MAX_HOURS,
DAILY_NORMAL_MAX_HOURS,
})}`
: undefined;
})}
/>
);

const matches = useIsMobile();

return (
<FormProvider {...formProps}>
{!matches ? (
// Desktop version
<Modal open={isOpen} onClose={onModalClose}>
<Card sx={modalSx}>
<CardContent
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Timesheet Input")}
</Typography>
<Box
sx={{
marginInline: -3,
marginBlock: 4,
}}
>
<TimesheetTable
companyHolidays={companyHolidays}
assignedProjects={assignedProjects}
allProjects={allProjects}
leaveRecords={leaveRecords}
fastEntryEnabled={fastEntryEnabled}
/>
</Box>
{errorComponent}
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={onCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
</CardActions>
</CardContent>
</Card>
</Modal>
) : (
// Mobile version
<FullscreenModal
open={isOpen}
onClose={onModalClose}
closeModal={onCancel}
>
<Box
display="flex"
flexDirection="column"
gap={2}
height="100%"
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="h6" padding={2} flex="none">
{t("Timesheet Input")}
</Typography>
<MobileTimesheetTable
fastEntryEnabled={fastEntryEnabled}
companyHolidays={companyHolidays}
assignedProjects={assignedProjects}
allProjects={allProjects}
leaveRecords={leaveRecords}
errorComponent={errorComponent}
/>
</Box>
</FullscreenModal>
)}
</FormProvider>
);
};

export default TimesheetModal;

+ 0
- 1
src/components/TimesheetModal/index.ts 查看文件

@@ -1 +0,0 @@
export { default } from "./TimesheetModal";

+ 1
- 0
src/components/TimesheetTable/FastTimeEntryModal.tsx 查看文件

@@ -174,6 +174,7 @@ const FastTimeEntryModal: React.FC<Props> = ({
name="projectIds"
render={({ field }) => (
<ProjectSelect
includeLeaves={false}
error={Boolean(formState.errors.projectIds)}
multiple
allProjects={allProjectsWithFastEntry}


正在加载...
取消
保存