# 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; | |||
staffId: string; | |||
name: string; | |||
employType: string | null; | |||
}; | |||
}; | |||
@@ -21,6 +22,7 @@ export type TeamLeaves = { | |||
leaveEntries: RecordLeaveInput; | |||
staffId: string; | |||
name: string; | |||
employType: string | null; | |||
}; | |||
}; | |||
@@ -1,14 +1,9 @@ | |||
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | |||
import { HolidaysResult } from "../holidays"; | |||
import { | |||
LeaveEntry, | |||
RecordLeaveInput, | |||
RecordTimeLeaveInput, | |||
RecordTimesheetInput, | |||
TimeEntry, | |||
} from "./actions"; | |||
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; | |||
import { convertDateArrayToString } from "@/app/utils/formatUtil"; | |||
import compact from "lodash/compact"; | |||
import dayjs from "dayjs"; | |||
export type TimeEntryError = { | |||
[field in keyof TimeEntry]?: string; | |||
@@ -83,85 +78,10 @@ export const validateLeaveEntry = ( | |||
return Object.keys(error).length > 0 ? error : undefined; | |||
}; | |||
export const validateTimesheet = ( | |||
timesheet: RecordTimesheetInput, | |||
leaveRecords: RecordLeaveInput, | |||
companyHolidays: HolidaysResult[], | |||
): { [date: string]: string } | undefined => { | |||
const errors: { [date: string]: string } = {}; | |||
const holidays = new Set( | |||
compact([ | |||
...getPublicHolidaysForNYears(2).map((h) => h.date), | |||
...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||
]), | |||
); | |||
Object.keys(timesheet).forEach((date) => { | |||
const timeEntries = timesheet[date]; | |||
// Check each entry | |||
for (const entry of timeEntries) { | |||
const entryErrors = validateTimeEntry(entry, holidays.has(date)); | |||
if (entryErrors) { | |||
errors[date] = "There are errors in the entries"; | |||
return; | |||
} | |||
} | |||
// Check total hours | |||
const leaves = leaveRecords[date] || []; | |||
const totalHourError = checkTotalHours(timeEntries, leaves); | |||
if (totalHourError) { | |||
errors[date] = totalHourError; | |||
} | |||
}); | |||
return Object.keys(errors).length > 0 ? errors : undefined; | |||
}; | |||
export const validateLeaveRecord = ( | |||
leaveRecords: RecordLeaveInput, | |||
timesheet: RecordTimesheetInput, | |||
companyHolidays: HolidaysResult[], | |||
): { [date: string]: string } | undefined => { | |||
const errors: { [date: string]: string } = {}; | |||
const holidays = new Set( | |||
compact([ | |||
...getPublicHolidaysForNYears(2).map((h) => h.date), | |||
...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||
]), | |||
); | |||
Object.keys(leaveRecords).forEach((date) => { | |||
const leaves = leaveRecords[date]; | |||
// Check each leave entry | |||
for (const entry of leaves) { | |||
const entryError = validateLeaveEntry(entry, holidays.has(date)); | |||
if (entryError) { | |||
errors[date] = "There are errors in the entries"; | |||
return; | |||
} | |||
} | |||
// Check total hours | |||
const timeEntries = timesheet[date] || []; | |||
const totalHourError = checkTotalHours(timeEntries, leaves); | |||
if (totalHourError) { | |||
errors[date] = totalHourError; | |||
} | |||
}); | |||
return Object.keys(errors).length > 0 ? errors : undefined; | |||
}; | |||
export const validateTimeLeaveRecord = ( | |||
records: RecordTimeLeaveInput, | |||
companyHolidays: HolidaysResult[], | |||
isFullTime?: boolean, | |||
): { [date: string]: string } | undefined => { | |||
const errors: { [date: string]: string } = {}; | |||
@@ -173,14 +93,18 @@ export const validateTimeLeaveRecord = ( | |||
); | |||
Object.keys(records).forEach((date) => { | |||
const dayJsObj = dayjs(date); | |||
const isHoliday = | |||
holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||
const entries = records[date]; | |||
// Check each entry | |||
for (const entry of entries) { | |||
let entryError; | |||
if (entry.type === "leaveEntry") { | |||
entryError = validateLeaveEntry(entry, holidays.has(date)); | |||
entryError = validateLeaveEntry(entry, isHoliday); | |||
} else { | |||
entryError = validateTimeEntry(entry, holidays.has(date)); | |||
entryError = validateTimeEntry(entry, isHoliday); | |||
} | |||
if (entryError) { | |||
@@ -191,8 +115,10 @@ export const validateTimeLeaveRecord = ( | |||
// Check total hours | |||
const totalHourError = checkTotalHours( | |||
entries.filter((e) => e.type === "timeEntry"), | |||
entries.filter((e) => e.type === "leaveEntry"), | |||
entries.filter((e) => e.type === "timeEntry") as TimeEntry[], | |||
entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | |||
isHoliday, | |||
isFullTime, | |||
); | |||
if (totalHourError) { | |||
@@ -206,6 +132,8 @@ export const validateTimeLeaveRecord = ( | |||
export const checkTotalHours = ( | |||
timeEntries: TimeEntry[], | |||
leaves: LeaveEntry[], | |||
isHoliday?: boolean, | |||
isFullTime?: boolean, | |||
): string | undefined => { | |||
const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | |||
@@ -219,6 +147,12 @@ export const checkTotalHours = ( | |||
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."; | |||
} 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 ( | |||
totalInputHours + totalOtHours + leaveHours > | |||
TIMESHEET_DAILY_MAX_HOURS | |||
@@ -134,8 +134,8 @@ export function convertLocaleStringToNumber(numberString: string): number { | |||
} | |||
export function timestampToDateString(timestamp: string): string { | |||
if (timestamp === "0" || timestamp === null) { | |||
return "-"; | |||
if (timestamp === null){ | |||
return "-" | |||
} | |||
const date = new Date(timestamp); | |||
const year = date.getFullYear(); | |||
@@ -344,7 +344,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
{/* <Grid item xs={6}> | |||
<Checkbox | |||
{...register("isClpProject")} | |||
checked={Boolean(watch("isClpProject"))} | |||
@@ -353,7 +353,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
<Typography variant="overline" display="inline"> | |||
{t("CLP Project")} | |||
</Typography> | |||
</Grid> | |||
</Grid> */} | |||
</Grid> | |||
</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); | |||
const response = await importIssuedInovice(formData); | |||
console.log(response); | |||
// response: status, message, projectList, emptyRowList, invoiceList | |||
if (response.status) { | |||
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 { | |||
Box, | |||
Button, | |||
Card, | |||
CardActions, | |||
CardContent, | |||
Modal, | |||
ModalProps, | |||
SxProps, | |||
Typography, | |||
} from "@mui/material"; | |||
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 = { | |||
position: "absolute", | |||
@@ -52,167 +22,58 @@ const modalSx: SxProps = { | |||
maxWidth: 1400, | |||
}; | |||
interface Props extends LeaveCalendarProps { | |||
open: boolean; | |||
onClose: () => void; | |||
} | |||
const LeaveModal: React.FC<Props> = ({ | |||
isOpen, | |||
open, | |||
onClose, | |||
defaultLeaveRecords, | |||
timesheetRecords, | |||
leaveTypes, | |||
companyHolidays, | |||
allProjects, | |||
leaveRecords, | |||
timesheetRecords, | |||
isFullTime, | |||
}) => { | |||
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> | |||
</FullscreenModal> | |||
)} | |||
</FormProvider> | |||
</CardContent> | |||
</Card> | |||
</Modal> | |||
); | |||
}; | |||
@@ -1,5 +1,3 @@ | |||
import { Grid } from "@mui/material"; | |||
interface Props { | |||
width?: number; | |||
height?: number; | |||
@@ -16,7 +14,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||
<g clipPath="url(#a)"> | |||
<path id="logo" | |||
fill="#89ba17" stroke="#89ba17" stroke-width="1" | |||
fill="#89ba17" stroke="#89ba17" strokeWidth="1" | |||
d="M 98.00,125.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 | |||
@@ -66,7 +64,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||
41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | |||
<path id="word" | |||
fill="#111927" stroke="#111927" stroke-width="1" | |||
fill="#111927" stroke="#111927" strokeWidth="1" | |||
d="M 273.00,64.00 | |||
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 | |||
@@ -67,16 +67,16 @@ import { | |||
MAINTAIN_GROUP, | |||
MAINTAIN_HOLIDAY, | |||
VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | |||
GENERATE_LATE_START_REPORTS, | |||
GENERATE_LATE_START_REPORT, | |||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | |||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | |||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||
GENERATE_COST_AND_EXPENSE_REPORT, | |||
GENERATE_PROJECT_COMPLETION_REPORT, | |||
GENERATE_PROJECT_PANDL_REPORT, | |||
GENERATE_FINANCIAL_STATUS_REPORT, | |||
GENERATE_PROJECT_CASH_FLOW_REPORT, | |||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||
} from "@/middleware"; | |||
import { SessionWithAbilities } from "../AppBar/NavigationToggle"; | |||
import { authOptions } from "@/config/authConfig"; | |||
@@ -190,16 +190,16 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||
label: "Analysis Report", | |||
path: "", | |||
isHidden: ![ | |||
GENERATE_LATE_START_REPORTS, | |||
GENERATE_LATE_START_REPORT, | |||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | |||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | |||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||
GENERATE_COST_AND_EXPENSE_REPORT, | |||
GENERATE_PROJECT_COMPLETION_REPORT, | |||
GENERATE_PROJECT_PANDL_REPORT, | |||
GENERATE_FINANCIAL_STATUS_REPORT, | |||
GENERATE_PROJECT_CASH_FLOW_REPORT, | |||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||
].some((ability) => | |||
abilities!.includes(ability), | |||
), | |||
@@ -208,7 +208,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||
icon: <Analytics />, | |||
label: "Late Start Report", | |||
path: "/analytics/LateStartReport", | |||
isHidden: ![GENERATE_LATE_START_REPORTS].some((ability) => | |||
isHidden: ![GENERATE_LATE_START_REPORT].some((ability) => | |||
abilities!.includes(ability), | |||
), | |||
}, | |||
@@ -232,7 +232,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||
icon: <Analytics />, | |||
label: "Cost and Expense Report", | |||
path: "/analytics/CostandExpenseReport", | |||
isHidden: ![GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => | |||
isHidden: ![GENERATE_COST_AND_EXPENSE_REPORT].some((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; | |||
leaves: RecordLeaveInput; | |||
onDateSelect: (date: string) => void; | |||
onMonthChange: (day: Dayjs) => void; | |||
} | |||
const getColor = ( | |||
@@ -72,6 +73,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||
timesheet, | |||
leaves, | |||
onDateSelect, | |||
onMonthChange, | |||
}) => { | |||
const { | |||
i18n: { language }, | |||
@@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||
> | |||
<DateCalendar | |||
onChange={onChange} | |||
onMonthChange={onMonthChange} | |||
disableFuture | |||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
slots={{ day: EntryDay as any }} | |||
@@ -20,12 +20,17 @@ import { ProjectWithTasks } from "@/app/api/projects"; | |||
import { LeaveType } from "@/app/api/timesheets"; | |||
import useIsMobile from "@/app/utils/useIsMobile"; | |||
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; | |||
handleClose: () => void; | |||
leaveTypes: LeaveType[]; | |||
allProjects: ProjectWithTasks[]; | |||
companyHolidays: HolidaysResult[]; | |||
} | |||
const Indicator = styled(Box)(() => ({ | |||
@@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||
const { t } = useTranslation("home"); | |||
const [selectedDate, setSelectedDate] = useState(""); | |||
const [currentMonth, setMonthChange] = useState(dayjs()); | |||
const clearDate = useCallback(() => { | |||
setSelectedDate(""); | |||
@@ -54,40 +60,52 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||
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(); | |||
@@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||
startIcon={<ArrowBack />} | |||
onClick={clearDate} | |||
> | |||
{t("Back")} | |||
{t("Back to Monthly Summary")} | |||
</Button> | |||
)} | |||
</Box> | |||
</Box> | |||
</FullscreenModal> | |||
) : ( | |||
<Dialog onClose={onClose} open={open}> | |||
<Dialog onClose={onClose} open={open} maxWidth="md"> | |||
<DialogTitle>{t("Past Entries")}</DialogTitle> | |||
<DialogContent>{content}</DialogContent> | |||
{selectedDate && ( | |||
@@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||
startIcon={<ArrowBack />} | |||
onClick={clearDate} | |||
> | |||
{t("Back")} | |||
{t("Back to Monthly Summary")} | |||
</Button> | |||
</DialogActions> | |||
)} | |||
@@ -57,7 +57,12 @@ const PastEntryList: React.FC<Props> = ({ | |||
const dayJsObj = dayjs(date); | |||
return ( | |||
<Stack gap={2} marginBlockEnd={2} minWidth={{ sm: 375 }}> | |||
<Stack | |||
gap={2} | |||
marginBlockEnd={2} | |||
minWidth={{ sm: 375 }} | |||
maxHeight={{ sm: 500 }} | |||
> | |||
<Typography | |||
variant="overline" | |||
color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||
@@ -94,17 +99,25 @@ const PastEntryList: React.FC<Props> = ({ | |||
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> | |||
<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> | |||
); | |||
}; | |||
@@ -409,7 +409,7 @@ const ProgressByClient: React.FC<Props> = () => { | |||
const spentManhours = chartProjectSpentHour[dataPointIndex]; | |||
const value = series[seriesIndex][dataPointIndex]; | |||
const tooltipContent = ` | |||
<div style="width: 250px;"> | |||
<div style="width: auto;"> | |||
<span style="font-weight: bold;">${projectCode} - ${projectName}</span> | |||
<br> | |||
Budget Manhours: ${budgetManhours} hours | |||
@@ -492,7 +492,7 @@ const ProgressByTeam: React.FC = () => { | |||
const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; | |||
const value = series[seriesIndex][dataPointIndex]; | |||
const tooltipContent = ` | |||
<div style="width: 100%;"> | |||
<div style="width: auto;"> | |||
<span style="font-weight: bold;">${projectCode} - ${projectName}</span> | |||
<br> | |||
Budget Manhours: ${budgetManhours} hours | |||
@@ -954,7 +954,7 @@ const ProjectCashFlow: React.FC = () => { | |||
className="text-sm font-medium ml-5" | |||
style={{ color: "#898d8d" }} | |||
> | |||
Accounts Receivable | |||
Remaining Budget | |||
</div> | |||
<div | |||
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 endCount = weeklyPlanned[i].endCount | |||
for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { | |||
if (j >= startCount && j < endCount) { | |||
if (j >= startCount && j <= endCount) { | |||
weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) | |||
} else { | |||
weeklyPlannedSubList.push(0) | |||
@@ -503,7 +503,8 @@ const StaffUtilization: React.FC<Props> = ({ abilities, staff }) => { | |||
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 staffList = [] | |||
var maxValue = 5 | |||
@@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||
}, {}); | |||
}, [assignedProjects]); | |||
const { getValues, setValue, clearErrors } = | |||
const { getValues, setValue, clearErrors, setError } = | |||
useFormContext<RecordTimeLeaveInput>(); | |||
const currentEntries = getValues(day); | |||
@@ -393,7 +393,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||
params.row._error?.[ | |||
params.field as keyof Omit<TimeLeaveEntry, "type"> | |||
]; | |||
const content = <GridEditInputCell {...params} />; | |||
const content = ( | |||
<GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | |||
<Box width="100%">{content}</Box> | |||
@@ -423,7 +425,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||
params.row._error?.[ | |||
params.field as keyof Omit<TimeLeaveEntry, "type"> | |||
]; | |||
const content = <GridEditInputCell {...params} />; | |||
const content = ( | |||
<GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||
); | |||
return errorMessage ? ( | |||
<Tooltip title={t(errorMessage)}> | |||
<Box width="100%">{content}</Box> | |||
@@ -486,8 +490,13 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||
.filter((e): e is TimeLeaveEntry => Boolean(e)); | |||
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( | |||
(entry) => entry._isPlanned !== undefined && !entry._isPlanned, | |||
@@ -49,6 +49,7 @@ interface Props { | |||
companyHolidays: HolidaysResult[]; | |||
fastEntryEnabled?: boolean; | |||
leaveTypes: LeaveType[]; | |||
isFullTime: boolean; | |||
} | |||
const modalSx: SxProps = { | |||
@@ -71,6 +72,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||
companyHolidays, | |||
fastEntryEnabled, | |||
leaveTypes, | |||
isFullTime | |||
}) => { | |||
const { t } = useTranslation("home"); | |||
@@ -106,7 +108,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||
const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | |||
async (data) => { | |||
const errors = validateTimeLeaveRecord(data, companyHolidays); | |||
const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime); | |||
if (errors) { | |||
Object.keys(errors).forEach((date) => | |||
formProps.setError(date, { | |||
@@ -131,7 +133,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||
formProps.reset(newFormValues); | |||
onClose(); | |||
}, | |||
[companyHolidays, formProps, onClose], | |||
[companyHolidays, formProps, onClose, isFullTime], | |||
); | |||
const onCancel = useCallback(() => { | |||
@@ -347,6 +347,10 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||
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 intStaffId = parseInt(selectedStaff.id); | |||
const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | |||
const timesheets = | |||
@@ -360,7 +364,11 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||
leaves, | |||
"id", | |||
); | |||
totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||
totalHourError = checkTotalHours( | |||
timesheets, | |||
leavesWithNewEntry, | |||
Boolean(isHoliday), | |||
); | |||
} else { | |||
// newEntry is a timesheet entry | |||
const timesheetsWithNewEntry = unionBy( | |||
@@ -368,11 +376,15 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||
timesheets, | |||
"id", | |||
); | |||
totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves); | |||
totalHourError = checkTotalHours( | |||
timesheetsWithNewEntry, | |||
leaves, | |||
Boolean(isHoliday), | |||
); | |||
} | |||
if (totalHourError) throw Error(totalHourError); | |||
}, | |||
[localTeamLeaves, localTeamTimesheets, selectedStaff.id], | |||
[localTeamLeaves, localTeamTimesheets, selectedStaff, companyHolidays], | |||
); | |||
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" | |||
render={({ field }) => ( | |||
<ProjectSelect | |||
includeLeaves={false} | |||
error={Boolean(formState.errors.projectIds)} | |||
multiple | |||
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 { manhourFormatter } from "@/app/utils/formatUtil"; | |||
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[]; | |||
maintainNormalStaffWorkspaceAbility?: boolean; | |||
maintainManagementStaffWorkspaceAbility?: boolean; | |||
} | |||
const ProjectGrid: React.FC<Props> = ({ projects, maintainNormalStaffWorkspaceAbility, maintainManagementStaffWorkspaceAbility }) => { | |||
const ProjectGrid: React.FC<Props> = ({ | |||
projects, | |||
maintainNormalStaffWorkspaceAbility, | |||
maintainManagementStaffWorkspaceAbility, | |||
}) => { | |||
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 ( | |||
<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={{ | |||
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> | |||
</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> | |||
); | |||
}; | |||
@@ -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 Button from "@mui/material/Button"; | |||
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 AssignedProjects from "./AssignedProjects"; | |||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||
@@ -19,6 +24,7 @@ import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal" | |||
import { HolidaysResult } from "@/app/api/holidays"; | |||
import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | |||
import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | |||
import LeaveModal from "../LeaveModal"; | |||
export interface Props { | |||
leaveTypes: LeaveType[]; | |||
@@ -32,6 +38,7 @@ export interface Props { | |||
fastEntryEnabled: boolean; | |||
maintainNormalStaffWorkspaceAbility: boolean; | |||
maintainManagementStaffWorkspaceAbility: boolean; | |||
isFullTime: boolean; | |||
} | |||
const menuItemSx: SxProps = { | |||
@@ -51,10 +58,12 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
fastEntryEnabled, | |||
maintainNormalStaffWorkspaceAbility, | |||
maintainManagementStaffWorkspaceAbility, | |||
isFullTime, | |||
}) => { | |||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | |||
const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); | |||
const [isLeaveCalendarVisible, setLeaveCalendarVisible] = useState(false); | |||
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | |||
const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | |||
useState(false); | |||
@@ -81,6 +90,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
setTimeLeaveModalVisible(false); | |||
}, []); | |||
const handleOpenLeaveCalendarButton = useCallback(() => { | |||
setAnchorEl(null); | |||
setLeaveCalendarVisible(true); | |||
}, []); | |||
const handleCloseLeaveCalendarButton = useCallback(() => { | |||
setLeaveCalendarVisible(false); | |||
}, []); | |||
const handlePastEventClick = useCallback(() => { | |||
setAnchorEl(null); | |||
setPastEventModalVisible(true); | |||
@@ -136,6 +154,10 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
<MoreTime /> | |||
{t("Enter Timesheet")} | |||
</MenuItem> | |||
<MenuItem onClick={handleOpenLeaveCalendarButton} sx={menuItemSx}> | |||
<Luggage /> | |||
{t("Record Leave")} | |||
</MenuItem> | |||
<MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | |||
<CalendarMonth /> | |||
{t("View Past Entries")} | |||
@@ -154,6 +176,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
leaves={defaultLeaveRecords} | |||
allProjects={allProjects} | |||
leaveTypes={leaveTypes} | |||
companyHolidays={holidays} | |||
/> | |||
<TimeLeaveModal | |||
fastEntryEnabled={fastEntryEnabled} | |||
@@ -165,6 +188,17 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||
assignedProjects={assignedProjects} | |||
timesheetRecords={defaultTimesheets} | |||
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 | |||
@@ -11,8 +11,12 @@ import { | |||
fetchTimesheets, | |||
} from "@/app/api/timesheets"; | |||
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 [ | |||
@@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||
leaveTypes, | |||
holidays, | |||
abilities, | |||
userStaff, | |||
] = await Promise.all([ | |||
fetchTeamMemberLeaves(), | |||
fetchTeamMemberTimesheets(), | |||
@@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||
fetchLeaves(), | |||
fetchLeaveTypes(), | |||
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 ( | |||
<UserWorkspacePage | |||
isFullTime={isFullTime} | |||
teamLeaves={teamLeaves} | |||
teamTimesheets={teamTimesheets} | |||
allProjects={allProjects} | |||
@@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||
// Change to access check | |||
fastEntryEnabled={fastEntryEnabled} | |||
maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | |||
maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility} | |||
maintainManagementStaffWorkspaceAbility={ | |||
maintainManagementStaffWorkspaceAbility | |||
} | |||
/> | |||
); | |||
}; | |||
@@ -6,10 +6,11 @@ export interface SessionStaff { | |||
id: number; | |||
teamId: number; | |||
isTeamLead: boolean; | |||
employType: string | null; | |||
} | |||
export interface SessionWithTokens extends Session { | |||
staff?: SessionStaff; | |||
role?: String; | |||
role?: string; | |||
abilities?: string[]; | |||
accessToken?: string; | |||
refreshToken?: string; | |||
@@ -60,14 +61,14 @@ export const authOptions: AuthOptions = { | |||
session({ session, token }) { | |||
const sessionWithToken: SessionWithTokens = { | |||
...session, | |||
role: token.role as String, | |||
role: token.role as string, | |||
// Add the data from the token to the session | |||
abilities: (token.abilities as ability[]).map( | |||
(item: ability) => item.actionSubjectCombo, | |||
) as string[], | |||
accessToken: token.accessToken as string | undefined, | |||
refreshToken: token.refreshToken as string | undefined, | |||
staff: token.staff as SessionStaff | |||
staff: token.staff as SessionStaff, | |||
}; | |||
// console.log(sessionWithToken) | |||
return sessionWithToken; | |||
@@ -59,16 +59,16 @@ export const [ | |||
VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | |||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||
GENERATE_LATE_START_REPORTS, | |||
GENERATE_LATE_START_REPORT, | |||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | |||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | |||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||
GENERATE_COST_AND_EXPENSE_REPORT, | |||
GENERATE_PROJECT_COMPLETION_REPORT, | |||
GENERATE_PROJECT_PANDL_REPORT, | |||
GENERATE_FINANCIAL_STATUS_REPORT, | |||
GENERATE_PROJECT_CASH_FLOW_REPORT, | |||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||
] = [ | |||
'MAINTAIN_USER', | |||
'MAINTAIN_TIMESHEET', | |||
@@ -109,16 +109,16 @@ export const [ | |||
'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', | |||
'MAINTAIN_NORMAL_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 = [ | |||
@@ -243,20 +243,21 @@ export default async function middleware( | |||
if (req.nextUrl.pathname.startsWith('/analytics')) { | |||
isAuth = [ | |||
GENERATE_LATE_START_REPORTS, | |||
GENERATE_LATE_START_REPORT, | |||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | |||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | |||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||
GENERATE_COST_AND_EXPENSE_REPORT, | |||
GENERATE_PROJECT_COMPLETION_REPORT, | |||
GENERATE_PROJECT_PANDL_REPORT, | |||
GENERATE_FINANCIAL_STATUS_REPORT, | |||
GENERATE_PROJECT_CASH_FLOW_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')) { | |||
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')) { | |||
@@ -268,7 +269,7 @@ export default async function middleware( | |||
} | |||
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')) { | |||