@@ -1,12 +1,6 @@ | |||||
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | ||||
import { HolidaysResult } from "../holidays"; | 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 { convertDateArrayToString } from "@/app/utils/formatUtil"; | ||||
import compact from "lodash/compact"; | import compact from "lodash/compact"; | ||||
@@ -83,82 +77,6 @@ export const validateLeaveEntry = ( | |||||
return Object.keys(error).length > 0 ? error : undefined; | 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 = ( | export const validateTimeLeaveRecord = ( | ||||
records: RecordTimeLeaveInput, | records: RecordTimeLeaveInput, | ||||
companyHolidays: HolidaysResult[], | companyHolidays: HolidaysResult[], | ||||
@@ -191,8 +109,8 @@ export const validateTimeLeaveRecord = ( | |||||
// Check total hours | // Check total hours | ||||
const totalHourError = checkTotalHours( | 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) { | 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" | name="projectIds" | ||||
render={({ field }) => ( | render={({ field }) => ( | ||||
<ProjectSelect | <ProjectSelect | ||||
includeLeaves={false} | |||||
error={Boolean(formState.errors.projectIds)} | error={Boolean(formState.errors.projectIds)} | ||||
multiple | multiple | ||||
allProjects={allProjectsWithFastEntry} | allProjects={allProjectsWithFastEntry} | ||||