| @@ -1,12 +1,6 @@ | |||
| import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | |||
| import { HolidaysResult } from "../holidays"; | |||
| import { | |||
| LeaveEntry, | |||
| RecordLeaveInput, | |||
| RecordTimeLeaveInput, | |||
| RecordTimesheetInput, | |||
| TimeEntry, | |||
| } from "./actions"; | |||
| import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; | |||
| import { convertDateArrayToString } from "@/app/utils/formatUtil"; | |||
| import compact from "lodash/compact"; | |||
| @@ -83,82 +77,6 @@ export const validateLeaveEntry = ( | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }; | |||
| export const validateTimesheet = ( | |||
| timesheet: RecordTimesheetInput, | |||
| leaveRecords: RecordLeaveInput, | |||
| companyHolidays: HolidaysResult[], | |||
| ): { [date: string]: string } | undefined => { | |||
| const errors: { [date: string]: string } = {}; | |||
| const holidays = new Set( | |||
| compact([ | |||
| ...getPublicHolidaysForNYears(2).map((h) => h.date), | |||
| ...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||
| ]), | |||
| ); | |||
| Object.keys(timesheet).forEach((date) => { | |||
| const timeEntries = timesheet[date]; | |||
| // Check each entry | |||
| for (const entry of timeEntries) { | |||
| const entryErrors = validateTimeEntry(entry, holidays.has(date)); | |||
| if (entryErrors) { | |||
| errors[date] = "There are errors in the entries"; | |||
| return; | |||
| } | |||
| } | |||
| // Check total hours | |||
| const leaves = leaveRecords[date] || []; | |||
| const totalHourError = checkTotalHours(timeEntries, leaves); | |||
| if (totalHourError) { | |||
| errors[date] = totalHourError; | |||
| } | |||
| }); | |||
| return Object.keys(errors).length > 0 ? errors : undefined; | |||
| }; | |||
| export const validateLeaveRecord = ( | |||
| leaveRecords: RecordLeaveInput, | |||
| timesheet: RecordTimesheetInput, | |||
| companyHolidays: HolidaysResult[], | |||
| ): { [date: string]: string } | undefined => { | |||
| const errors: { [date: string]: string } = {}; | |||
| const holidays = new Set( | |||
| compact([ | |||
| ...getPublicHolidaysForNYears(2).map((h) => h.date), | |||
| ...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||
| ]), | |||
| ); | |||
| Object.keys(leaveRecords).forEach((date) => { | |||
| const leaves = leaveRecords[date]; | |||
| // Check each leave entry | |||
| for (const entry of leaves) { | |||
| const entryError = validateLeaveEntry(entry, holidays.has(date)); | |||
| if (entryError) { | |||
| errors[date] = "There are errors in the entries"; | |||
| return; | |||
| } | |||
| } | |||
| // Check total hours | |||
| const timeEntries = timesheet[date] || []; | |||
| const totalHourError = checkTotalHours(timeEntries, leaves); | |||
| if (totalHourError) { | |||
| errors[date] = totalHourError; | |||
| } | |||
| }); | |||
| return Object.keys(errors).length > 0 ? errors : undefined; | |||
| }; | |||
| export const validateTimeLeaveRecord = ( | |||
| records: RecordTimeLeaveInput, | |||
| companyHolidays: HolidaysResult[], | |||
| @@ -191,8 +109,8 @@ export const validateTimeLeaveRecord = ( | |||
| // Check total hours | |||
| const totalHourError = checkTotalHours( | |||
| entries.filter((e) => e.type === "timeEntry"), | |||
| entries.filter((e) => e.type === "leaveEntry"), | |||
| entries.filter((e) => e.type === "timeEntry") as TimeEntry[], | |||
| entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | |||
| ); | |||
| if (totalHourError) { | |||
| @@ -1,109 +0,0 @@ | |||
| "use client"; | |||
| import { useState } from "react"; | |||
| import Button from "@mui/material/Button"; | |||
| import { Card, Modal } from "@mui/material"; | |||
| import TimesheetInputGrid from "./LeaveInputGrid"; | |||
| // import { fetchLeaves } from "@/app/api/leave"; | |||
| interface EnterTimesheetModalProps { | |||
| isOpen: boolean; | |||
| onClose: () => void; | |||
| modalStyle?: any; | |||
| } | |||
| const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ | |||
| ...props | |||
| }) => { | |||
| const [lockConfirm, setLockConfirm] = useState(false); | |||
| const columns = [ | |||
| { | |||
| id: "projectCode", | |||
| field: "projectCode", | |||
| headerName: "Project Code and Name", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| id: "task", | |||
| field: "task", | |||
| headerName: "Task", | |||
| flex: 1, | |||
| }, | |||
| ]; | |||
| const rows = [ | |||
| { | |||
| id: 1, | |||
| projectCode: "M1001", | |||
| task: "1.2", | |||
| }, | |||
| { | |||
| id: 2, | |||
| projectCode: "M1301", | |||
| task: "1.1", | |||
| }, | |||
| ]; | |||
| const fetchTimesheet = async () => { | |||
| // fetchLeaves(); | |||
| // const res = await fetch(`http://localhost:8090/api/timesheets`, { | |||
| // // const res = await fetch(`${BASE_API_URL}/timesheets`, { | |||
| // method: "GET", | |||
| // mode: 'no-cors', | |||
| // }); | |||
| // console.log(res.json); | |||
| }; | |||
| return ( | |||
| <Modal open={props.isOpen} onClose={props.onClose}> | |||
| <div> | |||
| {/* <Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||
| <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | |||
| Record Leave | |||
| </div> | |||
| </Typography> */} | |||
| <Card style={{ | |||
| flex: 10, | |||
| marginBottom: "20px", | |||
| width: "90%", | |||
| // height: "80%", | |||
| position: "fixed", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| }}> | |||
| <TimesheetInputGrid setLockConfirm={setLockConfirm}/> | |||
| <div | |||
| style={{ | |||
| display: "flex", | |||
| justifyContent: "space-between", | |||
| width: "100%", | |||
| flex: 1, | |||
| padding: "20px", | |||
| }} | |||
| > | |||
| <Button | |||
| disabled={lockConfirm} | |||
| variant="contained" | |||
| onClick={props.onClose} | |||
| > | |||
| Confirm | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| onClick={props.onClose} | |||
| sx={{ "background-color": "#F890A5" }} | |||
| > | |||
| Cancel | |||
| </Button> | |||
| </div> | |||
| </Card> | |||
| </div> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default EnterTimesheetModal; | |||
| @@ -1,548 +0,0 @@ | |||
| "use client"; | |||
| import Grid from "@mui/material/Grid"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import { useState, useEffect } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import PageTitle from "../PageTitle/PageTitle"; | |||
| import { Suspense } from "react"; | |||
| import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Link from "next/link"; | |||
| import { t } from "i18next"; | |||
| import { | |||
| Box, | |||
| Container, | |||
| Modal, | |||
| Select, | |||
| SelectChangeEvent, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Close } from "@mui/icons-material"; | |||
| import AddIcon from "@mui/icons-material/Add"; | |||
| import EditIcon from "@mui/icons-material/Edit"; | |||
| import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import CancelIcon from "@mui/icons-material/Close"; | |||
| import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; | |||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||
| import Swal from "sweetalert2"; | |||
| import { msg } from "../Swal/CustomAlerts"; | |||
| import React from "react"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import { | |||
| GridRowsProp, | |||
| GridRowModesModel, | |||
| GridRowModes, | |||
| DataGrid, | |||
| GridColDef, | |||
| GridToolbarContainer, | |||
| GridFooterContainer, | |||
| GridActionsCellItem, | |||
| GridEventListener, | |||
| GridRowId, | |||
| GridRowModel, | |||
| GridRowEditStopReasons, | |||
| GridEditInputCell, | |||
| GridValueSetterParams, | |||
| } from "@mui/x-data-grid"; | |||
| import { LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs from "dayjs"; | |||
| import { Props } from "react-intl/src/components/relative"; | |||
| const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; | |||
| interface BottomBarProps { | |||
| getHoursTotal: (column: string) => number; | |||
| setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| interface EditToolbarProps { | |||
| // setDay: (newDay : dayjs.Dayjs) => void; | |||
| setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| interface EditFooterProps { | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| const EditToolbar = (props: EditToolbarProps) => { | |||
| const { setDay } = props; | |||
| const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||
| const handleClickLeft = () => { | |||
| if (selectedDate) { | |||
| const newDate = selectedDate.add(-7, "day"); | |||
| setSelectedDate(newDate); | |||
| } | |||
| }; | |||
| const handleClickRight = () => { | |||
| if (selectedDate) { | |||
| const newDate = | |||
| selectedDate.add(7, "day") > dayjs() | |||
| ? dayjs() | |||
| : selectedDate.add(7, "day"); | |||
| setSelectedDate(newDate); | |||
| } | |||
| }; | |||
| const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||
| const newDate = dayjs(date); | |||
| setSelectedDate(newDate); | |||
| }; | |||
| useEffect(() => { | |||
| setDay((oldDay) => selectedDate); | |||
| }, [selectedDate]); | |||
| return ( | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <div | |||
| style={{ | |||
| display: "flex", | |||
| justifyContent: "flex-end", | |||
| width: "100%", | |||
| paddingBottom: "20px", | |||
| }} | |||
| > | |||
| <Typography variant="h5" id="modal-title" sx={{ flex: 1 }}> | |||
| Record Leave | |||
| </Typography> | |||
| <Button | |||
| sx={{ "border-radius": "30%", marginRight: "20px" }} | |||
| variant="contained" | |||
| onClick={handleClickLeft} | |||
| > | |||
| <ArrowBackIcon /> | |||
| </Button> | |||
| <DatePicker | |||
| value={selectedDate} | |||
| onChange={handleDateChange} | |||
| disableFuture={true} | |||
| /> | |||
| <Button | |||
| sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }} | |||
| variant="contained" | |||
| onClick={handleClickRight} | |||
| > | |||
| <ArrowForwardIcon /> | |||
| </Button> | |||
| </div> | |||
| </LocalizationProvider> | |||
| ); | |||
| }; | |||
| const BottomBar = (props: BottomBarProps) => { | |||
| const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | |||
| // const getHoursTotal = props.getHoursTotal; | |||
| const [newId, setNewId] = useState(-1); | |||
| const [invalidDays, setInvalidDays] = useState(0); | |||
| const handleAddClick = () => { | |||
| const id = newId; | |||
| setNewId(newId - 1); | |||
| setRows((oldRows) => [ | |||
| ...oldRows, | |||
| { id, projectCode: "", task: "", isNew: true }, | |||
| ]); | |||
| setRowModesModel((oldModel) => ({ | |||
| ...oldModel, | |||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, | |||
| })); | |||
| }; | |||
| const totalColDef = { | |||
| flex: 1, | |||
| // style: {color:getHoursTotal('mon')>24?"red":"black"} | |||
| }; | |||
| const TotalCell = ({ value }: Props) => { | |||
| const [invalid, setInvalid] = useState(false); | |||
| useEffect(() => { | |||
| const newInvalid = (value ?? 0) > 24; | |||
| setInvalid(newInvalid); | |||
| }, [value]); | |||
| return ( | |||
| <Box flex={1} style={{ color: invalid ? "red" : "black" }}> | |||
| {value} | |||
| </Box> | |||
| ); | |||
| }; | |||
| const checkUnlockConfirmBtn = () => { | |||
| // setLockConfirm((oldLock)=> valid); | |||
| setLockConfirm((oldLock) => | |||
| weekdays.every((weekday) => { | |||
| getHoursTotal(weekday) <= 24; | |||
| }), | |||
| ); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
| <Box flex={5.7} textAlign={"right"} marginRight="4rem"> | |||
| <b>Total:</b> | |||
| </Box> | |||
| <TotalCell value={getHoursTotal("mon")} /> | |||
| <TotalCell value={getHoursTotal("tue")} /> | |||
| <TotalCell value={getHoursTotal("wed")} /> | |||
| <TotalCell value={getHoursTotal("thu")} /> | |||
| <TotalCell value={getHoursTotal("fri")} /> | |||
| <TotalCell value={getHoursTotal("sat")} /> | |||
| <TotalCell value={getHoursTotal("sun")} /> | |||
| </div> | |||
| <Button | |||
| variant="outlined" | |||
| color="primary" | |||
| startIcon={<AddIcon />} | |||
| onClick={handleAddClick} | |||
| > | |||
| Add record | |||
| </Button> | |||
| </div> | |||
| ); | |||
| }; | |||
| const EditFooter = (props: EditFooterProps) => { | |||
| return ( | |||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
| <Box flex={1}> | |||
| <b>Total: </b> | |||
| </Box> | |||
| <Box flex={2}>ssss</Box> | |||
| </div> | |||
| ); | |||
| }; | |||
| interface TimesheetInputGridProps { | |||
| setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||
| onClose?: () => void; | |||
| } | |||
| const initialRows: GridRowsProp = [ | |||
| { | |||
| id: 1, | |||
| projectCode: "M1001", | |||
| task: "1.2", | |||
| mon: 2.5, | |||
| }, | |||
| { | |||
| id: 2, | |||
| projectCode: "M1002", | |||
| task: "1.3", | |||
| mon: 3.25, | |||
| }, | |||
| ]; | |||
| const options = ["M1001", "M1301", "M1354", "M1973"]; | |||
| const options2 = [ | |||
| "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", | |||
| "1.2 - Cash flow forecast", | |||
| "1.3 - Cost studies fo alterative design solutions", | |||
| "1.4 = Attend design co-ordination / project review meetings", | |||
| "1.5 - Prepare / Review RIC", | |||
| ]; | |||
| const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { | |||
| if (date.day() == 0) { | |||
| return date.add(weekday - date.day() - 7, "day").format("DD MMM"); | |||
| } else { | |||
| return date.add(weekday - date.day(), "day").format("DD MMM"); | |||
| } | |||
| }; | |||
| const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ | |||
| ...props | |||
| }) => { | |||
| const [rows, setRows] = useState(initialRows); | |||
| const [day, setDay] = useState(dayjs()); | |||
| const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | |||
| {}, | |||
| ); | |||
| const { setLockConfirm } = props; | |||
| const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
| params, | |||
| event, | |||
| ) => { | |||
| if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
| event.defaultMuiPrevented = true; | |||
| } | |||
| }; | |||
| const handleEditClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||
| }; | |||
| const handleSaveClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||
| }; | |||
| const handleDeleteClick = (id: GridRowId) => () => { | |||
| setRows(rows.filter((row) => row.id !== id)); | |||
| }; | |||
| const handleCancelClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ | |||
| ...rowModesModel, | |||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||
| }); | |||
| const editedRow = rows.find((row) => row.id === id); | |||
| if (editedRow!.isNew) { | |||
| setRows(rows.filter((row) => row.id !== id)); | |||
| } | |||
| }; | |||
| const processRowUpdate = (newRow: GridRowModel) => { | |||
| const updatedRow = { ...newRow, isNew: false }; | |||
| setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||
| return updatedRow; | |||
| }; | |||
| const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | |||
| setRowModesModel(newRowModesModel); | |||
| }; | |||
| const getHoursTotal = (column: any) => { | |||
| let sum = 0; | |||
| rows.forEach((row) => { | |||
| sum += row[column] ?? 0; | |||
| }); | |||
| return sum; | |||
| }; | |||
| const weekdayColConfig: any = { | |||
| type: "number", | |||
| // sortable: false, | |||
| //width: 100, | |||
| flex: 1, | |||
| align: "left", | |||
| headerAlign: "left", | |||
| editable: true, | |||
| renderEditCell: (value: any) => ( | |||
| <GridEditInputCell | |||
| {...value} | |||
| inputProps={{ | |||
| max: 24, | |||
| min: 0, | |||
| step: 0.25, | |||
| }} | |||
| /> | |||
| ), | |||
| }; | |||
| const columns: GridColDef[] = [ | |||
| { | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "Actions", | |||
| width: 100, | |||
| cellClassName: "actions", | |||
| getActions: ({ id }) => { | |||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
| if (isInEditMode) { | |||
| return [ | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-save`} | |||
| icon={<SaveIcon />} | |||
| title="Save" | |||
| label="Save" | |||
| sx={{ | |||
| color: "primary.main", | |||
| }} | |||
| onClick={handleSaveClick(id)} | |||
| />, | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-cancel`} | |||
| icon={<CancelIcon />} | |||
| title="Cancel" | |||
| label="Cancel" | |||
| className="textPrimary" | |||
| onClick={handleCancelClick(id)} | |||
| color="inherit" | |||
| />, | |||
| ]; | |||
| } | |||
| return [ | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-edit`} | |||
| icon={<EditIcon />} | |||
| title="Edit" | |||
| label="Edit" | |||
| className="textPrimary" | |||
| onClick={handleEditClick(id)} | |||
| color="inherit" | |||
| />, | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-delete`} | |||
| title="Delete" | |||
| label="Delete" | |||
| icon={<DeleteIcon />} | |||
| onClick={handleDeleteClick(id)} | |||
| sx={{ color: "red" }} | |||
| />, | |||
| ]; | |||
| }, | |||
| }, | |||
| { | |||
| field: "projectCode", | |||
| headerName: "Project Code", | |||
| // width: 220, | |||
| flex: 2, | |||
| editable: true, | |||
| type: "singleSelect", | |||
| valueOptions: options, | |||
| }, | |||
| { | |||
| field: "task", | |||
| headerName: "Task", | |||
| // width: 220, | |||
| flex: 3, | |||
| editable: true, | |||
| type: "singleSelect", | |||
| valueOptions: options2, | |||
| }, | |||
| { | |||
| // Mon | |||
| field: "mon", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Mon - {getDateForHeader(day, 1)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Tue | |||
| field: "tue", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Tue - {getDateForHeader(day, 2)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Wed | |||
| field: "wed", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Wed - {getDateForHeader(day, 3)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Thu | |||
| field: "thu", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Thu - {getDateForHeader(day, 4)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Fri | |||
| field: "fri", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Fri - {getDateForHeader(day, 5)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Sat | |||
| field: "sat", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Sat - {getDateForHeader(day, 6)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Sun | |||
| field: "sun", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return ( | |||
| <div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div> | |||
| ); | |||
| }, | |||
| }, | |||
| // { | |||
| // field: 'joinDate', | |||
| // headerName: 'Join date', | |||
| // type: 'date', | |||
| // width: 180, | |||
| // editable: true, | |||
| // }, | |||
| ]; | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| // marginBottom: '-5px', | |||
| display: "flex", | |||
| "flex-direction": "column", | |||
| // 'justify-content': 'flex-end', | |||
| padding: "20px", | |||
| height: "100%", //'30rem', | |||
| width: "100%", | |||
| "& .actions": { | |||
| color: "text.secondary", | |||
| }, | |||
| "& .header": { | |||
| // border: 1, | |||
| // 'border-width': '1px', | |||
| // 'border-color': 'grey', | |||
| }, | |||
| "& .textPrimary": { | |||
| color: "text.primary", | |||
| }, | |||
| }} | |||
| > | |||
| <DataGrid | |||
| rows={rows} | |||
| columns={columns} | |||
| editMode="row" | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={handleRowModesModelChange} | |||
| onRowEditStop={handleRowEditStop} | |||
| processRowUpdate={processRowUpdate} | |||
| disableRowSelectionOnClick={true} | |||
| disableColumnMenu={true} | |||
| hideFooterPagination={true} | |||
| slots={{ | |||
| toolbar: EditToolbar, | |||
| // footer: EditFooter, | |||
| }} | |||
| slotProps={{ | |||
| toolbar: { setDay, setRows, setRowModesModel }, | |||
| // footer: { setDay, setRows, setRowModesModel }, | |||
| }} | |||
| initialState={{ | |||
| pagination: { paginationModel: { pageSize: 100 } }, | |||
| }} | |||
| sx={{ flex: 1 }} | |||
| /> | |||
| <BottomBar | |||
| getHoursTotal={getHoursTotal} | |||
| setRows={setRows} | |||
| setRowModesModel={setRowModesModel} | |||
| setLockConfirm={setLockConfirm} | |||
| // sx={{flex:3}} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default TimesheetInputGrid; | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./EnterLeaveModal"; | |||
| @@ -1,109 +0,0 @@ | |||
| "use client"; | |||
| import { useState } from "react"; | |||
| import Button from "@mui/material/Button"; | |||
| import { Card, Modal } from "@mui/material"; | |||
| import TimesheetInputGrid from "./TimesheetInputGrid"; | |||
| // import { fetchTimesheets } from "@/app/api/timesheets"; | |||
| interface EnterTimesheetModalProps { | |||
| isOpen: boolean; | |||
| onClose: () => void; | |||
| modalStyle?: any; | |||
| } | |||
| const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ | |||
| ...props | |||
| }) => { | |||
| const [lockConfirm, setLockConfirm] = useState(false); | |||
| const columns = [ | |||
| { | |||
| id: "projectCode", | |||
| field: "projectCode", | |||
| headerName: "Project Code and Name", | |||
| flex: 1, | |||
| }, | |||
| { | |||
| id: "task", | |||
| field: "task", | |||
| headerName: "Task", | |||
| flex: 1, | |||
| }, | |||
| ]; | |||
| const rows = [ | |||
| { | |||
| id: 1, | |||
| projectCode: "M1001", | |||
| task: "1.2", | |||
| }, | |||
| { | |||
| id: 2, | |||
| projectCode: "M1301", | |||
| task: "1.1", | |||
| }, | |||
| ]; | |||
| const fetchTimesheet = async () => { | |||
| // fetchTimesheets(); | |||
| // const res = await fetch(`http://localhost:8090/api/timesheets`, { | |||
| // // const res = await fetch(`${BASE_API_URL}/timesheets`, { | |||
| // method: "GET", | |||
| // mode: 'no-cors', | |||
| // }); | |||
| // console.log(res.json); | |||
| }; | |||
| return ( | |||
| <Modal open={props.isOpen} onClose={props.onClose}> | |||
| <div> | |||
| {/* <Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||
| <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | |||
| Timesheet Input | |||
| </div> | |||
| </Typography> */} | |||
| <Card style={{ | |||
| flex: 10, | |||
| marginBottom: "20px", | |||
| width: "90%", | |||
| // height: "80%", | |||
| position: "fixed", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| }}> | |||
| <TimesheetInputGrid setLockConfirm={setLockConfirm}/> | |||
| <div | |||
| style={{ | |||
| display: "flex", | |||
| justifyContent: "space-between", | |||
| width: "100%", | |||
| flex: 1, | |||
| padding: "20px", | |||
| }} | |||
| > | |||
| <Button | |||
| disabled={lockConfirm} | |||
| variant="contained" | |||
| onClick={props.onClose} | |||
| > | |||
| Confirm | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| onClick={props.onClose} | |||
| sx={{ "background-color": "#F890A5" }} | |||
| > | |||
| Cancel | |||
| </Button> | |||
| </div> | |||
| </Card> | |||
| </div> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default EnterTimesheetModal; | |||
| @@ -1,548 +0,0 @@ | |||
| "use client"; | |||
| import Grid from "@mui/material/Grid"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import { useState, useEffect } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import PageTitle from "../PageTitle/PageTitle"; | |||
| import { Suspense } from "react"; | |||
| import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Link from "next/link"; | |||
| import { t } from "i18next"; | |||
| import { | |||
| Box, | |||
| Container, | |||
| Modal, | |||
| Select, | |||
| SelectChangeEvent, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Close } from "@mui/icons-material"; | |||
| import AddIcon from "@mui/icons-material/Add"; | |||
| import EditIcon from "@mui/icons-material/Edit"; | |||
| import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import CancelIcon from "@mui/icons-material/Close"; | |||
| import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; | |||
| import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||
| import Swal from "sweetalert2"; | |||
| import { msg } from "../Swal/CustomAlerts"; | |||
| import React from "react"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import { | |||
| GridRowsProp, | |||
| GridRowModesModel, | |||
| GridRowModes, | |||
| DataGrid, | |||
| GridColDef, | |||
| GridToolbarContainer, | |||
| GridFooterContainer, | |||
| GridActionsCellItem, | |||
| GridEventListener, | |||
| GridRowId, | |||
| GridRowModel, | |||
| GridRowEditStopReasons, | |||
| GridEditInputCell, | |||
| GridValueSetterParams, | |||
| } from "@mui/x-data-grid"; | |||
| import { LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs from "dayjs"; | |||
| import { Props } from "react-intl/src/components/relative"; | |||
| const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; | |||
| interface BottomBarProps { | |||
| getHoursTotal: (column: string) => number; | |||
| setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| interface EditToolbarProps { | |||
| // setDay: (newDay : dayjs.Dayjs) => void; | |||
| setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| interface EditFooterProps { | |||
| setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||
| setRowModesModel: ( | |||
| newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||
| ) => void; | |||
| } | |||
| const EditToolbar = (props: EditToolbarProps) => { | |||
| const { setDay } = props; | |||
| const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||
| const handleClickLeft = () => { | |||
| if (selectedDate) { | |||
| const newDate = selectedDate.add(-7, "day"); | |||
| setSelectedDate(newDate); | |||
| } | |||
| }; | |||
| const handleClickRight = () => { | |||
| if (selectedDate) { | |||
| const newDate = | |||
| selectedDate.add(7, "day") > dayjs() | |||
| ? dayjs() | |||
| : selectedDate.add(7, "day"); | |||
| setSelectedDate(newDate); | |||
| } | |||
| }; | |||
| const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||
| const newDate = dayjs(date); | |||
| setSelectedDate(newDate); | |||
| }; | |||
| useEffect(() => { | |||
| setDay((oldDay) => selectedDate); | |||
| }, [selectedDate]); | |||
| return ( | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <div | |||
| style={{ | |||
| display: "flex", | |||
| justifyContent: "flex-end", | |||
| width: "100%", | |||
| paddingBottom: "20px", | |||
| }} | |||
| > | |||
| <Typography variant="h5" id="modal-title" sx={{ flex: 1 }}> | |||
| Timesheet Input | |||
| </Typography> | |||
| <Button | |||
| sx={{ "border-radius": "30%", marginRight: "20px" }} | |||
| variant="contained" | |||
| onClick={handleClickLeft} | |||
| > | |||
| <ArrowBackIcon /> | |||
| </Button> | |||
| <DatePicker | |||
| value={selectedDate} | |||
| onChange={handleDateChange} | |||
| disableFuture={true} | |||
| /> | |||
| <Button | |||
| sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }} | |||
| variant="contained" | |||
| onClick={handleClickRight} | |||
| > | |||
| <ArrowForwardIcon /> | |||
| </Button> | |||
| </div> | |||
| </LocalizationProvider> | |||
| ); | |||
| }; | |||
| const BottomBar = (props: BottomBarProps) => { | |||
| const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | |||
| // const getHoursTotal = props.getHoursTotal; | |||
| const [newId, setNewId] = useState(-1); | |||
| const [invalidDays, setInvalidDays] = useState(0); | |||
| const handleAddClick = () => { | |||
| const id = newId; | |||
| setNewId(newId - 1); | |||
| setRows((oldRows) => [ | |||
| ...oldRows, | |||
| { id, projectCode: "", task: "", isNew: true }, | |||
| ]); | |||
| setRowModesModel((oldModel) => ({ | |||
| ...oldModel, | |||
| [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, | |||
| })); | |||
| }; | |||
| const totalColDef = { | |||
| flex: 1, | |||
| // style: {color:getHoursTotal('mon')>24?"red":"black"} | |||
| }; | |||
| const TotalCell = ({ value }: Props) => { | |||
| const [invalid, setInvalid] = useState(false); | |||
| useEffect(() => { | |||
| const newInvalid = (value ?? 0) > 24; | |||
| setInvalid(newInvalid); | |||
| }, [value]); | |||
| return ( | |||
| <Box flex={1} style={{ color: invalid ? "red" : "black" }}> | |||
| {value} | |||
| </Box> | |||
| ); | |||
| }; | |||
| const checkUnlockConfirmBtn = () => { | |||
| // setLockConfirm((oldLock)=> valid); | |||
| setLockConfirm((oldLock) => | |||
| weekdays.every((weekday) => { | |||
| getHoursTotal(weekday) <= 24; | |||
| }), | |||
| ); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
| <Box flex={5.7} textAlign={"right"} marginRight="4rem"> | |||
| <b>Total:</b> | |||
| </Box> | |||
| <TotalCell value={getHoursTotal("mon")} /> | |||
| <TotalCell value={getHoursTotal("tue")} /> | |||
| <TotalCell value={getHoursTotal("wed")} /> | |||
| <TotalCell value={getHoursTotal("thu")} /> | |||
| <TotalCell value={getHoursTotal("fri")} /> | |||
| <TotalCell value={getHoursTotal("sat")} /> | |||
| <TotalCell value={getHoursTotal("sun")} /> | |||
| </div> | |||
| <Button | |||
| variant="outlined" | |||
| color="primary" | |||
| startIcon={<AddIcon />} | |||
| onClick={handleAddClick} | |||
| > | |||
| Add record | |||
| </Button> | |||
| </div> | |||
| ); | |||
| }; | |||
| const EditFooter = (props: EditFooterProps) => { | |||
| return ( | |||
| <div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||
| <Box flex={1}> | |||
| <b>Total: </b> | |||
| </Box> | |||
| <Box flex={2}>ssss</Box> | |||
| </div> | |||
| ); | |||
| }; | |||
| interface TimesheetInputGridProps { | |||
| setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||
| onClose?: () => void; | |||
| } | |||
| const initialRows: GridRowsProp = [ | |||
| { | |||
| id: 1, | |||
| projectCode: "M1001", | |||
| task: "1.2", | |||
| mon: 2.5, | |||
| }, | |||
| { | |||
| id: 2, | |||
| projectCode: "M1002", | |||
| task: "1.3", | |||
| mon: 3.25, | |||
| }, | |||
| ]; | |||
| const options = ["M1001", "M1301", "M1354", "M1973"]; | |||
| const options2 = [ | |||
| "1.1 - Preparation of preliminary Cost Estimate / Cost Plan", | |||
| "1.2 - Cash flow forecast", | |||
| "1.3 - Cost studies fo alterative design solutions", | |||
| "1.4 = Attend design co-ordination / project review meetings", | |||
| "1.5 - Prepare / Review RIC", | |||
| ]; | |||
| const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { | |||
| if (date.day() == 0) { | |||
| return date.add(weekday - date.day() - 7, "day").format("DD MMM"); | |||
| } else { | |||
| return date.add(weekday - date.day(), "day").format("DD MMM"); | |||
| } | |||
| }; | |||
| const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ | |||
| ...props | |||
| }) => { | |||
| const [rows, setRows] = useState(initialRows); | |||
| const [day, setDay] = useState(dayjs()); | |||
| const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | |||
| {}, | |||
| ); | |||
| const { setLockConfirm } = props; | |||
| const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||
| params, | |||
| event, | |||
| ) => { | |||
| if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
| event.defaultMuiPrevented = true; | |||
| } | |||
| }; | |||
| const handleEditClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||
| }; | |||
| const handleSaveClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||
| }; | |||
| const handleDeleteClick = (id: GridRowId) => () => { | |||
| setRows(rows.filter((row) => row.id !== id)); | |||
| }; | |||
| const handleCancelClick = (id: GridRowId) => () => { | |||
| setRowModesModel({ | |||
| ...rowModesModel, | |||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||
| }); | |||
| const editedRow = rows.find((row) => row.id === id); | |||
| if (editedRow!.isNew) { | |||
| setRows(rows.filter((row) => row.id !== id)); | |||
| } | |||
| }; | |||
| const processRowUpdate = (newRow: GridRowModel) => { | |||
| const updatedRow = { ...newRow, isNew: false }; | |||
| setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||
| return updatedRow; | |||
| }; | |||
| const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | |||
| setRowModesModel(newRowModesModel); | |||
| }; | |||
| const getHoursTotal = (column: any) => { | |||
| let sum = 0; | |||
| rows.forEach((row) => { | |||
| sum += row[column] ?? 0; | |||
| }); | |||
| return sum; | |||
| }; | |||
| const weekdayColConfig: any = { | |||
| type: "number", | |||
| // sortable: false, | |||
| //width: 100, | |||
| flex: 1, | |||
| align: "left", | |||
| headerAlign: "left", | |||
| editable: true, | |||
| renderEditCell: (value: any) => ( | |||
| <GridEditInputCell | |||
| {...value} | |||
| inputProps={{ | |||
| max: 24, | |||
| min: 0, | |||
| step: 0.25, | |||
| }} | |||
| /> | |||
| ), | |||
| }; | |||
| const columns: GridColDef[] = [ | |||
| { | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "Actions", | |||
| width: 100, | |||
| cellClassName: "actions", | |||
| getActions: ({ id }) => { | |||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
| if (isInEditMode) { | |||
| return [ | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-save`} | |||
| icon={<SaveIcon />} | |||
| title="Save" | |||
| label="Save" | |||
| sx={{ | |||
| color: "primary.main", | |||
| }} | |||
| onClick={handleSaveClick(id)} | |||
| />, | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-cancel`} | |||
| icon={<CancelIcon />} | |||
| title="Cancel" | |||
| label="Cancel" | |||
| className="textPrimary" | |||
| onClick={handleCancelClick(id)} | |||
| color="inherit" | |||
| />, | |||
| ]; | |||
| } | |||
| return [ | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-edit`} | |||
| icon={<EditIcon />} | |||
| title="Edit" | |||
| label="Edit" | |||
| className="textPrimary" | |||
| onClick={handleEditClick(id)} | |||
| color="inherit" | |||
| />, | |||
| <GridActionsCellItem | |||
| key={`actions-${id}-delete`} | |||
| title="Delete" | |||
| label="Delete" | |||
| icon={<DeleteIcon />} | |||
| onClick={handleDeleteClick(id)} | |||
| sx={{ color: "red" }} | |||
| />, | |||
| ]; | |||
| }, | |||
| }, | |||
| { | |||
| field: "projectCode", | |||
| headerName: "Project Code", | |||
| // width: 220, | |||
| flex: 2, | |||
| editable: true, | |||
| type: "singleSelect", | |||
| valueOptions: options, | |||
| }, | |||
| { | |||
| field: "task", | |||
| headerName: "Task", | |||
| // width: 220, | |||
| flex: 3, | |||
| editable: true, | |||
| type: "singleSelect", | |||
| valueOptions: options2, | |||
| }, | |||
| { | |||
| // Mon | |||
| field: "mon", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Mon - {getDateForHeader(day, 1)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Tue | |||
| field: "tue", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Tue - {getDateForHeader(day, 2)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Wed | |||
| field: "wed", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Wed - {getDateForHeader(day, 3)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Thu | |||
| field: "thu", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Thu - {getDateForHeader(day, 4)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Fri | |||
| field: "fri", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Fri - {getDateForHeader(day, 5)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Sat | |||
| field: "sat", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return <div>Sat - {getDateForHeader(day, 6)}</div>; | |||
| }, | |||
| }, | |||
| { | |||
| // Sun | |||
| field: "sun", | |||
| ...weekdayColConfig, | |||
| renderHeader: () => { | |||
| return ( | |||
| <div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div> | |||
| ); | |||
| }, | |||
| }, | |||
| // { | |||
| // field: 'joinDate', | |||
| // headerName: 'Join date', | |||
| // type: 'date', | |||
| // width: 180, | |||
| // editable: true, | |||
| // }, | |||
| ]; | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| // marginBottom: '-5px', | |||
| display: "flex", | |||
| "flex-direction": "column", | |||
| // 'justify-content': 'flex-end', | |||
| padding: "20px", | |||
| height: "100%", //'30rem', | |||
| width: "100%", | |||
| "& .actions": { | |||
| color: "text.secondary", | |||
| }, | |||
| "& .header": { | |||
| // border: 1, | |||
| // 'border-width': '1px', | |||
| // 'border-color': 'grey', | |||
| }, | |||
| "& .textPrimary": { | |||
| color: "text.primary", | |||
| }, | |||
| }} | |||
| > | |||
| <DataGrid | |||
| rows={rows} | |||
| columns={columns} | |||
| editMode="row" | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={handleRowModesModelChange} | |||
| onRowEditStop={handleRowEditStop} | |||
| processRowUpdate={processRowUpdate} | |||
| disableRowSelectionOnClick={true} | |||
| disableColumnMenu={true} | |||
| hideFooterPagination={true} | |||
| slots={{ | |||
| toolbar: EditToolbar, | |||
| // footer: EditFooter, | |||
| }} | |||
| slotProps={{ | |||
| toolbar: { setDay, setRows, setRowModesModel }, | |||
| // footer: { setDay, setRows, setRowModesModel }, | |||
| }} | |||
| initialState={{ | |||
| pagination: { paginationModel: { pageSize: 100 } }, | |||
| }} | |||
| sx={{ flex: 1 }} | |||
| /> | |||
| <BottomBar | |||
| getHoursTotal={getHoursTotal} | |||
| setRows={setRows} | |||
| setRowModesModel={setRowModesModel} | |||
| setLockConfirm={setLockConfirm} | |||
| // sx={{flex:3}} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default TimesheetInputGrid; | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./EnterTimesheetModal"; | |||
| @@ -1,223 +0,0 @@ | |||
| import React, { useCallback, useEffect, useMemo } from "react"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Card, | |||
| CardActions, | |||
| CardContent, | |||
| Modal, | |||
| ModalProps, | |||
| SxProps, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import TimesheetTable from "../TimesheetTable"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Check, Close } from "@mui/icons-material"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { | |||
| RecordLeaveInput, | |||
| RecordTimesheetInput, | |||
| saveTimesheet, | |||
| } from "@/app/api/timesheets/actions"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
| import FullscreenModal from "../FullscreenModal"; | |||
| import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; | |||
| import useIsMobile from "@/app/utils/useIsMobile"; | |||
| import { HolidaysResult } from "@/app/api/holidays"; | |||
| import { | |||
| DAILY_NORMAL_MAX_HOURS, | |||
| TIMESHEET_DAILY_MAX_HOURS, | |||
| validateTimesheet, | |||
| } from "@/app/api/timesheets/utils"; | |||
| import ErrorAlert from "../ErrorAlert"; | |||
| interface Props { | |||
| isOpen: boolean; | |||
| onClose: () => void; | |||
| allProjects: ProjectWithTasks[]; | |||
| assignedProjects: AssignedProject[]; | |||
| defaultTimesheets?: RecordTimesheetInput; | |||
| leaveRecords: RecordLeaveInput; | |||
| companyHolidays: HolidaysResult[]; | |||
| fastEntryEnabled?: boolean; | |||
| } | |||
| const modalSx: SxProps = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| width: { xs: "calc(100% - 2rem)", sm: "90%" }, | |||
| maxHeight: "90%", | |||
| maxWidth: 1400, | |||
| }; | |||
| const TimesheetModal: React.FC<Props> = ({ | |||
| isOpen, | |||
| onClose, | |||
| allProjects, | |||
| assignedProjects, | |||
| defaultTimesheets, | |||
| leaveRecords, | |||
| companyHolidays, | |||
| fastEntryEnabled, | |||
| }) => { | |||
| const { t } = useTranslation("home"); | |||
| const defaultValues = useMemo(() => { | |||
| const today = dayjs(); | |||
| return Array(7) | |||
| .fill(undefined) | |||
| .reduce<RecordTimesheetInput>((acc, _, index) => { | |||
| const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||
| return { | |||
| ...acc, | |||
| [date]: defaultTimesheets?.[date] ?? [], | |||
| }; | |||
| }, {}); | |||
| }, [defaultTimesheets]); | |||
| const formProps = useForm<RecordTimesheetInput>({ defaultValues }); | |||
| useEffect(() => { | |||
| formProps.reset(defaultValues); | |||
| }, [defaultValues, formProps]); | |||
| const onSubmit = useCallback<SubmitHandler<RecordTimesheetInput>>( | |||
| async (data) => { | |||
| const errors = validateTimesheet(data, leaveRecords, companyHolidays); | |||
| if (errors) { | |||
| Object.keys(errors).forEach((date) => | |||
| formProps.setError(date, { | |||
| message: errors[date], | |||
| }), | |||
| ); | |||
| return; | |||
| } | |||
| const savedRecords = await saveTimesheet(data); | |||
| const today = dayjs(); | |||
| const newFormValues = Array(7) | |||
| .fill(undefined) | |||
| .reduce<RecordTimesheetInput>((acc, _, index) => { | |||
| const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||
| return { | |||
| ...acc, | |||
| [date]: savedRecords[date] ?? [], | |||
| }; | |||
| }, {}); | |||
| formProps.reset(newFormValues); | |||
| onClose(); | |||
| }, | |||
| [companyHolidays, formProps, leaveRecords, onClose], | |||
| ); | |||
| const onCancel = useCallback(() => { | |||
| formProps.reset(defaultValues); | |||
| onClose(); | |||
| }, [defaultValues, formProps, onClose]); | |||
| const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (_, reason) => { | |||
| if (reason !== "backdropClick") { | |||
| onClose(); | |||
| } | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const errorComponent = ( | |||
| <ErrorAlert | |||
| errors={Object.keys(formProps.formState.errors).map((date) => { | |||
| const error = formProps.formState.errors[date]?.message; | |||
| return error | |||
| ? `${date}: ${t(error, { | |||
| TIMESHEET_DAILY_MAX_HOURS, | |||
| DAILY_NORMAL_MAX_HOURS, | |||
| })}` | |||
| : undefined; | |||
| })} | |||
| /> | |||
| ); | |||
| const matches = useIsMobile(); | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| {!matches ? ( | |||
| // Desktop version | |||
| <Modal open={isOpen} onClose={onModalClose}> | |||
| <Card sx={modalSx}> | |||
| <CardContent | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Timesheet Input")} | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| marginInline: -3, | |||
| marginBlock: 4, | |||
| }} | |||
| > | |||
| <TimesheetTable | |||
| companyHolidays={companyHolidays} | |||
| assignedProjects={assignedProjects} | |||
| allProjects={allProjects} | |||
| leaveRecords={leaveRecords} | |||
| fastEntryEnabled={fastEntryEnabled} | |||
| /> | |||
| </Box> | |||
| {errorComponent} | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| onClick={onCancel} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||
| {t("Save")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| </Modal> | |||
| ) : ( | |||
| // Mobile version | |||
| <FullscreenModal | |||
| open={isOpen} | |||
| onClose={onModalClose} | |||
| closeModal={onCancel} | |||
| > | |||
| <Box | |||
| display="flex" | |||
| flexDirection="column" | |||
| gap={2} | |||
| height="100%" | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| <Typography variant="h6" padding={2} flex="none"> | |||
| {t("Timesheet Input")} | |||
| </Typography> | |||
| <MobileTimesheetTable | |||
| fastEntryEnabled={fastEntryEnabled} | |||
| companyHolidays={companyHolidays} | |||
| assignedProjects={assignedProjects} | |||
| allProjects={allProjects} | |||
| leaveRecords={leaveRecords} | |||
| errorComponent={errorComponent} | |||
| /> | |||
| </Box> | |||
| </FullscreenModal> | |||
| )} | |||
| </FormProvider> | |||
| ); | |||
| }; | |||
| export default TimesheetModal; | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./TimesheetModal"; | |||
| @@ -174,6 +174,7 @@ const FastTimeEntryModal: React.FC<Props> = ({ | |||
| name="projectIds" | |||
| render={({ field }) => ( | |||
| <ProjectSelect | |||
| includeLeaves={false} | |||
| error={Boolean(formState.errors.projectIds)} | |||
| multiple | |||
| allProjects={allProjectsWithFastEntry} | |||