# Conflicts: # src/app/utils/formatUtil.ts # src/components/InvoiceSearch/InvoiceSearch.tsx # src/components/InvoiceSearch/InvoiceSearchWrapper.tsxtags/Baseline_30082024_FRONTEND_UAT
| @@ -13,6 +13,7 @@ export type TeamTimeSheets = { | |||||
| timeEntries: RecordTimesheetInput; | timeEntries: RecordTimesheetInput; | ||||
| staffId: string; | staffId: string; | ||||
| name: string; | name: string; | ||||
| employType: string | null; | |||||
| }; | }; | ||||
| }; | }; | ||||
| @@ -21,6 +22,7 @@ export type TeamLeaves = { | |||||
| leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
| staffId: string; | staffId: string; | ||||
| name: string; | name: string; | ||||
| employType: string | null; | |||||
| }; | }; | ||||
| }; | }; | ||||
| @@ -1,14 +1,9 @@ | |||||
| 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"; | ||||
| import dayjs from "dayjs"; | |||||
| export type TimeEntryError = { | export type TimeEntryError = { | ||||
| [field in keyof TimeEntry]?: string; | [field in keyof TimeEntry]?: string; | ||||
| @@ -83,85 +78,10 @@ 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[], | ||||
| isFullTime?: boolean, | |||||
| ): { [date: string]: string } | undefined => { | ): { [date: string]: string } | undefined => { | ||||
| const errors: { [date: string]: string } = {}; | const errors: { [date: string]: string } = {}; | ||||
| @@ -173,14 +93,18 @@ export const validateTimeLeaveRecord = ( | |||||
| ); | ); | ||||
| Object.keys(records).forEach((date) => { | Object.keys(records).forEach((date) => { | ||||
| const dayJsObj = dayjs(date); | |||||
| const isHoliday = | |||||
| holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const entries = records[date]; | const entries = records[date]; | ||||
| // Check each entry | // Check each entry | ||||
| for (const entry of entries) { | for (const entry of entries) { | ||||
| let entryError; | let entryError; | ||||
| if (entry.type === "leaveEntry") { | if (entry.type === "leaveEntry") { | ||||
| entryError = validateLeaveEntry(entry, holidays.has(date)); | |||||
| entryError = validateLeaveEntry(entry, isHoliday); | |||||
| } else { | } else { | ||||
| entryError = validateTimeEntry(entry, holidays.has(date)); | |||||
| entryError = validateTimeEntry(entry, isHoliday); | |||||
| } | } | ||||
| if (entryError) { | if (entryError) { | ||||
| @@ -191,8 +115,10 @@ 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[], | |||||
| isHoliday, | |||||
| isFullTime, | |||||
| ); | ); | ||||
| if (totalHourError) { | if (totalHourError) { | ||||
| @@ -206,6 +132,8 @@ export const validateTimeLeaveRecord = ( | |||||
| export const checkTotalHours = ( | export const checkTotalHours = ( | ||||
| timeEntries: TimeEntry[], | timeEntries: TimeEntry[], | ||||
| leaves: LeaveEntry[], | leaves: LeaveEntry[], | ||||
| isHoliday?: boolean, | |||||
| isFullTime?: boolean, | |||||
| ): string | undefined => { | ): string | undefined => { | ||||
| const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | ||||
| @@ -219,6 +147,12 @@ export const checkTotalHours = ( | |||||
| if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | ||||
| return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; | return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; | ||||
| } else if ( | |||||
| isFullTime && | |||||
| !isHoliday && | |||||
| totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS | |||||
| ) { | |||||
| return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; | |||||
| } else if ( | } else if ( | ||||
| totalInputHours + totalOtHours + leaveHours > | totalInputHours + totalOtHours + leaveHours > | ||||
| TIMESHEET_DAILY_MAX_HOURS | TIMESHEET_DAILY_MAX_HOURS | ||||
| @@ -134,8 +134,8 @@ export function convertLocaleStringToNumber(numberString: string): number { | |||||
| } | } | ||||
| export function timestampToDateString(timestamp: string): string { | export function timestampToDateString(timestamp: string): string { | ||||
| if (timestamp === "0" || timestamp === null) { | |||||
| return "-"; | |||||
| if (timestamp === null){ | |||||
| return "-" | |||||
| } | } | ||||
| const date = new Date(timestamp); | const date = new Date(timestamp); | ||||
| const year = date.getFullYear(); | const year = date.getFullYear(); | ||||
| @@ -344,7 +344,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| {/* <Grid item xs={6}> | |||||
| <Checkbox | <Checkbox | ||||
| {...register("isClpProject")} | {...register("isClpProject")} | ||||
| checked={Boolean(watch("isClpProject"))} | checked={Boolean(watch("isClpProject"))} | ||||
| @@ -353,7 +353,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
| <Typography variant="overline" display="inline"> | <Typography variant="overline" display="inline"> | ||||
| {t("CLP Project")} | {t("CLP Project")} | ||||
| </Typography> | </Typography> | ||||
| </Grid> | |||||
| </Grid> */} | |||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> | ||||
| @@ -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"; | |||||
| @@ -100,7 +100,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic | |||||
| formData.append('multipartFileList', file); | formData.append('multipartFileList', file); | ||||
| const response = await importIssuedInovice(formData); | const response = await importIssuedInovice(formData); | ||||
| console.log(response); | |||||
| // response: status, message, projectList, emptyRowList, invoiceList | |||||
| if (response.status) { | if (response.status) { | ||||
| successDialog(t("Import Success"), t).then(() => { | successDialog(t("Import Success"), t).then(() => { | ||||
| @@ -0,0 +1,285 @@ | |||||
| import React, { useCallback, useMemo, useState } from "react"; | |||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { LeaveType } from "@/app/api/timesheets"; | |||||
| import dayGridPlugin from "@fullcalendar/daygrid"; | |||||
| import interactionPlugin from "@fullcalendar/interaction"; | |||||
| import { Box, useTheme } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| getHolidayForDate, | |||||
| getPublicHolidaysForNYears, | |||||
| } from "@/app/utils/holidayUtils"; | |||||
| import { | |||||
| INPUT_DATE_FORMAT, | |||||
| convertDateArrayToString, | |||||
| } from "@/app/utils/formatUtil"; | |||||
| import StyledFullCalendar from "../StyledFullCalendar"; | |||||
| import { ProjectWithTasks } from "@/app/api/projects"; | |||||
| import { | |||||
| LeaveEntry, | |||||
| RecordLeaveInput, | |||||
| RecordTimesheetInput, | |||||
| saveLeave, | |||||
| } from "@/app/api/timesheets/actions"; | |||||
| import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | |||||
| import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | |||||
| import dayjs from "dayjs"; | |||||
| import { checkTotalHours } from "@/app/api/timesheets/utils"; | |||||
| import unionBy from "lodash/unionBy"; | |||||
| export interface Props { | |||||
| leaveTypes: LeaveType[]; | |||||
| companyHolidays: HolidaysResult[]; | |||||
| allProjects: ProjectWithTasks[]; | |||||
| leaveRecords: RecordLeaveInput; | |||||
| timesheetRecords: RecordTimesheetInput; | |||||
| isFullTime: boolean; | |||||
| } | |||||
| interface EventClickArg { | |||||
| event: { | |||||
| start: Date | null; | |||||
| startStr: string; | |||||
| extendedProps: { | |||||
| calendar?: string; | |||||
| entry?: LeaveEntry; | |||||
| }; | |||||
| }; | |||||
| } | |||||
| const LeaveCalendar: React.FC<Props> = ({ | |||||
| companyHolidays, | |||||
| allProjects, | |||||
| leaveTypes, | |||||
| timesheetRecords, | |||||
| leaveRecords, | |||||
| isFullTime, | |||||
| }) => { | |||||
| const { t } = useTranslation(["home", "common"]); | |||||
| const theme = useTheme(); | |||||
| const projectMap = useMemo(() => { | |||||
| return allProjects.reduce<{ | |||||
| [id: ProjectWithTasks["id"]]: ProjectWithTasks; | |||||
| }>((acc, project) => { | |||||
| return { ...acc, [project.id]: project }; | |||||
| }, {}); | |||||
| }, [allProjects]); | |||||
| const leaveMap = useMemo(() => { | |||||
| return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>( | |||||
| (acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }), | |||||
| {}, | |||||
| ); | |||||
| }, [leaveTypes]); | |||||
| const [localLeaveRecords, setLocalLeaveEntries] = useState(leaveRecords); | |||||
| // leave edit modal related | |||||
| const [leaveEditModalProps, setLeaveEditModalProps] = useState< | |||||
| Partial<LeaveEditModalProps> | |||||
| >({}); | |||||
| const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false); | |||||
| const openLeaveEditModal = useCallback( | |||||
| (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { | |||||
| setLeaveEditModalProps({ | |||||
| defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
| recordDate, | |||||
| isHoliday, | |||||
| onDelete: defaultValues | |||||
| ? async () => { | |||||
| if (!recordDate || !leaveRecords[recordDate]) { | |||||
| return; | |||||
| } | |||||
| const leaveEntriesAtDate = leaveRecords[recordDate]; | |||||
| const newLeaveRecords = { | |||||
| ...leaveRecords, | |||||
| [recordDate!]: leaveEntriesAtDate.filter( | |||||
| (e) => e.id !== defaultValues.id, | |||||
| ), | |||||
| }; | |||||
| const savedLeaveRecords = await saveLeave(newLeaveRecords); | |||||
| setLocalLeaveEntries(savedLeaveRecords); | |||||
| setLeaveEditModalOpen(false); | |||||
| } | |||||
| : undefined, | |||||
| }); | |||||
| setLeaveEditModalOpen(true); | |||||
| }, | |||||
| [leaveRecords], | |||||
| ); | |||||
| const closeLeaveEditModal = useCallback(() => { | |||||
| setLeaveEditModalOpen(false); | |||||
| }, []); | |||||
| // calendar related | |||||
| const holidays = useMemo(() => { | |||||
| return [ | |||||
| ...getPublicHolidaysForNYears(2), | |||||
| ...companyHolidays.map((h) => ({ | |||||
| title: h.name, | |||||
| date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT), | |||||
| extendedProps: { | |||||
| calender: "holiday", | |||||
| }, | |||||
| })), | |||||
| ].map((e) => ({ | |||||
| ...e, | |||||
| backgroundColor: theme.palette.error.main, | |||||
| borderColor: theme.palette.error.main, | |||||
| })); | |||||
| }, [companyHolidays, theme.palette.error.main]); | |||||
| const leaveEntries = useMemo( | |||||
| () => | |||||
| Object.keys(localLeaveRecords).flatMap((date, index) => { | |||||
| return localLeaveRecords[date].map((entry) => ({ | |||||
| id: `${date}-${index}-leave-${entry.id}`, | |||||
| date, | |||||
| title: `${t("{{count}} hour", { | |||||
| ns: "common", | |||||
| count: entry.inputHours || 0, | |||||
| })} (${leaveMap[entry.leaveTypeId]})`, | |||||
| backgroundColor: theme.palette.warning.light, | |||||
| borderColor: theme.palette.warning.light, | |||||
| textColor: theme.palette.text.primary, | |||||
| extendedProps: { | |||||
| calendar: "leaveEntry", | |||||
| entry, | |||||
| }, | |||||
| })); | |||||
| }), | |||||
| [leaveMap, localLeaveRecords, t, theme], | |||||
| ); | |||||
| const timeEntries = useMemo( | |||||
| () => | |||||
| Object.keys(timesheetRecords).flatMap((date, index) => { | |||||
| return timesheetRecords[date].map((entry) => ({ | |||||
| id: `${date}-${index}-time-${entry.id}`, | |||||
| date, | |||||
| title: `${t("{{count}} hour", { | |||||
| ns: "common", | |||||
| count: (entry.inputHours || 0) + (entry.otHours || 0), | |||||
| })} (${ | |||||
| entry.projectId | |||||
| ? projectMap[entry.projectId].code | |||||
| : t("Non-billable task") | |||||
| })`, | |||||
| backgroundColor: theme.palette.info.main, | |||||
| borderColor: theme.palette.info.main, | |||||
| extendedProps: { | |||||
| calendar: "timeEntry", | |||||
| entry, | |||||
| }, | |||||
| })); | |||||
| }), | |||||
| [projectMap, timesheetRecords, t, theme], | |||||
| ); | |||||
| const handleEventClick = useCallback( | |||||
| ({ event }: EventClickArg) => { | |||||
| const dayJsObj = dayjs(event.startStr); | |||||
| const holiday = getHolidayForDate(event.startStr, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| if ( | |||||
| event.extendedProps.calendar === "leaveEntry" && | |||||
| event.extendedProps.entry | |||||
| ) { | |||||
| openLeaveEditModal( | |||||
| event.extendedProps.entry as LeaveEntry, | |||||
| event.startStr, | |||||
| Boolean(isHoliday), | |||||
| ); | |||||
| } | |||||
| }, | |||||
| [companyHolidays, openLeaveEditModal], | |||||
| ); | |||||
| const handleDateClick = useCallback( | |||||
| (e: { dateStr: string; dayEl: HTMLElement }) => { | |||||
| const dayJsObj = dayjs(e.dateStr); | |||||
| const holiday = getHolidayForDate(e.dateStr, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); | |||||
| }, | |||||
| [companyHolidays, openLeaveEditModal], | |||||
| ); | |||||
| const checkTotalHoursForDate = useCallback( | |||||
| (newEntry: LeaveEntry, date?: string) => { | |||||
| if (!date) { | |||||
| throw Error("Invalid date"); | |||||
| } | |||||
| const dayJsObj = dayjs(date); | |||||
| const holiday = getHolidayForDate(date, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const leaves = localLeaveRecords[date] || []; | |||||
| const timesheets = timesheetRecords[date] || []; | |||||
| const leavesWithNewEntry = unionBy( | |||||
| [newEntry as LeaveEntry], | |||||
| leaves, | |||||
| "id", | |||||
| ); | |||||
| const totalHourError = checkTotalHours( | |||||
| timesheets, | |||||
| leavesWithNewEntry, | |||||
| Boolean(isHoliday), | |||||
| isFullTime, | |||||
| ); | |||||
| if (totalHourError) throw Error(totalHourError); | |||||
| }, | |||||
| [companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], | |||||
| ); | |||||
| const handleSaveLeave = useCallback( | |||||
| async (leaveEntry: LeaveEntry, recordDate?: string) => { | |||||
| checkTotalHoursForDate(leaveEntry, recordDate); | |||||
| const leaveEntriesAtDate = leaveRecords[recordDate!] || []; | |||||
| const newLeaveRecords = { | |||||
| ...leaveRecords, | |||||
| [recordDate!]: [ | |||||
| ...leaveEntriesAtDate.filter((e) => e.id !== leaveEntry.id), | |||||
| leaveEntry, | |||||
| ], | |||||
| }; | |||||
| const savedLeaveRecords = await saveLeave(newLeaveRecords); | |||||
| setLocalLeaveEntries(savedLeaveRecords); | |||||
| setLeaveEditModalOpen(false); | |||||
| }, | |||||
| [checkTotalHoursForDate, leaveRecords], | |||||
| ); | |||||
| return ( | |||||
| <Box> | |||||
| <StyledFullCalendar | |||||
| plugins={[dayGridPlugin, interactionPlugin]} | |||||
| initialView="dayGridMonth" | |||||
| buttonText={{ today: t("Today") }} | |||||
| events={[...holidays, ...timeEntries, ...leaveEntries]} | |||||
| eventClick={handleEventClick} | |||||
| dateClick={handleDateClick} | |||||
| /> | |||||
| <LeaveEditModal | |||||
| modalSx={{ maxWidth: 400 }} | |||||
| leaveTypes={leaveTypes} | |||||
| open={leaveEditModalOpen} | |||||
| onClose={closeLeaveEditModal} | |||||
| onSave={handleSaveLeave} | |||||
| {...leaveEditModalProps} | |||||
| /> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default LeaveCalendar; | |||||
| @@ -1,46 +1,16 @@ | |||||
| import React, { useCallback, useEffect, useMemo } from "react"; | |||||
| import useIsMobile from "@/app/utils/useIsMobile"; | |||||
| import React from "react"; | |||||
| import FullscreenModal from "../FullscreenModal"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | |||||
| Card, | Card, | ||||
| CardActions, | |||||
| CardContent, | CardContent, | ||||
| Modal, | Modal, | ||||
| ModalProps, | |||||
| SxProps, | SxProps, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Check, Close } from "@mui/icons-material"; | |||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { | |||||
| RecordLeaveInput, | |||||
| RecordTimesheetInput, | |||||
| saveLeave, | |||||
| } from "@/app/api/timesheets/actions"; | |||||
| import dayjs from "dayjs"; | |||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import LeaveTable from "../LeaveTable"; | |||||
| import { LeaveType } from "@/app/api/timesheets"; | |||||
| import FullscreenModal from "../FullscreenModal"; | |||||
| import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; | |||||
| import useIsMobile from "@/app/utils/useIsMobile"; | |||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { | |||||
| DAILY_NORMAL_MAX_HOURS, | |||||
| TIMESHEET_DAILY_MAX_HOURS, | |||||
| validateLeaveRecord, | |||||
| } from "@/app/api/timesheets/utils"; | |||||
| import ErrorAlert from "../ErrorAlert"; | |||||
| interface Props { | |||||
| isOpen: boolean; | |||||
| onClose: () => void; | |||||
| defaultLeaveRecords?: RecordLeaveInput; | |||||
| leaveTypes: LeaveType[]; | |||||
| timesheetRecords: RecordTimesheetInput; | |||||
| companyHolidays: HolidaysResult[]; | |||||
| } | |||||
| import LeaveCalendar, { Props as LeaveCalendarProps } from "./LeaveCalendar"; | |||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| position: "absolute", | position: "absolute", | ||||
| @@ -52,167 +22,58 @@ const modalSx: SxProps = { | |||||
| maxWidth: 1400, | maxWidth: 1400, | ||||
| }; | }; | ||||
| interface Props extends LeaveCalendarProps { | |||||
| open: boolean; | |||||
| onClose: () => void; | |||||
| } | |||||
| const LeaveModal: React.FC<Props> = ({ | const LeaveModal: React.FC<Props> = ({ | ||||
| isOpen, | |||||
| open, | |||||
| onClose, | onClose, | ||||
| defaultLeaveRecords, | |||||
| timesheetRecords, | |||||
| leaveTypes, | leaveTypes, | ||||
| companyHolidays, | companyHolidays, | ||||
| allProjects, | |||||
| leaveRecords, | |||||
| timesheetRecords, | |||||
| isFullTime, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const isMobile = useIsMobile(); | |||||
| const defaultValues = useMemo(() => { | |||||
| const today = dayjs(); | |||||
| return Array(7) | |||||
| .fill(undefined) | |||||
| .reduce<RecordLeaveInput>((acc, _, index) => { | |||||
| const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
| return { | |||||
| ...acc, | |||||
| [date]: defaultLeaveRecords?.[date] ?? [], | |||||
| }; | |||||
| }, {}); | |||||
| }, [defaultLeaveRecords]); | |||||
| const formProps = useForm<RecordLeaveInput>({ defaultValues }); | |||||
| useEffect(() => { | |||||
| formProps.reset(defaultValues); | |||||
| }, [defaultValues, formProps]); | |||||
| const onSubmit = useCallback<SubmitHandler<RecordLeaveInput>>( | |||||
| async (data) => { | |||||
| const errors = validateLeaveRecord( | |||||
| data, | |||||
| timesheetRecords, | |||||
| companyHolidays, | |||||
| ); | |||||
| if (errors) { | |||||
| Object.keys(errors).forEach((date) => | |||||
| formProps.setError(date, { | |||||
| message: errors[date], | |||||
| }), | |||||
| ); | |||||
| return; | |||||
| } | |||||
| const savedRecords = await saveLeave(data); | |||||
| const today = dayjs(); | |||||
| const newFormValues = Array(7) | |||||
| .fill(undefined) | |||||
| .reduce<RecordLeaveInput>((acc, _, index) => { | |||||
| const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
| return { | |||||
| ...acc, | |||||
| [date]: savedRecords[date] ?? [], | |||||
| }; | |||||
| }, {}); | |||||
| formProps.reset(newFormValues); | |||||
| onClose(); | |||||
| }, | |||||
| [companyHolidays, formProps, onClose, timesheetRecords], | |||||
| ); | |||||
| const onCancel = useCallback(() => { | |||||
| formProps.reset(defaultValues); | |||||
| onClose(); | |||||
| }, [defaultValues, formProps, onClose]); | |||||
| const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (_, reason) => { | |||||
| if (reason !== "backdropClick") { | |||||
| onCancel(); | |||||
| } | |||||
| }, | |||||
| [onCancel], | |||||
| ); | |||||
| 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 title = t("Record leave"); | |||||
| const content = ( | |||||
| <LeaveCalendar | |||||
| isFullTime={isFullTime} | |||||
| leaveTypes={leaveTypes} | |||||
| companyHolidays={companyHolidays} | |||||
| allProjects={allProjects} | |||||
| leaveRecords={leaveRecords} | |||||
| timesheetRecords={timesheetRecords} | |||||
| /> | /> | ||||
| ); | ); | ||||
| 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("Record Leave")} | |||||
| </Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| marginInline: -3, | |||||
| marginBlock: 4, | |||||
| }} | |||||
| > | |||||
| <LeaveTable | |||||
| companyHolidays={companyHolidays} | |||||
| leaveTypes={leaveTypes} | |||||
| timesheetRecords={timesheetRecords} | |||||
| /> | |||||
| </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("Record Leave")} | |||||
| </Typography> | |||||
| <MobileLeaveTable | |||||
| companyHolidays={companyHolidays} | |||||
| leaveTypes={leaveTypes} | |||||
| timesheetRecords={timesheetRecords} | |||||
| errorComponent={errorComponent} | |||||
| /> | |||||
| return isMobile ? ( | |||||
| <FullscreenModal open={open} onClose={onClose} closeModal={onClose}> | |||||
| <Box display="flex" flexDirection="column" gap={2} height="100%"> | |||||
| <Typography variant="h6" flex="none" padding={2}> | |||||
| {title} | |||||
| </Typography> | |||||
| <Box paddingInline={2}>{content}</Box> | |||||
| </Box> | |||||
| </FullscreenModal> | |||||
| ) : ( | |||||
| <Modal open={open} onClose={onClose}> | |||||
| <Card sx={modalSx}> | |||||
| <CardContent> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {title} | |||||
| </Typography> | |||||
| <Box maxHeight={900} overflow="scroll"> | |||||
| {content} | |||||
| </Box> | </Box> | ||||
| </FullscreenModal> | |||||
| )} | |||||
| </FormProvider> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </Modal> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,5 +1,3 @@ | |||||
| import { Grid } from "@mui/material"; | |||||
| interface Props { | interface Props { | ||||
| width?: number; | width?: number; | ||||
| height?: number; | height?: number; | ||||
| @@ -16,7 +14,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
| <g clipPath="url(#a)"> | <g clipPath="url(#a)"> | ||||
| <path id="logo" | <path id="logo" | ||||
| fill="#89ba17" stroke="#89ba17" stroke-width="1" | |||||
| fill="#89ba17" stroke="#89ba17" strokeWidth="1" | |||||
| d="M 98.00,125.00 | d="M 98.00,125.00 | ||||
| C 92.11,126.67 84.23,126.00 78.00,126.00 | C 92.11,126.67 84.23,126.00 78.00,126.00 | ||||
| 68.19,126.00 48.68,126.75 40.00,125.00 | 68.19,126.00 48.68,126.75 40.00,125.00 | ||||
| @@ -66,7 +64,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
| 41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | 41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | ||||
| <path id="word" | <path id="word" | ||||
| fill="#111927" stroke="#111927" stroke-width="1" | |||||
| fill="#111927" stroke="#111927" strokeWidth="1" | |||||
| d="M 273.00,64.00 | d="M 273.00,64.00 | ||||
| C 273.00,64.00 279.96,66.35 279.96,66.35 | C 273.00,64.00 279.96,66.35 279.96,66.35 | ||||
| 283.26,67.45 289.15,67.63 290.83,63.79 | 283.26,67.45 289.15,67.63 290.83,63.79 | ||||
| @@ -67,16 +67,16 @@ import { | |||||
| MAINTAIN_GROUP, | MAINTAIN_GROUP, | ||||
| MAINTAIN_HOLIDAY, | MAINTAIN_HOLIDAY, | ||||
| VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | ||||
| GENERATE_LATE_START_REPORTS, | |||||
| GENERATE_LATE_START_REPORT, | |||||
| GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
| GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
| GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
| GENERATE_COST_AND_EXPENSE_REPORT, | |||||
| GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
| GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
| GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
| GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
| GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
| } from "@/middleware"; | } from "@/middleware"; | ||||
| import { SessionWithAbilities } from "../AppBar/NavigationToggle"; | import { SessionWithAbilities } from "../AppBar/NavigationToggle"; | ||||
| import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
| @@ -190,16 +190,16 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| label: "Analysis Report", | label: "Analysis Report", | ||||
| path: "", | path: "", | ||||
| isHidden: ![ | isHidden: ![ | ||||
| GENERATE_LATE_START_REPORTS, | |||||
| GENERATE_LATE_START_REPORT, | |||||
| GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
| GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
| GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
| GENERATE_COST_AND_EXPENSE_REPORT, | |||||
| GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
| GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
| GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
| GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
| GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
| ].some((ability) => | ].some((ability) => | ||||
| abilities!.includes(ability), | abilities!.includes(ability), | ||||
| ), | ), | ||||
| @@ -208,7 +208,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| icon: <Analytics />, | icon: <Analytics />, | ||||
| label: "Late Start Report", | label: "Late Start Report", | ||||
| path: "/analytics/LateStartReport", | path: "/analytics/LateStartReport", | ||||
| isHidden: ![GENERATE_LATE_START_REPORTS].some((ability) => | |||||
| isHidden: ![GENERATE_LATE_START_REPORT].some((ability) => | |||||
| abilities!.includes(ability), | abilities!.includes(ability), | ||||
| ), | ), | ||||
| }, | }, | ||||
| @@ -232,7 +232,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
| icon: <Analytics />, | icon: <Analytics />, | ||||
| label: "Cost and Expense Report", | label: "Cost and Expense Report", | ||||
| path: "/analytics/CostandExpenseReport", | path: "/analytics/CostandExpenseReport", | ||||
| isHidden: ![GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => | |||||
| isHidden: ![GENERATE_COST_AND_EXPENSE_REPORT].some((ability) => | |||||
| abilities!.includes(ability), | abilities!.includes(ability), | ||||
| ), | ), | ||||
| }, | }, | ||||
| @@ -0,0 +1,196 @@ | |||||
| import { | |||||
| RecordLeaveInput, | |||||
| RecordTimesheetInput, | |||||
| } from "@/app/api/timesheets/actions"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CardActionArea, | |||||
| CardContent, | |||||
| Stack, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import union from "lodash/union"; | |||||
| import { useCallback, useMemo } from "react"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | |||||
| import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import pickBy from "lodash/pickBy"; | |||||
| interface Props { | |||||
| currentMonth: Dayjs; | |||||
| timesheet: RecordTimesheetInput; | |||||
| leaves: RecordLeaveInput; | |||||
| companyHolidays: HolidaysResult[]; | |||||
| onDateSelect: (date: string) => void; | |||||
| } | |||||
| const MonthlySummary: React.FC<Props> = ({ | |||||
| timesheet, | |||||
| leaves, | |||||
| currentMonth, | |||||
| companyHolidays, | |||||
| onDateSelect, | |||||
| }) => { | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("home"); | |||||
| const timesheetForCurrentMonth = useMemo(() => { | |||||
| return pickBy(timesheet, (_, date) => { | |||||
| return currentMonth.isSame(dayjs(date), "month"); | |||||
| }); | |||||
| }, [currentMonth, timesheet]); | |||||
| const leavesForCurrentMonth = useMemo(() => { | |||||
| return pickBy(leaves, (_, date) => { | |||||
| return currentMonth.isSame(dayjs(date), "month"); | |||||
| }); | |||||
| }, [currentMonth, leaves]); | |||||
| const days = useMemo(() => { | |||||
| return union( | |||||
| Object.keys(timesheetForCurrentMonth), | |||||
| Object.keys(leavesForCurrentMonth), | |||||
| ); | |||||
| }, [timesheetForCurrentMonth, leavesForCurrentMonth]).sort(); | |||||
| const makeSelectDate = useCallback( | |||||
| (date: string) => () => { | |||||
| onDateSelect(date); | |||||
| }, | |||||
| [onDateSelect], | |||||
| ); | |||||
| return ( | |||||
| <Stack | |||||
| gap={2} | |||||
| marginBlockEnd={2} | |||||
| minWidth={{ sm: 375 }} | |||||
| maxHeight={{ sm: 500 }} | |||||
| > | |||||
| <Typography variant="overline">{t("Monthly Summary")}</Typography> | |||||
| <Box sx={{ overflowY: "scroll" }} flex={1}> | |||||
| {days.map((day, index) => { | |||||
| const dayJsObj = dayjs(day); | |||||
| const holiday = getHolidayForDate(day, companyHolidays); | |||||
| const isHoliday = | |||||
| holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const ls = leavesForCurrentMonth[day]; | |||||
| const leaveHours = | |||||
| ls?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | |||||
| const ts = timesheetForCurrentMonth[day]; | |||||
| const timesheetNormalHours = | |||||
| ts?.reduce((acc, entry) => acc + (entry.inputHours || 0), 0) || 0; | |||||
| const timesheetOtHours = | |||||
| ts?.reduce((acc, entry) => acc + (entry.otHours || 0), 0) || 0; | |||||
| const timesheetHours = timesheetNormalHours + timesheetOtHours; | |||||
| return ( | |||||
| <Card | |||||
| key={`${day}-${index}`} | |||||
| sx={{ marginBlockEnd: 2, marginInline: 2 }} | |||||
| > | |||||
| <CardActionArea onClick={makeSelectDate(day)}> | |||||
| <CardContent sx={{ padding: 3 }}> | |||||
| <Typography | |||||
| variant="overline" | |||||
| component="div" | |||||
| sx={{ | |||||
| color: isHoliday ? "error.main" : undefined, | |||||
| }} | |||||
| > | |||||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | |||||
| {holiday && ( | |||||
| <Typography | |||||
| marginInlineStart={1} | |||||
| variant="caption" | |||||
| >{`(${holiday.title})`}</Typography> | |||||
| )} | |||||
| </Typography> | |||||
| <Stack spacing={1}> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| flexWrap: "wrap", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="body2"> | |||||
| {t("Timesheet Hours")} | |||||
| </Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(timesheetHours)} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| flexWrap: "wrap", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="body2"> | |||||
| {t("Leave Hours")} | |||||
| </Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(leaveHours)} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| flexWrap: "wrap", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="body2"> | |||||
| {t("Daily Total Hours")} | |||||
| </Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(timesheetHours + leaveHours)} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </CardActionArea> | |||||
| </Card> | |||||
| ); | |||||
| })} | |||||
| </Box> | |||||
| <Typography variant="overline"> | |||||
| {`${t("Total Monthly Work Hours")}: ${manhourFormatter.format( | |||||
| Object.values(timesheetForCurrentMonth) | |||||
| .flatMap((entries) => entries) | |||||
| .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) | |||||
| .reduce((acc, cur) => { | |||||
| return acc + cur; | |||||
| }, 0), | |||||
| )}`} | |||||
| </Typography> | |||||
| <Typography variant="overline"> | |||||
| {`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format( | |||||
| Object.values(leavesForCurrentMonth) | |||||
| .flatMap((entries) => entries) | |||||
| .map((entry) => entry.inputHours) | |||||
| .reduce((acc, cur) => { | |||||
| return acc + cur; | |||||
| }, 0), | |||||
| )}`} | |||||
| </Typography> | |||||
| </Stack> | |||||
| ); | |||||
| }; | |||||
| export default MonthlySummary; | |||||
| @@ -26,6 +26,7 @@ export interface Props { | |||||
| timesheet: RecordTimesheetInput; | timesheet: RecordTimesheetInput; | ||||
| leaves: RecordLeaveInput; | leaves: RecordLeaveInput; | ||||
| onDateSelect: (date: string) => void; | onDateSelect: (date: string) => void; | ||||
| onMonthChange: (day: Dayjs) => void; | |||||
| } | } | ||||
| const getColor = ( | const getColor = ( | ||||
| @@ -72,6 +73,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||||
| timesheet, | timesheet, | ||||
| leaves, | leaves, | ||||
| onDateSelect, | onDateSelect, | ||||
| onMonthChange, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| i18n: { language }, | i18n: { language }, | ||||
| @@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||||
| > | > | ||||
| <DateCalendar | <DateCalendar | ||||
| onChange={onChange} | onChange={onChange} | ||||
| onMonthChange={onMonthChange} | |||||
| disableFuture | disableFuture | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| slots={{ day: EntryDay as any }} | slots={{ day: EntryDay as any }} | ||||
| @@ -20,12 +20,17 @@ import { ProjectWithTasks } from "@/app/api/projects"; | |||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import useIsMobile from "@/app/utils/useIsMobile"; | import useIsMobile from "@/app/utils/useIsMobile"; | ||||
| import FullscreenModal from "../FullscreenModal"; | import FullscreenModal from "../FullscreenModal"; | ||||
| import MonthlySummary from "./MonthlySummary"; | |||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import dayjs from "dayjs"; | |||||
| interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> { | |||||
| interface Props | |||||
| extends Omit<PastEntryCalendarProps, "onDateSelect" | "onMonthChange"> { | |||||
| open: boolean; | open: boolean; | ||||
| handleClose: () => void; | handleClose: () => void; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const Indicator = styled(Box)(() => ({ | const Indicator = styled(Box)(() => ({ | ||||
| @@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const [selectedDate, setSelectedDate] = useState(""); | const [selectedDate, setSelectedDate] = useState(""); | ||||
| const [currentMonth, setMonthChange] = useState(dayjs()); | |||||
| const clearDate = useCallback(() => { | const clearDate = useCallback(() => { | ||||
| setSelectedDate(""); | setSelectedDate(""); | ||||
| @@ -54,40 +60,52 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
| handleClose(); | handleClose(); | ||||
| }, [handleClose]); | }, [handleClose]); | ||||
| const content = selectedDate ? ( | |||||
| <> | |||||
| <PastEntryList | |||||
| date={selectedDate} | |||||
| timesheet={timesheet} | |||||
| leaves={leaves} | |||||
| allProjects={allProjects} | |||||
| leaveTypes={leaveTypes} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| <Stack marginBlockEnd={2}> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "info.light" }} /> | |||||
| <Typography variant="caption">{t("Has timesheet entry")}</Typography> | |||||
| </Box> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "warning.light" }} /> | |||||
| <Typography variant="caption">{t("Has leave entry")}</Typography> | |||||
| </Box> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "success.light" }} /> | |||||
| <Typography variant="caption"> | |||||
| {t("Has both timesheet and leave entry")} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Stack> | |||||
| <PastEntryCalendar | |||||
| timesheet={timesheet} | |||||
| leaves={leaves} | |||||
| onDateSelect={setSelectedDate} | |||||
| /> | |||||
| </> | |||||
| const content = ( | |||||
| <Box sx={{ display: "flex", flexDirection: { xs: "column", sm: "row" } }}> | |||||
| <Box> | |||||
| <Stack marginBlockEnd={2}> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "info.light" }} /> | |||||
| <Typography variant="caption"> | |||||
| {t("Has timesheet entry")} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "warning.light" }} /> | |||||
| <Typography variant="caption">{t("Has leave entry")}</Typography> | |||||
| </Box> | |||||
| <Box display="flex" alignItems="center" gap={1}> | |||||
| <Indicator sx={{ backgroundColor: "success.light" }} /> | |||||
| <Typography variant="caption"> | |||||
| {t("Has both timesheet and leave entry")} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Stack> | |||||
| <PastEntryCalendar | |||||
| timesheet={timesheet} | |||||
| leaves={leaves} | |||||
| onDateSelect={setSelectedDate} | |||||
| onMonthChange={setMonthChange} | |||||
| /> | |||||
| </Box> | |||||
| {selectedDate ? ( | |||||
| <PastEntryList | |||||
| date={selectedDate} | |||||
| timesheet={timesheet} | |||||
| leaves={leaves} | |||||
| allProjects={allProjects} | |||||
| leaveTypes={leaveTypes} | |||||
| /> | |||||
| ) : ( | |||||
| <MonthlySummary | |||||
| currentMonth={currentMonth} | |||||
| timesheet={timesheet} | |||||
| leaves={leaves} | |||||
| companyHolidays={[]} | |||||
| onDateSelect={setSelectedDate} | |||||
| /> | |||||
| )} | |||||
| </Box> | |||||
| ); | ); | ||||
| const isMobile = useIsMobile(); | const isMobile = useIsMobile(); | ||||
| @@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
| startIcon={<ArrowBack />} | startIcon={<ArrowBack />} | ||||
| onClick={clearDate} | onClick={clearDate} | ||||
| > | > | ||||
| {t("Back")} | |||||
| {t("Back to Monthly Summary")} | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| </FullscreenModal> | </FullscreenModal> | ||||
| ) : ( | ) : ( | ||||
| <Dialog onClose={onClose} open={open}> | |||||
| <Dialog onClose={onClose} open={open} maxWidth="md"> | |||||
| <DialogTitle>{t("Past Entries")}</DialogTitle> | <DialogTitle>{t("Past Entries")}</DialogTitle> | ||||
| <DialogContent>{content}</DialogContent> | <DialogContent>{content}</DialogContent> | ||||
| {selectedDate && ( | {selectedDate && ( | ||||
| @@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
| startIcon={<ArrowBack />} | startIcon={<ArrowBack />} | ||||
| onClick={clearDate} | onClick={clearDate} | ||||
| > | > | ||||
| {t("Back")} | |||||
| {t("Back to Monthly Summary")} | |||||
| </Button> | </Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| )} | )} | ||||
| @@ -57,7 +57,12 @@ const PastEntryList: React.FC<Props> = ({ | |||||
| const dayJsObj = dayjs(date); | const dayJsObj = dayjs(date); | ||||
| return ( | return ( | ||||
| <Stack gap={2} marginBlockEnd={2} minWidth={{ sm: 375 }}> | |||||
| <Stack | |||||
| gap={2} | |||||
| marginBlockEnd={2} | |||||
| minWidth={{ sm: 375 }} | |||||
| maxHeight={{ sm: 500 }} | |||||
| > | |||||
| <Typography | <Typography | ||||
| variant="overline" | variant="overline" | ||||
| color={dayJsObj.day() === 0 ? "error.main" : undefined} | color={dayJsObj.day() === 0 ? "error.main" : undefined} | ||||
| @@ -94,17 +99,25 @@ const PastEntryList: React.FC<Props> = ({ | |||||
| leaveTypeMap={leaveTypeMap} | leaveTypeMap={leaveTypeMap} | ||||
| /> | /> | ||||
| ))} | ))} | ||||
| <Typography | |||||
| variant="overline" | |||||
| > | |||||
| {t("Total Work Hours")}: {manhourFormatter.format(timeEntries.map(entry => (entry.inputHours ?? 0) + (entry.otHours ?? 0)).reduce((acc, cur) => { return acc + cur }, 0))} | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="overline" | |||||
| > | |||||
| {t("Total Leave Hours")}: {manhourFormatter.format(leaveEntries.map(entry => entry.inputHours).reduce((acc, cur) => { return acc + cur }, 0))} | |||||
| </Typography> | |||||
| </Box> | </Box> | ||||
| <Typography variant="overline"> | |||||
| {`${t("Total Work Hours")}: ${manhourFormatter.format( | |||||
| timeEntries | |||||
| .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) | |||||
| .reduce((acc, cur) => { | |||||
| return acc + cur; | |||||
| }, 0), | |||||
| )}`} | |||||
| </Typography> | |||||
| <Typography variant="overline"> | |||||
| {`${t("Total Leave Hours")}: ${manhourFormatter.format( | |||||
| leaveEntries | |||||
| .map((entry) => entry.inputHours) | |||||
| .reduce((acc, cur) => { | |||||
| return acc + cur; | |||||
| }, 0), | |||||
| )}`} | |||||
| </Typography> | |||||
| </Stack> | </Stack> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -409,7 +409,7 @@ const ProgressByClient: React.FC<Props> = () => { | |||||
| const spentManhours = chartProjectSpentHour[dataPointIndex]; | const spentManhours = chartProjectSpentHour[dataPointIndex]; | ||||
| const value = series[seriesIndex][dataPointIndex]; | const value = series[seriesIndex][dataPointIndex]; | ||||
| const tooltipContent = ` | const tooltipContent = ` | ||||
| <div style="width: 250px;"> | |||||
| <div style="width: auto;"> | |||||
| <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | ||||
| <br> | <br> | ||||
| Budget Manhours: ${budgetManhours} hours | Budget Manhours: ${budgetManhours} hours | ||||
| @@ -492,7 +492,7 @@ const ProgressByTeam: React.FC = () => { | |||||
| const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; | const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; | ||||
| const value = series[seriesIndex][dataPointIndex]; | const value = series[seriesIndex][dataPointIndex]; | ||||
| const tooltipContent = ` | const tooltipContent = ` | ||||
| <div style="width: 100%;"> | |||||
| <div style="width: auto;"> | |||||
| <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | ||||
| <br> | <br> | ||||
| Budget Manhours: ${budgetManhours} hours | Budget Manhours: ${budgetManhours} hours | ||||
| @@ -954,7 +954,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
| style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
| > | > | ||||
| Accounts Receivable | |||||
| Remaining Budget | |||||
| </div> | </div> | ||||
| <div | <div | ||||
| className="text-lg font-medium ml-5 mb-2" | className="text-lg font-medium ml-5 mb-2" | ||||
| @@ -362,7 +362,7 @@ const StaffUtilization: React.FC<Props> = ({ abilities, staff }) => { | |||||
| const startCount = weeklyPlanned[i].startCount | const startCount = weeklyPlanned[i].startCount | ||||
| const endCount = weeklyPlanned[i].endCount | const endCount = weeklyPlanned[i].endCount | ||||
| for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { | for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { | ||||
| if (j >= startCount && j < endCount) { | |||||
| if (j >= startCount && j <= endCount) { | |||||
| weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) | weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) | ||||
| } else { | } else { | ||||
| weeklyPlannedSubList.push(0) | weeklyPlannedSubList.push(0) | ||||
| @@ -503,7 +503,8 @@ const StaffUtilization: React.FC<Props> = ({ abilities, staff }) => { | |||||
| const fetchMonthlyUnsubmittedData = async () => { | const fetchMonthlyUnsubmittedData = async () => { | ||||
| const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); | |||||
| const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.startOf('month').format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); | |||||
| const result = [] | const result = [] | ||||
| const staffList = [] | const staffList = [] | ||||
| var maxValue = 5 | var maxValue = 5 | ||||
| @@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| }, {}); | }, {}); | ||||
| }, [assignedProjects]); | }, [assignedProjects]); | ||||
| const { getValues, setValue, clearErrors } = | |||||
| const { getValues, setValue, clearErrors, setError } = | |||||
| useFormContext<RecordTimeLeaveInput>(); | useFormContext<RecordTimeLeaveInput>(); | ||||
| const currentEntries = getValues(day); | const currentEntries = getValues(day); | ||||
| @@ -393,7 +393,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| params.row._error?.[ | params.row._error?.[ | ||||
| params.field as keyof Omit<TimeLeaveEntry, "type"> | params.field as keyof Omit<TimeLeaveEntry, "type"> | ||||
| ]; | ]; | ||||
| const content = <GridEditInputCell {...params} />; | |||||
| const content = ( | |||||
| <GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||||
| ); | |||||
| return errorMessage ? ( | return errorMessage ? ( | ||||
| <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | ||||
| <Box width="100%">{content}</Box> | <Box width="100%">{content}</Box> | ||||
| @@ -423,7 +425,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| params.row._error?.[ | params.row._error?.[ | ||||
| params.field as keyof Omit<TimeLeaveEntry, "type"> | params.field as keyof Omit<TimeLeaveEntry, "type"> | ||||
| ]; | ]; | ||||
| const content = <GridEditInputCell {...params} />; | |||||
| const content = ( | |||||
| <GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||||
| ); | |||||
| return errorMessage ? ( | return errorMessage ? ( | ||||
| <Tooltip title={t(errorMessage)}> | <Tooltip title={t(errorMessage)}> | ||||
| <Box width="100%">{content}</Box> | <Box width="100%">{content}</Box> | ||||
| @@ -486,8 +490,13 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
| .filter((e): e is TimeLeaveEntry => Boolean(e)); | .filter((e): e is TimeLeaveEntry => Boolean(e)); | ||||
| setValue(day, newEntries); | setValue(day, newEntries); | ||||
| clearErrors(day); | |||||
| }, [getValues, entries, setValue, day, clearErrors]); | |||||
| if (entries.some((e) => e._isNew)) { | |||||
| setError(day, { message: "There are some unsaved entries." }); | |||||
| } else { | |||||
| clearErrors(day); | |||||
| } | |||||
| }, [getValues, entries, setValue, day, clearErrors, setError]); | |||||
| const hasOutOfPlannedStages = entries.some( | const hasOutOfPlannedStages = entries.some( | ||||
| (entry) => entry._isPlanned !== undefined && !entry._isPlanned, | (entry) => entry._isPlanned !== undefined && !entry._isPlanned, | ||||
| @@ -49,6 +49,7 @@ interface Props { | |||||
| companyHolidays: HolidaysResult[]; | companyHolidays: HolidaysResult[]; | ||||
| fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| isFullTime: boolean; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -71,6 +72,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| companyHolidays, | companyHolidays, | ||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| leaveTypes, | leaveTypes, | ||||
| isFullTime | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -106,7 +108,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | ||||
| async (data) => { | async (data) => { | ||||
| const errors = validateTimeLeaveRecord(data, companyHolidays); | |||||
| const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime); | |||||
| if (errors) { | if (errors) { | ||||
| Object.keys(errors).forEach((date) => | Object.keys(errors).forEach((date) => | ||||
| formProps.setError(date, { | formProps.setError(date, { | ||||
| @@ -131,7 +133,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| formProps.reset(newFormValues); | formProps.reset(newFormValues); | ||||
| onClose(); | onClose(); | ||||
| }, | }, | ||||
| [companyHolidays, formProps, onClose], | |||||
| [companyHolidays, formProps, onClose, isFullTime], | |||||
| ); | ); | ||||
| const onCancel = useCallback(() => { | const onCancel = useCallback(() => { | ||||
| @@ -347,6 +347,10 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| if (!date) { | if (!date) { | ||||
| throw Error("Invalid date"); | throw Error("Invalid date"); | ||||
| } | } | ||||
| const dayJsObj = dayjs(date); | |||||
| const holiday = getHolidayForDate(date, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const intStaffId = parseInt(selectedStaff.id); | const intStaffId = parseInt(selectedStaff.id); | ||||
| const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | ||||
| const timesheets = | const timesheets = | ||||
| @@ -360,7 +364,11 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| leaves, | leaves, | ||||
| "id", | "id", | ||||
| ); | ); | ||||
| totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||||
| totalHourError = checkTotalHours( | |||||
| timesheets, | |||||
| leavesWithNewEntry, | |||||
| Boolean(isHoliday), | |||||
| ); | |||||
| } else { | } else { | ||||
| // newEntry is a timesheet entry | // newEntry is a timesheet entry | ||||
| const timesheetsWithNewEntry = unionBy( | const timesheetsWithNewEntry = unionBy( | ||||
| @@ -368,11 +376,15 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| timesheets, | timesheets, | ||||
| "id", | "id", | ||||
| ); | ); | ||||
| totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves); | |||||
| totalHourError = checkTotalHours( | |||||
| timesheetsWithNewEntry, | |||||
| leaves, | |||||
| Boolean(isHoliday), | |||||
| ); | |||||
| } | } | ||||
| if (totalHourError) throw Error(totalHourError); | if (totalHourError) throw Error(totalHourError); | ||||
| }, | }, | ||||
| [localTeamLeaves, localTeamTimesheets, selectedStaff.id], | |||||
| [localTeamLeaves, localTeamTimesheets, selectedStaff, companyHolidays], | |||||
| ); | ); | ||||
| const handleSave = useCallback( | const handleSave = useCallback( | ||||
| @@ -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} | ||||
| @@ -1,82 +1,171 @@ | |||||
| import React from "react"; | |||||
| import { Box, Card, CardContent, Grid, Typography } from "@mui/material"; | |||||
| import React, { useCallback, useState } from "react"; | |||||
| import { | |||||
| Box, | |||||
| Card, | |||||
| CardContent, | |||||
| Grid, | |||||
| ToggleButton, | |||||
| ToggleButtonGroup, | |||||
| ToggleButtonProps, | |||||
| Tooltip, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | import { manhourFormatter } from "@/app/utils/formatUtil"; | ||||
| import { AssignedProject } from "@/app/api/projects"; | import { AssignedProject } from "@/app/api/projects"; | ||||
| import { TableRows, ViewModule, TableChart } from "@mui/icons-material"; | |||||
| import ProjectTable from "./ProjectTable"; | |||||
| interface Props { | |||||
| export interface Props { | |||||
| projects: AssignedProject[]; | projects: AssignedProject[]; | ||||
| maintainNormalStaffWorkspaceAbility?: boolean; | maintainNormalStaffWorkspaceAbility?: boolean; | ||||
| maintainManagementStaffWorkspaceAbility?: boolean; | maintainManagementStaffWorkspaceAbility?: boolean; | ||||
| } | } | ||||
| const ProjectGrid: React.FC<Props> = ({ projects, maintainNormalStaffWorkspaceAbility, maintainManagementStaffWorkspaceAbility }) => { | |||||
| const ProjectGrid: React.FC<Props> = ({ | |||||
| projects, | |||||
| maintainNormalStaffWorkspaceAbility, | |||||
| maintainManagementStaffWorkspaceAbility, | |||||
| }) => { | |||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const [view, setView] = useState<"grid" | "list" | "table">("grid"); | |||||
| const handleViewChange = useCallback< | |||||
| NonNullable<ToggleButtonProps["onChange"]> | |||||
| >((e, value) => { | |||||
| if (value) { | |||||
| setView(value); | |||||
| } | |||||
| }, []); | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Grid container columns={{ xs: 4, sm: 8, md: 12, lg: 16 }} spacing={2}> | |||||
| {projects.map((project, idx) => ( | |||||
| <Grid key={`${project.code}${idx}`} item xs={4}> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Typography variant="overline">{project.code}</Typography> | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| overflow: "hidden", | |||||
| textOverflow: "ellipsis", | |||||
| whiteSpace: "nowrap", | |||||
| marginBlockEnd: 3, | |||||
| }} | |||||
| > | |||||
| {project.name} | |||||
| </Typography> | |||||
| {/* Hours Spent */} | |||||
| {(Boolean(maintainNormalStaffWorkspaceAbility) || Boolean(maintainManagementStaffWorkspaceAbility)) && <><Typography variant="subtitle2">{t("Hours Spent:")}</Typography> | |||||
| <Box | |||||
| <ToggleButtonGroup | |||||
| color="primary" | |||||
| value={view} | |||||
| exclusive | |||||
| onChange={handleViewChange} | |||||
| sx={{ marginBlockEnd: 2 }} | |||||
| > | |||||
| <ToggleButton value="grid"> | |||||
| <Tooltip title={t("Grid view")}> | |||||
| <ViewModule /> | |||||
| </Tooltip> | |||||
| </ToggleButton> | |||||
| <ToggleButton value="list"> | |||||
| <Tooltip title={t("List view")}> | |||||
| <TableRows /> | |||||
| </Tooltip> | |||||
| </ToggleButton> | |||||
| <ToggleButton value="table"> | |||||
| <Tooltip title={t("Table view")}> | |||||
| <TableChart /> | |||||
| </Tooltip> | |||||
| </ToggleButton> | |||||
| </ToggleButtonGroup> | |||||
| {view === "table" ? ( | |||||
| <ProjectTable | |||||
| projects={projects} | |||||
| maintainManagementStaffWorkspaceAbility={ | |||||
| maintainManagementStaffWorkspaceAbility | |||||
| } | |||||
| maintainNormalStaffWorkspaceAbility={ | |||||
| maintainNormalStaffWorkspaceAbility | |||||
| } | |||||
| /> | |||||
| ) : ( | |||||
| <Grid | |||||
| container | |||||
| columns={view === "list" ? 4 : { xs: 4, sm: 8, md: 12, lg: 16 }} | |||||
| spacing={2} | |||||
| alignItems="stretch" | |||||
| > | |||||
| {projects.map((project, idx) => ( | |||||
| <Grid key={`${project.code}${idx}`} item xs={4}> | |||||
| <Card sx={{ height: "100%" }}> | |||||
| <CardContent | |||||
| sx={{ | sx={{ | ||||
| display: "flex", | display: "flex", | ||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| flexDirection: "column", | |||||
| height: "100%", | |||||
| }} | }} | ||||
| > | > | ||||
| <Typography variant="caption">{t("Normal")}</Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpent : project.currentStaffHoursSpent)} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="caption">{t("Others")}</Typography> | |||||
| <Typography>{`${manhourFormatter.format( | |||||
| Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpentOther : project.currentStaffHoursSpentOther, | |||||
| )}`}</Typography> | |||||
| </Box></>} | |||||
| {/* Hours Allocated */} | |||||
| {Boolean(maintainManagementStaffWorkspaceAbility) && <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}> | |||||
| {t("Hours Allocated:")} | |||||
| </Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(project.hoursAllocated)} | |||||
| <Typography variant="overline">{project.code}</Typography> | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| marginBlockEnd: 3, | |||||
| }} | |||||
| > | |||||
| {project.name} | |||||
| </Typography> | </Typography> | ||||
| </Box>} | |||||
| </CardContent> | |||||
| </Card> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| {/* Spacer */} | |||||
| <Box sx={{ flex: 1 }} /> | |||||
| {/* Hours Spent */} | |||||
| {(Boolean(maintainNormalStaffWorkspaceAbility) || | |||||
| Boolean(maintainManagementStaffWorkspaceAbility)) && ( | |||||
| <> | |||||
| <Typography variant="subtitle2"> | |||||
| {t("Hours Spent:")} | |||||
| </Typography> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="caption">{t("Normal")}</Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format( | |||||
| Boolean(maintainManagementStaffWorkspaceAbility) | |||||
| ? project.hoursSpent | |||||
| : project.currentStaffHoursSpent, | |||||
| )} | |||||
| </Typography> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography variant="caption">{t("Others")}</Typography> | |||||
| <Typography>{`${manhourFormatter.format( | |||||
| Boolean(maintainManagementStaffWorkspaceAbility) | |||||
| ? project.hoursSpentOther | |||||
| : project.currentStaffHoursSpentOther, | |||||
| )}`}</Typography> | |||||
| </Box> | |||||
| </> | |||||
| )} | |||||
| {/* Hours Allocated */} | |||||
| {Boolean(maintainManagementStaffWorkspaceAbility) && ( | |||||
| <Box | |||||
| sx={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "baseline", | |||||
| }} | |||||
| > | |||||
| <Typography | |||||
| variant="subtitle2" | |||||
| sx={{ marginBlockStart: 2 }} | |||||
| > | |||||
| {t("Hours Allocated:")} | |||||
| </Typography> | |||||
| <Typography> | |||||
| {manhourFormatter.format(project.hoursAllocated)} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| </CardContent> | |||||
| </Card> | |||||
| </Grid> | |||||
| ))} | |||||
| </Grid> | |||||
| )} | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -0,0 +1,106 @@ | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Props } from "./ProjectGrid"; | |||||
| import { | |||||
| Paper, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| } from "@mui/material"; | |||||
| import { useMemo } from "react"; | |||||
| import { AssignedProject } from "@/app/api/projects"; | |||||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | |||||
| interface Column { | |||||
| name: keyof AssignedProject; | |||||
| label: string; | |||||
| } | |||||
| const hourColumns: Array<keyof AssignedProject> = [ | |||||
| "currentStaffHoursSpent", | |||||
| "currentStaffHoursSpentOther", | |||||
| "hoursAllocated", | |||||
| "hoursSpent", | |||||
| "hoursSpentOther", | |||||
| ]; | |||||
| const ProjectTable: React.FC<Props> = ({ | |||||
| projects, | |||||
| maintainManagementStaffWorkspaceAbility, | |||||
| maintainNormalStaffWorkspaceAbility, | |||||
| }) => { | |||||
| const { t } = useTranslation("home"); | |||||
| const columns = useMemo<Column[]>(() => { | |||||
| return [ | |||||
| { name: "code", label: t("Project Code") }, | |||||
| { name: "name", label: t("Project Name") }, | |||||
| ...(maintainManagementStaffWorkspaceAbility || | |||||
| maintainNormalStaffWorkspaceAbility | |||||
| ? maintainManagementStaffWorkspaceAbility | |||||
| ? ([ | |||||
| { name: "hoursSpent", label: t("Total Normal Hours Spent") }, | |||||
| { name: "hoursSpentOther", label: t("Total Other Hours Spent") }, | |||||
| { name: "hoursAllocated", label: t("Hours Allocated") }, | |||||
| ] satisfies Column[]) | |||||
| : ([ | |||||
| { | |||||
| name: "currentStaffHoursSpent", | |||||
| label: t("Normal Hours Spent"), | |||||
| }, | |||||
| { | |||||
| name: "currentStaffHoursSpentOther", | |||||
| label: t("Other Hours Spent"), | |||||
| }, | |||||
| ] satisfies Column[]) | |||||
| : []), | |||||
| ]; | |||||
| }, [ | |||||
| maintainManagementStaffWorkspaceAbility, | |||||
| maintainNormalStaffWorkspaceAbility, | |||||
| t, | |||||
| ]); | |||||
| return ( | |||||
| <Paper sx={{ overflow: "hidden" }}> | |||||
| <TableContainer> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| {columns.map((column, idx) => ( | |||||
| <TableCell key={`${column.name.toString()}-${idx}`}> | |||||
| {column.label} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {projects.map((project) => { | |||||
| return ( | |||||
| <TableRow hover tabIndex={-1} key={project.id}> | |||||
| {columns.map((column, idx) => { | |||||
| const columnName = column.name; | |||||
| const needsFormatting = hourColumns.includes(columnName); | |||||
| return ( | |||||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||||
| {needsFormatting | |||||
| ? manhourFormatter.format( | |||||
| project[columnName] as number, | |||||
| ) | |||||
| : project[columnName]?.toString()} | |||||
| </TableCell> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ); | |||||
| })} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Paper> | |||||
| ); | |||||
| }; | |||||
| export default ProjectTable; | |||||
| @@ -4,7 +4,12 @@ import React, { useCallback, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import { CalendarMonth, EditCalendar, MoreTime } from "@mui/icons-material"; | |||||
| import { | |||||
| CalendarMonth, | |||||
| EditCalendar, | |||||
| Luggage, | |||||
| MoreTime, | |||||
| } from "@mui/icons-material"; | |||||
| import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | ||||
| import AssignedProjects from "./AssignedProjects"; | import AssignedProjects from "./AssignedProjects"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| @@ -19,6 +24,7 @@ import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal" | |||||
| import { HolidaysResult } from "@/app/api/holidays"; | import { HolidaysResult } from "@/app/api/holidays"; | ||||
| import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | ||||
| import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | ||||
| import LeaveModal from "../LeaveModal"; | |||||
| export interface Props { | export interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| @@ -32,6 +38,7 @@ export interface Props { | |||||
| fastEntryEnabled: boolean; | fastEntryEnabled: boolean; | ||||
| maintainNormalStaffWorkspaceAbility: boolean; | maintainNormalStaffWorkspaceAbility: boolean; | ||||
| maintainManagementStaffWorkspaceAbility: boolean; | maintainManagementStaffWorkspaceAbility: boolean; | ||||
| isFullTime: boolean; | |||||
| } | } | ||||
| const menuItemSx: SxProps = { | const menuItemSx: SxProps = { | ||||
| @@ -51,10 +58,12 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| fastEntryEnabled, | fastEntryEnabled, | ||||
| maintainNormalStaffWorkspaceAbility, | maintainNormalStaffWorkspaceAbility, | ||||
| maintainManagementStaffWorkspaceAbility, | maintainManagementStaffWorkspaceAbility, | ||||
| isFullTime, | |||||
| }) => { | }) => { | ||||
| const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
| const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); | const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); | ||||
| const [isLeaveCalendarVisible, setLeaveCalendarVisible] = useState(false); | |||||
| const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | ||||
| const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | ||||
| useState(false); | useState(false); | ||||
| @@ -81,6 +90,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| setTimeLeaveModalVisible(false); | setTimeLeaveModalVisible(false); | ||||
| }, []); | }, []); | ||||
| const handleOpenLeaveCalendarButton = useCallback(() => { | |||||
| setAnchorEl(null); | |||||
| setLeaveCalendarVisible(true); | |||||
| }, []); | |||||
| const handleCloseLeaveCalendarButton = useCallback(() => { | |||||
| setLeaveCalendarVisible(false); | |||||
| }, []); | |||||
| const handlePastEventClick = useCallback(() => { | const handlePastEventClick = useCallback(() => { | ||||
| setAnchorEl(null); | setAnchorEl(null); | ||||
| setPastEventModalVisible(true); | setPastEventModalVisible(true); | ||||
| @@ -136,6 +154,10 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| <MoreTime /> | <MoreTime /> | ||||
| {t("Enter Timesheet")} | {t("Enter Timesheet")} | ||||
| </MenuItem> | </MenuItem> | ||||
| <MenuItem onClick={handleOpenLeaveCalendarButton} sx={menuItemSx}> | |||||
| <Luggage /> | |||||
| {t("Record Leave")} | |||||
| </MenuItem> | |||||
| <MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | <MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | ||||
| <CalendarMonth /> | <CalendarMonth /> | ||||
| {t("View Past Entries")} | {t("View Past Entries")} | ||||
| @@ -154,6 +176,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| leaves={defaultLeaveRecords} | leaves={defaultLeaveRecords} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| companyHolidays={holidays} | |||||
| /> | /> | ||||
| <TimeLeaveModal | <TimeLeaveModal | ||||
| fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
| @@ -165,6 +188,17 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
| timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
| leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
| isFullTime={isFullTime} | |||||
| /> | |||||
| <LeaveModal | |||||
| open={isLeaveCalendarVisible} | |||||
| onClose={handleCloseLeaveCalendarButton} | |||||
| leaveTypes={leaveTypes} | |||||
| companyHolidays={holidays} | |||||
| allProjects={allProjects} | |||||
| leaveRecords={defaultLeaveRecords} | |||||
| timesheetRecords={defaultTimesheets} | |||||
| isFullTime={isFullTime} | |||||
| /> | /> | ||||
| {assignedProjects.length > 0 ? ( | {assignedProjects.length > 0 ? ( | ||||
| <AssignedProjects | <AssignedProjects | ||||
| @@ -11,8 +11,12 @@ import { | |||||
| fetchTimesheets, | fetchTimesheets, | ||||
| } from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
| import { fetchHolidays } from "@/app/api/holidays"; | import { fetchHolidays } from "@/app/api/holidays"; | ||||
| import { getUserAbilities } from "@/app/utils/commonUtil"; | |||||
| import { MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE } from "@/middleware"; | |||||
| import { getUserAbilities, getUserStaff } from "@/app/utils/commonUtil"; | |||||
| import { | |||||
| MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
| MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
| MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
| } from "@/middleware"; | |||||
| const UserWorkspaceWrapper: React.FC = async () => { | const UserWorkspaceWrapper: React.FC = async () => { | ||||
| const [ | const [ | ||||
| @@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| leaveTypes, | leaveTypes, | ||||
| holidays, | holidays, | ||||
| abilities, | abilities, | ||||
| userStaff, | |||||
| ] = await Promise.all([ | ] = await Promise.all([ | ||||
| fetchTeamMemberLeaves(), | fetchTeamMemberLeaves(), | ||||
| fetchTeamMemberTimesheets(), | fetchTeamMemberTimesheets(), | ||||
| @@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| fetchLeaves(), | fetchLeaves(), | ||||
| fetchLeaveTypes(), | fetchLeaveTypes(), | ||||
| fetchHolidays(), | fetchHolidays(), | ||||
| getUserAbilities() | |||||
| getUserAbilities(), | |||||
| getUserStaff(), | |||||
| ]); | ]); | ||||
| const fastEntryEnabled = abilities.includes(MAINTAIN_TIMESHEET_FAST_TIME_ENTRY) | |||||
| const maintainNormalStaffWorkspaceAbility = abilities.includes(MAINTAIN_NORMAL_STAFF_WORKSPACE) | |||||
| const maintainManagementStaffWorkspaceAbility = abilities.includes(MAINTAIN_MANAGEMENT_STAFF_WORKSPACE) | |||||
| const fastEntryEnabled = abilities.includes( | |||||
| MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
| ); | |||||
| const maintainNormalStaffWorkspaceAbility = abilities.includes( | |||||
| MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
| ); | |||||
| const maintainManagementStaffWorkspaceAbility = abilities.includes( | |||||
| MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
| ); | |||||
| const isFullTime = userStaff?.employType === "FT"; | |||||
| return ( | return ( | ||||
| <UserWorkspacePage | <UserWorkspacePage | ||||
| isFullTime={isFullTime} | |||||
| teamLeaves={teamLeaves} | teamLeaves={teamLeaves} | ||||
| teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| @@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
| // Change to access check | // Change to access check | ||||
| fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
| maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | ||||
| maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility} | |||||
| maintainManagementStaffWorkspaceAbility={ | |||||
| maintainManagementStaffWorkspaceAbility | |||||
| } | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -6,10 +6,11 @@ export interface SessionStaff { | |||||
| id: number; | id: number; | ||||
| teamId: number; | teamId: number; | ||||
| isTeamLead: boolean; | isTeamLead: boolean; | ||||
| employType: string | null; | |||||
| } | } | ||||
| export interface SessionWithTokens extends Session { | export interface SessionWithTokens extends Session { | ||||
| staff?: SessionStaff; | staff?: SessionStaff; | ||||
| role?: String; | |||||
| role?: string; | |||||
| abilities?: string[]; | abilities?: string[]; | ||||
| accessToken?: string; | accessToken?: string; | ||||
| refreshToken?: string; | refreshToken?: string; | ||||
| @@ -60,14 +61,14 @@ export const authOptions: AuthOptions = { | |||||
| session({ session, token }) { | session({ session, token }) { | ||||
| const sessionWithToken: SessionWithTokens = { | const sessionWithToken: SessionWithTokens = { | ||||
| ...session, | ...session, | ||||
| role: token.role as String, | |||||
| role: token.role as string, | |||||
| // Add the data from the token to the session | // Add the data from the token to the session | ||||
| abilities: (token.abilities as ability[]).map( | abilities: (token.abilities as ability[]).map( | ||||
| (item: ability) => item.actionSubjectCombo, | (item: ability) => item.actionSubjectCombo, | ||||
| ) as string[], | ) as string[], | ||||
| accessToken: token.accessToken as string | undefined, | accessToken: token.accessToken as string | undefined, | ||||
| refreshToken: token.refreshToken as string | undefined, | refreshToken: token.refreshToken as string | undefined, | ||||
| staff: token.staff as SessionStaff | |||||
| staff: token.staff as SessionStaff, | |||||
| }; | }; | ||||
| // console.log(sessionWithToken) | // console.log(sessionWithToken) | ||||
| return sessionWithToken; | return sessionWithToken; | ||||
| @@ -59,16 +59,16 @@ export const [ | |||||
| VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | ||||
| MAINTAIN_NORMAL_STAFF_WORKSPACE, | MAINTAIN_NORMAL_STAFF_WORKSPACE, | ||||
| MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | ||||
| GENERATE_LATE_START_REPORTS, | |||||
| GENERATE_LATE_START_REPORT, | |||||
| GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
| GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
| GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
| GENERATE_COST_AND_EXPENSE_REPORT, | |||||
| GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
| GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
| GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
| GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
| GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
| ] = [ | ] = [ | ||||
| 'MAINTAIN_USER', | 'MAINTAIN_USER', | ||||
| 'MAINTAIN_TIMESHEET', | 'MAINTAIN_TIMESHEET', | ||||
| @@ -109,16 +109,16 @@ export const [ | |||||
| 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', | 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', | ||||
| 'MAINTAIN_NORMAL_STAFF_WORKSPACE', | 'MAINTAIN_NORMAL_STAFF_WORKSPACE', | ||||
| 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', | 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', | ||||
| 'GENERATE_LATE_START_REPORTS', | |||||
| 'GENERATE_PROJECT_POTENTIAL_DELAY_REPORT', | |||||
| 'GENERATE_RESOURCE_OVERCONSUMPTION_REPORT', | |||||
| 'GENERATE_COST_ANT_EXPENSE_REPORT', | |||||
| 'GENERATE_PROJECT_COMPLETION_REPORT', | |||||
| 'GENERATE_PROJECT_P&L_REPORT', | |||||
| 'GENERATE_FINANCIAL_STATUS_REPORT', | |||||
| 'GENERATE_PROJECT_CASH_FLOW_REPORT', | |||||
| 'GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||||
| 'GENERATE_CROSS_TEAM_CHARGE_REPORT', | |||||
| 'G_LATE_START_REPORT', | |||||
| 'G_PROJECT_POTENTIAL_DELAY_REPORT', | |||||
| 'G_RESOURCE_OVERCONSUMPTION_REPORT', | |||||
| 'G_COST_AND_EXPENSE_REPORT', | |||||
| 'G_PROJECT_COMPLETION_REPORT', | |||||
| 'G_PROJECT_P&L_REPORT', | |||||
| 'G_FINANCIAL_STATUS_REPORT', | |||||
| 'G_PROJECT_CASH_FLOW_REPORT', | |||||
| 'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||||
| 'G_CROSS_TEAM_CHARGE_REPORT' | |||||
| ] | ] | ||||
| const PRIVATE_ROUTES = [ | const PRIVATE_ROUTES = [ | ||||
| @@ -243,20 +243,21 @@ export default async function middleware( | |||||
| if (req.nextUrl.pathname.startsWith('/analytics')) { | if (req.nextUrl.pathname.startsWith('/analytics')) { | ||||
| isAuth = [ | isAuth = [ | ||||
| GENERATE_LATE_START_REPORTS, | |||||
| GENERATE_LATE_START_REPORT, | |||||
| GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
| GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
| GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
| GENERATE_COST_AND_EXPENSE_REPORT, | |||||
| GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
| GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
| GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
| GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
| GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT,].some((ability) => abilities.includes(ability)); | |||||
| GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
| ].some((ability) => abilities.includes(ability)); | |||||
| } | } | ||||
| if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { | ||||
| isAuth = [GENERATE_LATE_START_REPORTS].some((ability) => abilities.includes(ability)); | |||||
| isAuth = [GENERATE_LATE_START_REPORT].some((ability) => abilities.includes(ability)); | |||||
| } | } | ||||
| if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { | ||||
| @@ -268,7 +269,7 @@ export default async function middleware( | |||||
| } | } | ||||
| if (req.nextUrl.pathname.startsWith('/analytics/CostandExpenseReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/CostandExpenseReport')) { | ||||
| isAuth = [GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => abilities.includes(ability)); | |||||
| isAuth = [GENERATE_COST_AND_EXPENSE_REPORT].some((ability) => abilities.includes(ability)); | |||||
| } | } | ||||
| if (req.nextUrl.pathname.startsWith('/analytics/ProjectCompletionReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/ProjectCompletionReport')) { | ||||