@@ -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) { | |||
@@ -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; |
@@ -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; |
@@ -1 +0,0 @@ | |||
export { default } from "./EnterLeaveModal"; |
@@ -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; |
@@ -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; |
@@ -1 +0,0 @@ | |||
export { default } from "./EnterTimesheetModal"; |
@@ -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; |
@@ -1 +0,0 @@ | |||
export { default } from "./TimesheetModal"; |
@@ -174,6 +174,7 @@ const FastTimeEntryModal: React.FC<Props> = ({ | |||
name="projectIds" | |||
render={({ field }) => ( | |||
<ProjectSelect | |||
includeLeaves={false} | |||
error={Boolean(formState.errors.projectIds)} | |||
multiple | |||
allProjects={allProjectsWithFastEntry} | |||