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