# Conflicts: # src/app/utils/formatUtil.ts # src/components/InvoiceSearch/InvoiceSearch.tsx # src/components/InvoiceSearch/InvoiceSearchWrapper.tsxtags/Baseline_30082024_FRONTEND_UAT
@@ -13,6 +13,7 @@ export type TeamTimeSheets = { | |||||
timeEntries: RecordTimesheetInput; | timeEntries: RecordTimesheetInput; | ||||
staffId: string; | staffId: string; | ||||
name: string; | name: string; | ||||
employType: string | null; | |||||
}; | }; | ||||
}; | }; | ||||
@@ -21,6 +22,7 @@ export type TeamLeaves = { | |||||
leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
staffId: string; | staffId: string; | ||||
name: string; | name: string; | ||||
employType: string | null; | |||||
}; | }; | ||||
}; | }; | ||||
@@ -1,14 +1,9 @@ | |||||
import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; | ||||
import { HolidaysResult } from "../holidays"; | import { HolidaysResult } from "../holidays"; | ||||
import { | |||||
LeaveEntry, | |||||
RecordLeaveInput, | |||||
RecordTimeLeaveInput, | |||||
RecordTimesheetInput, | |||||
TimeEntry, | |||||
} from "./actions"; | |||||
import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions"; | |||||
import { convertDateArrayToString } from "@/app/utils/formatUtil"; | import { convertDateArrayToString } from "@/app/utils/formatUtil"; | ||||
import compact from "lodash/compact"; | import compact from "lodash/compact"; | ||||
import dayjs from "dayjs"; | |||||
export type TimeEntryError = { | export type TimeEntryError = { | ||||
[field in keyof TimeEntry]?: string; | [field in keyof TimeEntry]?: string; | ||||
@@ -83,85 +78,10 @@ export const validateLeaveEntry = ( | |||||
return Object.keys(error).length > 0 ? error : undefined; | return Object.keys(error).length > 0 ? error : undefined; | ||||
}; | }; | ||||
export const validateTimesheet = ( | |||||
timesheet: RecordTimesheetInput, | |||||
leaveRecords: RecordLeaveInput, | |||||
companyHolidays: HolidaysResult[], | |||||
): { [date: string]: string } | undefined => { | |||||
const errors: { [date: string]: string } = {}; | |||||
const holidays = new Set( | |||||
compact([ | |||||
...getPublicHolidaysForNYears(2).map((h) => h.date), | |||||
...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||||
]), | |||||
); | |||||
Object.keys(timesheet).forEach((date) => { | |||||
const timeEntries = timesheet[date]; | |||||
// Check each entry | |||||
for (const entry of timeEntries) { | |||||
const entryErrors = validateTimeEntry(entry, holidays.has(date)); | |||||
if (entryErrors) { | |||||
errors[date] = "There are errors in the entries"; | |||||
return; | |||||
} | |||||
} | |||||
// Check total hours | |||||
const leaves = leaveRecords[date] || []; | |||||
const totalHourError = checkTotalHours(timeEntries, leaves); | |||||
if (totalHourError) { | |||||
errors[date] = totalHourError; | |||||
} | |||||
}); | |||||
return Object.keys(errors).length > 0 ? errors : undefined; | |||||
}; | |||||
export const validateLeaveRecord = ( | |||||
leaveRecords: RecordLeaveInput, | |||||
timesheet: RecordTimesheetInput, | |||||
companyHolidays: HolidaysResult[], | |||||
): { [date: string]: string } | undefined => { | |||||
const errors: { [date: string]: string } = {}; | |||||
const holidays = new Set( | |||||
compact([ | |||||
...getPublicHolidaysForNYears(2).map((h) => h.date), | |||||
...companyHolidays.map((h) => convertDateArrayToString(h.date)), | |||||
]), | |||||
); | |||||
Object.keys(leaveRecords).forEach((date) => { | |||||
const leaves = leaveRecords[date]; | |||||
// Check each leave entry | |||||
for (const entry of leaves) { | |||||
const entryError = validateLeaveEntry(entry, holidays.has(date)); | |||||
if (entryError) { | |||||
errors[date] = "There are errors in the entries"; | |||||
return; | |||||
} | |||||
} | |||||
// Check total hours | |||||
const timeEntries = timesheet[date] || []; | |||||
const totalHourError = checkTotalHours(timeEntries, leaves); | |||||
if (totalHourError) { | |||||
errors[date] = totalHourError; | |||||
} | |||||
}); | |||||
return Object.keys(errors).length > 0 ? errors : undefined; | |||||
}; | |||||
export const validateTimeLeaveRecord = ( | export const validateTimeLeaveRecord = ( | ||||
records: RecordTimeLeaveInput, | records: RecordTimeLeaveInput, | ||||
companyHolidays: HolidaysResult[], | companyHolidays: HolidaysResult[], | ||||
isFullTime?: boolean, | |||||
): { [date: string]: string } | undefined => { | ): { [date: string]: string } | undefined => { | ||||
const errors: { [date: string]: string } = {}; | const errors: { [date: string]: string } = {}; | ||||
@@ -173,14 +93,18 @@ export const validateTimeLeaveRecord = ( | |||||
); | ); | ||||
Object.keys(records).forEach((date) => { | Object.keys(records).forEach((date) => { | ||||
const dayJsObj = dayjs(date); | |||||
const isHoliday = | |||||
holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const entries = records[date]; | const entries = records[date]; | ||||
// Check each entry | // Check each entry | ||||
for (const entry of entries) { | for (const entry of entries) { | ||||
let entryError; | let entryError; | ||||
if (entry.type === "leaveEntry") { | if (entry.type === "leaveEntry") { | ||||
entryError = validateLeaveEntry(entry, holidays.has(date)); | |||||
entryError = validateLeaveEntry(entry, isHoliday); | |||||
} else { | } else { | ||||
entryError = validateTimeEntry(entry, holidays.has(date)); | |||||
entryError = validateTimeEntry(entry, isHoliday); | |||||
} | } | ||||
if (entryError) { | if (entryError) { | ||||
@@ -191,8 +115,10 @@ export const validateTimeLeaveRecord = ( | |||||
// Check total hours | // Check total hours | ||||
const totalHourError = checkTotalHours( | const totalHourError = checkTotalHours( | ||||
entries.filter((e) => e.type === "timeEntry"), | |||||
entries.filter((e) => e.type === "leaveEntry"), | |||||
entries.filter((e) => e.type === "timeEntry") as TimeEntry[], | |||||
entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[], | |||||
isHoliday, | |||||
isFullTime, | |||||
); | ); | ||||
if (totalHourError) { | if (totalHourError) { | ||||
@@ -206,6 +132,8 @@ export const validateTimeLeaveRecord = ( | |||||
export const checkTotalHours = ( | export const checkTotalHours = ( | ||||
timeEntries: TimeEntry[], | timeEntries: TimeEntry[], | ||||
leaves: LeaveEntry[], | leaves: LeaveEntry[], | ||||
isHoliday?: boolean, | |||||
isFullTime?: boolean, | |||||
): string | undefined => { | ): string | undefined => { | ||||
const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | ||||
@@ -219,6 +147,12 @@ export const checkTotalHours = ( | |||||
if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | ||||
return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; | return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; | ||||
} else if ( | |||||
isFullTime && | |||||
!isHoliday && | |||||
totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS | |||||
) { | |||||
return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}."; | |||||
} else if ( | } else if ( | ||||
totalInputHours + totalOtHours + leaveHours > | totalInputHours + totalOtHours + leaveHours > | ||||
TIMESHEET_DAILY_MAX_HOURS | TIMESHEET_DAILY_MAX_HOURS | ||||
@@ -134,8 +134,8 @@ export function convertLocaleStringToNumber(numberString: string): number { | |||||
} | } | ||||
export function timestampToDateString(timestamp: string): string { | export function timestampToDateString(timestamp: string): string { | ||||
if (timestamp === "0" || timestamp === null) { | |||||
return "-"; | |||||
if (timestamp === null){ | |||||
return "-" | |||||
} | } | ||||
const date = new Date(timestamp); | const date = new Date(timestamp); | ||||
const year = date.getFullYear(); | const year = date.getFullYear(); | ||||
@@ -344,7 +344,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | |||||
{/* <Grid item xs={6}> | |||||
<Checkbox | <Checkbox | ||||
{...register("isClpProject")} | {...register("isClpProject")} | ||||
checked={Boolean(watch("isClpProject"))} | checked={Boolean(watch("isClpProject"))} | ||||
@@ -353,7 +353,7 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||||
<Typography variant="overline" display="inline"> | <Typography variant="overline" display="inline"> | ||||
{t("CLP Project")} | {t("CLP Project")} | ||||
</Typography> | </Typography> | ||||
</Grid> | |||||
</Grid> */} | |||||
</Grid> | </Grid> | ||||
</Box> | </Box> | ||||
@@ -1,109 +0,0 @@ | |||||
"use client"; | |||||
import { useState } from "react"; | |||||
import Button from "@mui/material/Button"; | |||||
import { Card, Modal } from "@mui/material"; | |||||
import TimesheetInputGrid from "./LeaveInputGrid"; | |||||
// import { fetchLeaves } from "@/app/api/leave"; | |||||
interface EnterTimesheetModalProps { | |||||
isOpen: boolean; | |||||
onClose: () => void; | |||||
modalStyle?: any; | |||||
} | |||||
const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ | |||||
...props | |||||
}) => { | |||||
const [lockConfirm, setLockConfirm] = useState(false); | |||||
const columns = [ | |||||
{ | |||||
id: "projectCode", | |||||
field: "projectCode", | |||||
headerName: "Project Code and Name", | |||||
flex: 1, | |||||
}, | |||||
{ | |||||
id: "task", | |||||
field: "task", | |||||
headerName: "Task", | |||||
flex: 1, | |||||
}, | |||||
]; | |||||
const rows = [ | |||||
{ | |||||
id: 1, | |||||
projectCode: "M1001", | |||||
task: "1.2", | |||||
}, | |||||
{ | |||||
id: 2, | |||||
projectCode: "M1301", | |||||
task: "1.1", | |||||
}, | |||||
]; | |||||
const fetchTimesheet = async () => { | |||||
// fetchLeaves(); | |||||
// const res = await fetch(`http://localhost:8090/api/timesheets`, { | |||||
// // const res = await fetch(`${BASE_API_URL}/timesheets`, { | |||||
// method: "GET", | |||||
// mode: 'no-cors', | |||||
// }); | |||||
// console.log(res.json); | |||||
}; | |||||
return ( | |||||
<Modal open={props.isOpen} onClose={props.onClose}> | |||||
<div> | |||||
{/* <Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | |||||
Record Leave | |||||
</div> | |||||
</Typography> */} | |||||
<Card style={{ | |||||
flex: 10, | |||||
marginBottom: "20px", | |||||
width: "90%", | |||||
// height: "80%", | |||||
position: "fixed", | |||||
top: "50%", | |||||
left: "50%", | |||||
transform: "translate(-50%, -50%)", | |||||
}}> | |||||
<TimesheetInputGrid setLockConfirm={setLockConfirm}/> | |||||
<div | |||||
style={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
width: "100%", | |||||
flex: 1, | |||||
padding: "20px", | |||||
}} | |||||
> | |||||
<Button | |||||
disabled={lockConfirm} | |||||
variant="contained" | |||||
onClick={props.onClose} | |||||
> | |||||
Confirm | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
onClick={props.onClose} | |||||
sx={{ "background-color": "#F890A5" }} | |||||
> | |||||
Cancel | |||||
</Button> | |||||
</div> | |||||
</Card> | |||||
</div> | |||||
</Modal> | |||||
); | |||||
}; | |||||
export default EnterTimesheetModal; |
@@ -1,548 +0,0 @@ | |||||
"use client"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import Paper from "@mui/material/Paper"; | |||||
import { useState, useEffect } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import PageTitle from "../PageTitle/PageTitle"; | |||||
import { Suspense } from "react"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Link from "next/link"; | |||||
import { t } from "i18next"; | |||||
import { | |||||
Box, | |||||
Container, | |||||
Modal, | |||||
Select, | |||||
SelectChangeEvent, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import { Close } from "@mui/icons-material"; | |||||
import AddIcon from "@mui/icons-material/Add"; | |||||
import EditIcon from "@mui/icons-material/Edit"; | |||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | |||||
import SaveIcon from "@mui/icons-material/Save"; | |||||
import CancelIcon from "@mui/icons-material/Close"; | |||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; | |||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
import Swal from "sweetalert2"; | |||||
import { msg } from "../Swal/CustomAlerts"; | |||||
import React from "react"; | |||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
import { | |||||
GridRowsProp, | |||||
GridRowModesModel, | |||||
GridRowModes, | |||||
DataGrid, | |||||
GridColDef, | |||||
GridToolbarContainer, | |||||
GridFooterContainer, | |||||
GridActionsCellItem, | |||||
GridEventListener, | |||||
GridRowId, | |||||
GridRowModel, | |||||
GridRowEditStopReasons, | |||||
GridEditInputCell, | |||||
GridValueSetterParams, | |||||
} from "@mui/x-data-grid"; | |||||
import { LocalizationProvider } from "@mui/x-date-pickers"; | |||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | |||||
import { Props } from "react-intl/src/components/relative"; | |||||
const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; | |||||
interface BottomBarProps { | |||||
getHoursTotal: (column: string) => number; | |||||
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
interface EditToolbarProps { | |||||
// setDay: (newDay : dayjs.Dayjs) => void; | |||||
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
interface EditFooterProps { | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
const EditToolbar = (props: EditToolbarProps) => { | |||||
const { setDay } = props; | |||||
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||||
const handleClickLeft = () => { | |||||
if (selectedDate) { | |||||
const newDate = selectedDate.add(-7, "day"); | |||||
setSelectedDate(newDate); | |||||
} | |||||
}; | |||||
const handleClickRight = () => { | |||||
if (selectedDate) { | |||||
const newDate = | |||||
selectedDate.add(7, "day") > dayjs() | |||||
? dayjs() | |||||
: selectedDate.add(7, "day"); | |||||
setSelectedDate(newDate); | |||||
} | |||||
}; | |||||
const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||||
const newDate = dayjs(date); | |||||
setSelectedDate(newDate); | |||||
}; | |||||
useEffect(() => { | |||||
setDay((oldDay) => selectedDate); | |||||
}, [selectedDate]); | |||||
return ( | |||||
<LocalizationProvider dateAdapter={AdapterDayjs}> | |||||
<div | |||||
style={{ | |||||
display: "flex", | |||||
justifyContent: "flex-end", | |||||
width: "100%", | |||||
paddingBottom: "20px", | |||||
}} | |||||
> | |||||
<Typography variant="h5" id="modal-title" sx={{ flex: 1 }}> | |||||
Record Leave | |||||
</Typography> | |||||
<Button | |||||
sx={{ "border-radius": "30%", marginRight: "20px" }} | |||||
variant="contained" | |||||
onClick={handleClickLeft} | |||||
> | |||||
<ArrowBackIcon /> | |||||
</Button> | |||||
<DatePicker | |||||
value={selectedDate} | |||||
onChange={handleDateChange} | |||||
disableFuture={true} | |||||
/> | |||||
<Button | |||||
sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }} | |||||
variant="contained" | |||||
onClick={handleClickRight} | |||||
> | |||||
<ArrowForwardIcon /> | |||||
</Button> | |||||
</div> | |||||
</LocalizationProvider> | |||||
); | |||||
}; | |||||
const BottomBar = (props: BottomBarProps) => { | |||||
const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | |||||
// const getHoursTotal = props.getHoursTotal; | |||||
const [newId, setNewId] = useState(-1); | |||||
const [invalidDays, setInvalidDays] = useState(0); | |||||
const handleAddClick = () => { | |||||
const id = newId; | |||||
setNewId(newId - 1); | |||||
setRows((oldRows) => [ | |||||
...oldRows, | |||||
{ id, projectCode: "", task: "", isNew: true }, | |||||
]); | |||||
setRowModesModel((oldModel) => ({ | |||||
...oldModel, | |||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, | |||||
})); | |||||
}; | |||||
const totalColDef = { | |||||
flex: 1, | |||||
// style: {color:getHoursTotal('mon')>24?"red":"black"} | |||||
}; | |||||
const TotalCell = ({ value }: Props) => { | |||||
const [invalid, setInvalid] = useState(false); | |||||
useEffect(() => { | |||||
const newInvalid = (value ?? 0) > 24; | |||||
setInvalid(newInvalid); | |||||
}, [value]); | |||||
return ( | |||||
<Box flex={1} style={{ color: invalid ? "red" : "black" }}> | |||||
{value} | |||||
</Box> | |||||
); | |||||
}; | |||||
const checkUnlockConfirmBtn = () => { | |||||
// setLockConfirm((oldLock)=> valid); | |||||
setLockConfirm((oldLock) => | |||||
weekdays.every((weekday) => { | |||||
getHoursTotal(weekday) <= 24; | |||||
}), | |||||
); | |||||
}; | |||||
return ( | |||||
<div> | |||||
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||||
<Box flex={5.7} textAlign={"right"} marginRight="4rem"> | |||||
<b>Total:</b> | |||||
</Box> | |||||
<TotalCell value={getHoursTotal("mon")} /> | |||||
<TotalCell value={getHoursTotal("tue")} /> | |||||
<TotalCell value={getHoursTotal("wed")} /> | |||||
<TotalCell value={getHoursTotal("thu")} /> | |||||
<TotalCell value={getHoursTotal("fri")} /> | |||||
<TotalCell value={getHoursTotal("sat")} /> | |||||
<TotalCell value={getHoursTotal("sun")} /> | |||||
</div> | |||||
<Button | |||||
variant="outlined" | |||||
color="primary" | |||||
startIcon={<AddIcon />} | |||||
onClick={handleAddClick} | |||||
> | |||||
Add record | |||||
</Button> | |||||
</div> | |||||
); | |||||
}; | |||||
const EditFooter = (props: EditFooterProps) => { | |||||
return ( | |||||
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||||
<Box flex={1}> | |||||
<b>Total: </b> | |||||
</Box> | |||||
<Box flex={2}>ssss</Box> | |||||
</div> | |||||
); | |||||
}; | |||||
interface TimesheetInputGridProps { | |||||
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||||
onClose?: () => void; | |||||
} | |||||
const initialRows: GridRowsProp = [ | |||||
{ | |||||
id: 1, | |||||
projectCode: "M1001", | |||||
task: "1.2", | |||||
mon: 2.5, | |||||
}, | |||||
{ | |||||
id: 2, | |||||
projectCode: "M1002", | |||||
task: "1.3", | |||||
mon: 3.25, | |||||
}, | |||||
]; | |||||
const options = ["M1001", "M1301", "M1354", "M1973"]; | |||||
const options2 = [ | |||||
"1.1 - Preparation of preliminary Cost Estimate / Cost Plan", | |||||
"1.2 - Cash flow forecast", | |||||
"1.3 - Cost studies fo alterative design solutions", | |||||
"1.4 = Attend design co-ordination / project review meetings", | |||||
"1.5 - Prepare / Review RIC", | |||||
]; | |||||
const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { | |||||
if (date.day() == 0) { | |||||
return date.add(weekday - date.day() - 7, "day").format("DD MMM"); | |||||
} else { | |||||
return date.add(weekday - date.day(), "day").format("DD MMM"); | |||||
} | |||||
}; | |||||
const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ | |||||
...props | |||||
}) => { | |||||
const [rows, setRows] = useState(initialRows); | |||||
const [day, setDay] = useState(dayjs()); | |||||
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | |||||
{}, | |||||
); | |||||
const { setLockConfirm } = props; | |||||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||||
params, | |||||
event, | |||||
) => { | |||||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
event.defaultMuiPrevented = true; | |||||
} | |||||
}; | |||||
const handleEditClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||||
}; | |||||
const handleSaveClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||||
}; | |||||
const handleDeleteClick = (id: GridRowId) => () => { | |||||
setRows(rows.filter((row) => row.id !== id)); | |||||
}; | |||||
const handleCancelClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ | |||||
...rowModesModel, | |||||
[id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||||
}); | |||||
const editedRow = rows.find((row) => row.id === id); | |||||
if (editedRow!.isNew) { | |||||
setRows(rows.filter((row) => row.id !== id)); | |||||
} | |||||
}; | |||||
const processRowUpdate = (newRow: GridRowModel) => { | |||||
const updatedRow = { ...newRow, isNew: false }; | |||||
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||||
return updatedRow; | |||||
}; | |||||
const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | |||||
setRowModesModel(newRowModesModel); | |||||
}; | |||||
const getHoursTotal = (column: any) => { | |||||
let sum = 0; | |||||
rows.forEach((row) => { | |||||
sum += row[column] ?? 0; | |||||
}); | |||||
return sum; | |||||
}; | |||||
const weekdayColConfig: any = { | |||||
type: "number", | |||||
// sortable: false, | |||||
//width: 100, | |||||
flex: 1, | |||||
align: "left", | |||||
headerAlign: "left", | |||||
editable: true, | |||||
renderEditCell: (value: any) => ( | |||||
<GridEditInputCell | |||||
{...value} | |||||
inputProps={{ | |||||
max: 24, | |||||
min: 0, | |||||
step: 0.25, | |||||
}} | |||||
/> | |||||
), | |||||
}; | |||||
const columns: GridColDef[] = [ | |||||
{ | |||||
field: "actions", | |||||
type: "actions", | |||||
headerName: "Actions", | |||||
width: 100, | |||||
cellClassName: "actions", | |||||
getActions: ({ id }) => { | |||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||||
if (isInEditMode) { | |||||
return [ | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-save`} | |||||
icon={<SaveIcon />} | |||||
title="Save" | |||||
label="Save" | |||||
sx={{ | |||||
color: "primary.main", | |||||
}} | |||||
onClick={handleSaveClick(id)} | |||||
/>, | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-cancel`} | |||||
icon={<CancelIcon />} | |||||
title="Cancel" | |||||
label="Cancel" | |||||
className="textPrimary" | |||||
onClick={handleCancelClick(id)} | |||||
color="inherit" | |||||
/>, | |||||
]; | |||||
} | |||||
return [ | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-edit`} | |||||
icon={<EditIcon />} | |||||
title="Edit" | |||||
label="Edit" | |||||
className="textPrimary" | |||||
onClick={handleEditClick(id)} | |||||
color="inherit" | |||||
/>, | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-delete`} | |||||
title="Delete" | |||||
label="Delete" | |||||
icon={<DeleteIcon />} | |||||
onClick={handleDeleteClick(id)} | |||||
sx={{ color: "red" }} | |||||
/>, | |||||
]; | |||||
}, | |||||
}, | |||||
{ | |||||
field: "projectCode", | |||||
headerName: "Project Code", | |||||
// width: 220, | |||||
flex: 2, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions: options, | |||||
}, | |||||
{ | |||||
field: "task", | |||||
headerName: "Task", | |||||
// width: 220, | |||||
flex: 3, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions: options2, | |||||
}, | |||||
{ | |||||
// Mon | |||||
field: "mon", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Mon - {getDateForHeader(day, 1)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Tue | |||||
field: "tue", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Tue - {getDateForHeader(day, 2)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Wed | |||||
field: "wed", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Wed - {getDateForHeader(day, 3)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Thu | |||||
field: "thu", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Thu - {getDateForHeader(day, 4)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Fri | |||||
field: "fri", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Fri - {getDateForHeader(day, 5)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Sat | |||||
field: "sat", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Sat - {getDateForHeader(day, 6)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Sun | |||||
field: "sun", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return ( | |||||
<div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div> | |||||
); | |||||
}, | |||||
}, | |||||
// { | |||||
// field: 'joinDate', | |||||
// headerName: 'Join date', | |||||
// type: 'date', | |||||
// width: 180, | |||||
// editable: true, | |||||
// }, | |||||
]; | |||||
return ( | |||||
<Box | |||||
sx={{ | |||||
// marginBottom: '-5px', | |||||
display: "flex", | |||||
"flex-direction": "column", | |||||
// 'justify-content': 'flex-end', | |||||
padding: "20px", | |||||
height: "100%", //'30rem', | |||||
width: "100%", | |||||
"& .actions": { | |||||
color: "text.secondary", | |||||
}, | |||||
"& .header": { | |||||
// border: 1, | |||||
// 'border-width': '1px', | |||||
// 'border-color': 'grey', | |||||
}, | |||||
"& .textPrimary": { | |||||
color: "text.primary", | |||||
}, | |||||
}} | |||||
> | |||||
<DataGrid | |||||
rows={rows} | |||||
columns={columns} | |||||
editMode="row" | |||||
rowModesModel={rowModesModel} | |||||
onRowModesModelChange={handleRowModesModelChange} | |||||
onRowEditStop={handleRowEditStop} | |||||
processRowUpdate={processRowUpdate} | |||||
disableRowSelectionOnClick={true} | |||||
disableColumnMenu={true} | |||||
hideFooterPagination={true} | |||||
slots={{ | |||||
toolbar: EditToolbar, | |||||
// footer: EditFooter, | |||||
}} | |||||
slotProps={{ | |||||
toolbar: { setDay, setRows, setRowModesModel }, | |||||
// footer: { setDay, setRows, setRowModesModel }, | |||||
}} | |||||
initialState={{ | |||||
pagination: { paginationModel: { pageSize: 100 } }, | |||||
}} | |||||
sx={{ flex: 1 }} | |||||
/> | |||||
<BottomBar | |||||
getHoursTotal={getHoursTotal} | |||||
setRows={setRows} | |||||
setRowModesModel={setRowModesModel} | |||||
setLockConfirm={setLockConfirm} | |||||
// sx={{flex:3}} | |||||
/> | |||||
</Box> | |||||
); | |||||
}; | |||||
export default TimesheetInputGrid; |
@@ -1 +0,0 @@ | |||||
export { default } from "./EnterLeaveModal"; |
@@ -1,109 +0,0 @@ | |||||
"use client"; | |||||
import { useState } from "react"; | |||||
import Button from "@mui/material/Button"; | |||||
import { Card, Modal } from "@mui/material"; | |||||
import TimesheetInputGrid from "./TimesheetInputGrid"; | |||||
// import { fetchTimesheets } from "@/app/api/timesheets"; | |||||
interface EnterTimesheetModalProps { | |||||
isOpen: boolean; | |||||
onClose: () => void; | |||||
modalStyle?: any; | |||||
} | |||||
const EnterTimesheetModal: React.FC<EnterTimesheetModalProps> = ({ | |||||
...props | |||||
}) => { | |||||
const [lockConfirm, setLockConfirm] = useState(false); | |||||
const columns = [ | |||||
{ | |||||
id: "projectCode", | |||||
field: "projectCode", | |||||
headerName: "Project Code and Name", | |||||
flex: 1, | |||||
}, | |||||
{ | |||||
id: "task", | |||||
field: "task", | |||||
headerName: "Task", | |||||
flex: 1, | |||||
}, | |||||
]; | |||||
const rows = [ | |||||
{ | |||||
id: 1, | |||||
projectCode: "M1001", | |||||
task: "1.2", | |||||
}, | |||||
{ | |||||
id: 2, | |||||
projectCode: "M1301", | |||||
task: "1.1", | |||||
}, | |||||
]; | |||||
const fetchTimesheet = async () => { | |||||
// fetchTimesheets(); | |||||
// const res = await fetch(`http://localhost:8090/api/timesheets`, { | |||||
// // const res = await fetch(`${BASE_API_URL}/timesheets`, { | |||||
// method: "GET", | |||||
// mode: 'no-cors', | |||||
// }); | |||||
// console.log(res.json); | |||||
}; | |||||
return ( | |||||
<Modal open={props.isOpen} onClose={props.onClose}> | |||||
<div> | |||||
{/* <Typography variant="h5" id="modal-title" sx={{flex:1}}> | |||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}> | |||||
Timesheet Input | |||||
</div> | |||||
</Typography> */} | |||||
<Card style={{ | |||||
flex: 10, | |||||
marginBottom: "20px", | |||||
width: "90%", | |||||
// height: "80%", | |||||
position: "fixed", | |||||
top: "50%", | |||||
left: "50%", | |||||
transform: "translate(-50%, -50%)", | |||||
}}> | |||||
<TimesheetInputGrid setLockConfirm={setLockConfirm}/> | |||||
<div | |||||
style={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
width: "100%", | |||||
flex: 1, | |||||
padding: "20px", | |||||
}} | |||||
> | |||||
<Button | |||||
disabled={lockConfirm} | |||||
variant="contained" | |||||
onClick={props.onClose} | |||||
> | |||||
Confirm | |||||
</Button> | |||||
<Button | |||||
variant="contained" | |||||
onClick={props.onClose} | |||||
sx={{ "background-color": "#F890A5" }} | |||||
> | |||||
Cancel | |||||
</Button> | |||||
</div> | |||||
</Card> | |||||
</div> | |||||
</Modal> | |||||
); | |||||
}; | |||||
export default EnterTimesheetModal; |
@@ -1,548 +0,0 @@ | |||||
"use client"; | |||||
import Grid from "@mui/material/Grid"; | |||||
import Paper from "@mui/material/Paper"; | |||||
import { useState, useEffect } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import PageTitle from "../PageTitle/PageTitle"; | |||||
import { Suspense } from "react"; | |||||
import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import Link from "next/link"; | |||||
import { t } from "i18next"; | |||||
import { | |||||
Box, | |||||
Container, | |||||
Modal, | |||||
Select, | |||||
SelectChangeEvent, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import { Close } from "@mui/icons-material"; | |||||
import AddIcon from "@mui/icons-material/Add"; | |||||
import EditIcon from "@mui/icons-material/Edit"; | |||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined"; | |||||
import SaveIcon from "@mui/icons-material/Save"; | |||||
import CancelIcon from "@mui/icons-material/Close"; | |||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; | |||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
import Swal from "sweetalert2"; | |||||
import { msg } from "../Swal/CustomAlerts"; | |||||
import React from "react"; | |||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
import { | |||||
GridRowsProp, | |||||
GridRowModesModel, | |||||
GridRowModes, | |||||
DataGrid, | |||||
GridColDef, | |||||
GridToolbarContainer, | |||||
GridFooterContainer, | |||||
GridActionsCellItem, | |||||
GridEventListener, | |||||
GridRowId, | |||||
GridRowModel, | |||||
GridRowEditStopReasons, | |||||
GridEditInputCell, | |||||
GridValueSetterParams, | |||||
} from "@mui/x-data-grid"; | |||||
import { LocalizationProvider } from "@mui/x-date-pickers"; | |||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | |||||
import { Props } from "react-intl/src/components/relative"; | |||||
const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; | |||||
interface BottomBarProps { | |||||
getHoursTotal: (column: string) => number; | |||||
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
interface EditToolbarProps { | |||||
// setDay: (newDay : dayjs.Dayjs) => void; | |||||
setDay: (newDay: (oldDay: dayjs.Dayjs) => dayjs.Dayjs) => void; | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
interface EditFooterProps { | |||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void; | |||||
setRowModesModel: ( | |||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel, | |||||
) => void; | |||||
} | |||||
const EditToolbar = (props: EditToolbarProps) => { | |||||
const { setDay } = props; | |||||
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||||
const handleClickLeft = () => { | |||||
if (selectedDate) { | |||||
const newDate = selectedDate.add(-7, "day"); | |||||
setSelectedDate(newDate); | |||||
} | |||||
}; | |||||
const handleClickRight = () => { | |||||
if (selectedDate) { | |||||
const newDate = | |||||
selectedDate.add(7, "day") > dayjs() | |||||
? dayjs() | |||||
: selectedDate.add(7, "day"); | |||||
setSelectedDate(newDate); | |||||
} | |||||
}; | |||||
const handleDateChange = (date: dayjs.Dayjs | Date | null) => { | |||||
const newDate = dayjs(date); | |||||
setSelectedDate(newDate); | |||||
}; | |||||
useEffect(() => { | |||||
setDay((oldDay) => selectedDate); | |||||
}, [selectedDate]); | |||||
return ( | |||||
<LocalizationProvider dateAdapter={AdapterDayjs}> | |||||
<div | |||||
style={{ | |||||
display: "flex", | |||||
justifyContent: "flex-end", | |||||
width: "100%", | |||||
paddingBottom: "20px", | |||||
}} | |||||
> | |||||
<Typography variant="h5" id="modal-title" sx={{ flex: 1 }}> | |||||
Timesheet Input | |||||
</Typography> | |||||
<Button | |||||
sx={{ "border-radius": "30%", marginRight: "20px" }} | |||||
variant="contained" | |||||
onClick={handleClickLeft} | |||||
> | |||||
<ArrowBackIcon /> | |||||
</Button> | |||||
<DatePicker | |||||
value={selectedDate} | |||||
onChange={handleDateChange} | |||||
disableFuture={true} | |||||
/> | |||||
<Button | |||||
sx={{ "border-radius": "30%", margin: "0px 20px 0px 20px" }} | |||||
variant="contained" | |||||
onClick={handleClickRight} | |||||
> | |||||
<ArrowForwardIcon /> | |||||
</Button> | |||||
</div> | |||||
</LocalizationProvider> | |||||
); | |||||
}; | |||||
const BottomBar = (props: BottomBarProps) => { | |||||
const { setRows, setRowModesModel, getHoursTotal, setLockConfirm } = props; | |||||
// const getHoursTotal = props.getHoursTotal; | |||||
const [newId, setNewId] = useState(-1); | |||||
const [invalidDays, setInvalidDays] = useState(0); | |||||
const handleAddClick = () => { | |||||
const id = newId; | |||||
setNewId(newId - 1); | |||||
setRows((oldRows) => [ | |||||
...oldRows, | |||||
{ id, projectCode: "", task: "", isNew: true }, | |||||
]); | |||||
setRowModesModel((oldModel) => ({ | |||||
...oldModel, | |||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: "projectCode" }, | |||||
})); | |||||
}; | |||||
const totalColDef = { | |||||
flex: 1, | |||||
// style: {color:getHoursTotal('mon')>24?"red":"black"} | |||||
}; | |||||
const TotalCell = ({ value }: Props) => { | |||||
const [invalid, setInvalid] = useState(false); | |||||
useEffect(() => { | |||||
const newInvalid = (value ?? 0) > 24; | |||||
setInvalid(newInvalid); | |||||
}, [value]); | |||||
return ( | |||||
<Box flex={1} style={{ color: invalid ? "red" : "black" }}> | |||||
{value} | |||||
</Box> | |||||
); | |||||
}; | |||||
const checkUnlockConfirmBtn = () => { | |||||
// setLockConfirm((oldLock)=> valid); | |||||
setLockConfirm((oldLock) => | |||||
weekdays.every((weekday) => { | |||||
getHoursTotal(weekday) <= 24; | |||||
}), | |||||
); | |||||
}; | |||||
return ( | |||||
<div> | |||||
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||||
<Box flex={5.7} textAlign={"right"} marginRight="4rem"> | |||||
<b>Total:</b> | |||||
</Box> | |||||
<TotalCell value={getHoursTotal("mon")} /> | |||||
<TotalCell value={getHoursTotal("tue")} /> | |||||
<TotalCell value={getHoursTotal("wed")} /> | |||||
<TotalCell value={getHoursTotal("thu")} /> | |||||
<TotalCell value={getHoursTotal("fri")} /> | |||||
<TotalCell value={getHoursTotal("sat")} /> | |||||
<TotalCell value={getHoursTotal("sun")} /> | |||||
</div> | |||||
<Button | |||||
variant="outlined" | |||||
color="primary" | |||||
startIcon={<AddIcon />} | |||||
onClick={handleAddClick} | |||||
> | |||||
Add record | |||||
</Button> | |||||
</div> | |||||
); | |||||
}; | |||||
const EditFooter = (props: EditFooterProps) => { | |||||
return ( | |||||
<div style={{ display: "flex", justifyContent: "flex", width: "100%" }}> | |||||
<Box flex={1}> | |||||
<b>Total: </b> | |||||
</Box> | |||||
<Box flex={2}>ssss</Box> | |||||
</div> | |||||
); | |||||
}; | |||||
interface TimesheetInputGridProps { | |||||
setLockConfirm: (newLock: (oldLock: boolean) => boolean) => void; | |||||
onClose?: () => void; | |||||
} | |||||
const initialRows: GridRowsProp = [ | |||||
{ | |||||
id: 1, | |||||
projectCode: "M1001", | |||||
task: "1.2", | |||||
mon: 2.5, | |||||
}, | |||||
{ | |||||
id: 2, | |||||
projectCode: "M1002", | |||||
task: "1.3", | |||||
mon: 3.25, | |||||
}, | |||||
]; | |||||
const options = ["M1001", "M1301", "M1354", "M1973"]; | |||||
const options2 = [ | |||||
"1.1 - Preparation of preliminary Cost Estimate / Cost Plan", | |||||
"1.2 - Cash flow forecast", | |||||
"1.3 - Cost studies fo alterative design solutions", | |||||
"1.4 = Attend design co-ordination / project review meetings", | |||||
"1.5 - Prepare / Review RIC", | |||||
]; | |||||
const getDateForHeader = (date: dayjs.Dayjs, weekday: number) => { | |||||
if (date.day() == 0) { | |||||
return date.add(weekday - date.day() - 7, "day").format("DD MMM"); | |||||
} else { | |||||
return date.add(weekday - date.day(), "day").format("DD MMM"); | |||||
} | |||||
}; | |||||
const TimesheetInputGrid: React.FC<TimesheetInputGridProps> = ({ | |||||
...props | |||||
}) => { | |||||
const [rows, setRows] = useState(initialRows); | |||||
const [day, setDay] = useState(dayjs()); | |||||
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>( | |||||
{}, | |||||
); | |||||
const { setLockConfirm } = props; | |||||
const handleRowEditStop: GridEventListener<"rowEditStop"> = ( | |||||
params, | |||||
event, | |||||
) => { | |||||
if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
event.defaultMuiPrevented = true; | |||||
} | |||||
}; | |||||
const handleEditClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); | |||||
}; | |||||
const handleSaveClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); | |||||
}; | |||||
const handleDeleteClick = (id: GridRowId) => () => { | |||||
setRows(rows.filter((row) => row.id !== id)); | |||||
}; | |||||
const handleCancelClick = (id: GridRowId) => () => { | |||||
setRowModesModel({ | |||||
...rowModesModel, | |||||
[id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||||
}); | |||||
const editedRow = rows.find((row) => row.id === id); | |||||
if (editedRow!.isNew) { | |||||
setRows(rows.filter((row) => row.id !== id)); | |||||
} | |||||
}; | |||||
const processRowUpdate = (newRow: GridRowModel) => { | |||||
const updatedRow = { ...newRow, isNew: false }; | |||||
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||||
return updatedRow; | |||||
}; | |||||
const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { | |||||
setRowModesModel(newRowModesModel); | |||||
}; | |||||
const getHoursTotal = (column: any) => { | |||||
let sum = 0; | |||||
rows.forEach((row) => { | |||||
sum += row[column] ?? 0; | |||||
}); | |||||
return sum; | |||||
}; | |||||
const weekdayColConfig: any = { | |||||
type: "number", | |||||
// sortable: false, | |||||
//width: 100, | |||||
flex: 1, | |||||
align: "left", | |||||
headerAlign: "left", | |||||
editable: true, | |||||
renderEditCell: (value: any) => ( | |||||
<GridEditInputCell | |||||
{...value} | |||||
inputProps={{ | |||||
max: 24, | |||||
min: 0, | |||||
step: 0.25, | |||||
}} | |||||
/> | |||||
), | |||||
}; | |||||
const columns: GridColDef[] = [ | |||||
{ | |||||
field: "actions", | |||||
type: "actions", | |||||
headerName: "Actions", | |||||
width: 100, | |||||
cellClassName: "actions", | |||||
getActions: ({ id }) => { | |||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||||
if (isInEditMode) { | |||||
return [ | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-save`} | |||||
icon={<SaveIcon />} | |||||
title="Save" | |||||
label="Save" | |||||
sx={{ | |||||
color: "primary.main", | |||||
}} | |||||
onClick={handleSaveClick(id)} | |||||
/>, | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-cancel`} | |||||
icon={<CancelIcon />} | |||||
title="Cancel" | |||||
label="Cancel" | |||||
className="textPrimary" | |||||
onClick={handleCancelClick(id)} | |||||
color="inherit" | |||||
/>, | |||||
]; | |||||
} | |||||
return [ | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-edit`} | |||||
icon={<EditIcon />} | |||||
title="Edit" | |||||
label="Edit" | |||||
className="textPrimary" | |||||
onClick={handleEditClick(id)} | |||||
color="inherit" | |||||
/>, | |||||
<GridActionsCellItem | |||||
key={`actions-${id}-delete`} | |||||
title="Delete" | |||||
label="Delete" | |||||
icon={<DeleteIcon />} | |||||
onClick={handleDeleteClick(id)} | |||||
sx={{ color: "red" }} | |||||
/>, | |||||
]; | |||||
}, | |||||
}, | |||||
{ | |||||
field: "projectCode", | |||||
headerName: "Project Code", | |||||
// width: 220, | |||||
flex: 2, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions: options, | |||||
}, | |||||
{ | |||||
field: "task", | |||||
headerName: "Task", | |||||
// width: 220, | |||||
flex: 3, | |||||
editable: true, | |||||
type: "singleSelect", | |||||
valueOptions: options2, | |||||
}, | |||||
{ | |||||
// Mon | |||||
field: "mon", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Mon - {getDateForHeader(day, 1)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Tue | |||||
field: "tue", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Tue - {getDateForHeader(day, 2)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Wed | |||||
field: "wed", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Wed - {getDateForHeader(day, 3)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Thu | |||||
field: "thu", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Thu - {getDateForHeader(day, 4)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Fri | |||||
field: "fri", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Fri - {getDateForHeader(day, 5)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Sat | |||||
field: "sat", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return <div>Sat - {getDateForHeader(day, 6)}</div>; | |||||
}, | |||||
}, | |||||
{ | |||||
// Sun | |||||
field: "sun", | |||||
...weekdayColConfig, | |||||
renderHeader: () => { | |||||
return ( | |||||
<div style={{ color: "red" }}>Sun - {getDateForHeader(day, 7)}</div> | |||||
); | |||||
}, | |||||
}, | |||||
// { | |||||
// field: 'joinDate', | |||||
// headerName: 'Join date', | |||||
// type: 'date', | |||||
// width: 180, | |||||
// editable: true, | |||||
// }, | |||||
]; | |||||
return ( | |||||
<Box | |||||
sx={{ | |||||
// marginBottom: '-5px', | |||||
display: "flex", | |||||
"flex-direction": "column", | |||||
// 'justify-content': 'flex-end', | |||||
padding: "20px", | |||||
height: "100%", //'30rem', | |||||
width: "100%", | |||||
"& .actions": { | |||||
color: "text.secondary", | |||||
}, | |||||
"& .header": { | |||||
// border: 1, | |||||
// 'border-width': '1px', | |||||
// 'border-color': 'grey', | |||||
}, | |||||
"& .textPrimary": { | |||||
color: "text.primary", | |||||
}, | |||||
}} | |||||
> | |||||
<DataGrid | |||||
rows={rows} | |||||
columns={columns} | |||||
editMode="row" | |||||
rowModesModel={rowModesModel} | |||||
onRowModesModelChange={handleRowModesModelChange} | |||||
onRowEditStop={handleRowEditStop} | |||||
processRowUpdate={processRowUpdate} | |||||
disableRowSelectionOnClick={true} | |||||
disableColumnMenu={true} | |||||
hideFooterPagination={true} | |||||
slots={{ | |||||
toolbar: EditToolbar, | |||||
// footer: EditFooter, | |||||
}} | |||||
slotProps={{ | |||||
toolbar: { setDay, setRows, setRowModesModel }, | |||||
// footer: { setDay, setRows, setRowModesModel }, | |||||
}} | |||||
initialState={{ | |||||
pagination: { paginationModel: { pageSize: 100 } }, | |||||
}} | |||||
sx={{ flex: 1 }} | |||||
/> | |||||
<BottomBar | |||||
getHoursTotal={getHoursTotal} | |||||
setRows={setRows} | |||||
setRowModesModel={setRowModesModel} | |||||
setLockConfirm={setLockConfirm} | |||||
// sx={{flex:3}} | |||||
/> | |||||
</Box> | |||||
); | |||||
}; | |||||
export default TimesheetInputGrid; |
@@ -1 +0,0 @@ | |||||
export { default } from "./EnterTimesheetModal"; |
@@ -100,7 +100,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic | |||||
formData.append('multipartFileList', file); | formData.append('multipartFileList', file); | ||||
const response = await importIssuedInovice(formData); | const response = await importIssuedInovice(formData); | ||||
console.log(response); | |||||
// response: status, message, projectList, emptyRowList, invoiceList | |||||
if (response.status) { | if (response.status) { | ||||
successDialog(t("Import Success"), t).then(() => { | successDialog(t("Import Success"), t).then(() => { | ||||
@@ -0,0 +1,285 @@ | |||||
import React, { useCallback, useMemo, useState } from "react"; | |||||
import { HolidaysResult } from "@/app/api/holidays"; | |||||
import { LeaveType } from "@/app/api/timesheets"; | |||||
import dayGridPlugin from "@fullcalendar/daygrid"; | |||||
import interactionPlugin from "@fullcalendar/interaction"; | |||||
import { Box, useTheme } from "@mui/material"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { | |||||
getHolidayForDate, | |||||
getPublicHolidaysForNYears, | |||||
} from "@/app/utils/holidayUtils"; | |||||
import { | |||||
INPUT_DATE_FORMAT, | |||||
convertDateArrayToString, | |||||
} from "@/app/utils/formatUtil"; | |||||
import StyledFullCalendar from "../StyledFullCalendar"; | |||||
import { ProjectWithTasks } from "@/app/api/projects"; | |||||
import { | |||||
LeaveEntry, | |||||
RecordLeaveInput, | |||||
RecordTimesheetInput, | |||||
saveLeave, | |||||
} from "@/app/api/timesheets/actions"; | |||||
import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | |||||
import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | |||||
import dayjs from "dayjs"; | |||||
import { checkTotalHours } from "@/app/api/timesheets/utils"; | |||||
import unionBy from "lodash/unionBy"; | |||||
export interface Props { | |||||
leaveTypes: LeaveType[]; | |||||
companyHolidays: HolidaysResult[]; | |||||
allProjects: ProjectWithTasks[]; | |||||
leaveRecords: RecordLeaveInput; | |||||
timesheetRecords: RecordTimesheetInput; | |||||
isFullTime: boolean; | |||||
} | |||||
interface EventClickArg { | |||||
event: { | |||||
start: Date | null; | |||||
startStr: string; | |||||
extendedProps: { | |||||
calendar?: string; | |||||
entry?: LeaveEntry; | |||||
}; | |||||
}; | |||||
} | |||||
const LeaveCalendar: React.FC<Props> = ({ | |||||
companyHolidays, | |||||
allProjects, | |||||
leaveTypes, | |||||
timesheetRecords, | |||||
leaveRecords, | |||||
isFullTime, | |||||
}) => { | |||||
const { t } = useTranslation(["home", "common"]); | |||||
const theme = useTheme(); | |||||
const projectMap = useMemo(() => { | |||||
return allProjects.reduce<{ | |||||
[id: ProjectWithTasks["id"]]: ProjectWithTasks; | |||||
}>((acc, project) => { | |||||
return { ...acc, [project.id]: project }; | |||||
}, {}); | |||||
}, [allProjects]); | |||||
const leaveMap = useMemo(() => { | |||||
return leaveTypes.reduce<{ [id: LeaveType["id"]]: string }>( | |||||
(acc, leaveType) => ({ ...acc, [leaveType.id]: leaveType.name }), | |||||
{}, | |||||
); | |||||
}, [leaveTypes]); | |||||
const [localLeaveRecords, setLocalLeaveEntries] = useState(leaveRecords); | |||||
// leave edit modal related | |||||
const [leaveEditModalProps, setLeaveEditModalProps] = useState< | |||||
Partial<LeaveEditModalProps> | |||||
>({}); | |||||
const [leaveEditModalOpen, setLeaveEditModalOpen] = useState(false); | |||||
const openLeaveEditModal = useCallback( | |||||
(defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { | |||||
setLeaveEditModalProps({ | |||||
defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
recordDate, | |||||
isHoliday, | |||||
onDelete: defaultValues | |||||
? async () => { | |||||
if (!recordDate || !leaveRecords[recordDate]) { | |||||
return; | |||||
} | |||||
const leaveEntriesAtDate = leaveRecords[recordDate]; | |||||
const newLeaveRecords = { | |||||
...leaveRecords, | |||||
[recordDate!]: leaveEntriesAtDate.filter( | |||||
(e) => e.id !== defaultValues.id, | |||||
), | |||||
}; | |||||
const savedLeaveRecords = await saveLeave(newLeaveRecords); | |||||
setLocalLeaveEntries(savedLeaveRecords); | |||||
setLeaveEditModalOpen(false); | |||||
} | |||||
: undefined, | |||||
}); | |||||
setLeaveEditModalOpen(true); | |||||
}, | |||||
[leaveRecords], | |||||
); | |||||
const closeLeaveEditModal = useCallback(() => { | |||||
setLeaveEditModalOpen(false); | |||||
}, []); | |||||
// calendar related | |||||
const holidays = useMemo(() => { | |||||
return [ | |||||
...getPublicHolidaysForNYears(2), | |||||
...companyHolidays.map((h) => ({ | |||||
title: h.name, | |||||
date: convertDateArrayToString(h.date, INPUT_DATE_FORMAT), | |||||
extendedProps: { | |||||
calender: "holiday", | |||||
}, | |||||
})), | |||||
].map((e) => ({ | |||||
...e, | |||||
backgroundColor: theme.palette.error.main, | |||||
borderColor: theme.palette.error.main, | |||||
})); | |||||
}, [companyHolidays, theme.palette.error.main]); | |||||
const leaveEntries = useMemo( | |||||
() => | |||||
Object.keys(localLeaveRecords).flatMap((date, index) => { | |||||
return localLeaveRecords[date].map((entry) => ({ | |||||
id: `${date}-${index}-leave-${entry.id}`, | |||||
date, | |||||
title: `${t("{{count}} hour", { | |||||
ns: "common", | |||||
count: entry.inputHours || 0, | |||||
})} (${leaveMap[entry.leaveTypeId]})`, | |||||
backgroundColor: theme.palette.warning.light, | |||||
borderColor: theme.palette.warning.light, | |||||
textColor: theme.palette.text.primary, | |||||
extendedProps: { | |||||
calendar: "leaveEntry", | |||||
entry, | |||||
}, | |||||
})); | |||||
}), | |||||
[leaveMap, localLeaveRecords, t, theme], | |||||
); | |||||
const timeEntries = useMemo( | |||||
() => | |||||
Object.keys(timesheetRecords).flatMap((date, index) => { | |||||
return timesheetRecords[date].map((entry) => ({ | |||||
id: `${date}-${index}-time-${entry.id}`, | |||||
date, | |||||
title: `${t("{{count}} hour", { | |||||
ns: "common", | |||||
count: (entry.inputHours || 0) + (entry.otHours || 0), | |||||
})} (${ | |||||
entry.projectId | |||||
? projectMap[entry.projectId].code | |||||
: t("Non-billable task") | |||||
})`, | |||||
backgroundColor: theme.palette.info.main, | |||||
borderColor: theme.palette.info.main, | |||||
extendedProps: { | |||||
calendar: "timeEntry", | |||||
entry, | |||||
}, | |||||
})); | |||||
}), | |||||
[projectMap, timesheetRecords, t, theme], | |||||
); | |||||
const handleEventClick = useCallback( | |||||
({ event }: EventClickArg) => { | |||||
const dayJsObj = dayjs(event.startStr); | |||||
const holiday = getHolidayForDate(event.startStr, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
if ( | |||||
event.extendedProps.calendar === "leaveEntry" && | |||||
event.extendedProps.entry | |||||
) { | |||||
openLeaveEditModal( | |||||
event.extendedProps.entry as LeaveEntry, | |||||
event.startStr, | |||||
Boolean(isHoliday), | |||||
); | |||||
} | |||||
}, | |||||
[companyHolidays, openLeaveEditModal], | |||||
); | |||||
const handleDateClick = useCallback( | |||||
(e: { dateStr: string; dayEl: HTMLElement }) => { | |||||
const dayJsObj = dayjs(e.dateStr); | |||||
const holiday = getHolidayForDate(e.dateStr, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
openLeaveEditModal(undefined, e.dateStr, Boolean(isHoliday)); | |||||
}, | |||||
[companyHolidays, openLeaveEditModal], | |||||
); | |||||
const checkTotalHoursForDate = useCallback( | |||||
(newEntry: LeaveEntry, date?: string) => { | |||||
if (!date) { | |||||
throw Error("Invalid date"); | |||||
} | |||||
const dayJsObj = dayjs(date); | |||||
const holiday = getHolidayForDate(date, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const leaves = localLeaveRecords[date] || []; | |||||
const timesheets = timesheetRecords[date] || []; | |||||
const leavesWithNewEntry = unionBy( | |||||
[newEntry as LeaveEntry], | |||||
leaves, | |||||
"id", | |||||
); | |||||
const totalHourError = checkTotalHours( | |||||
timesheets, | |||||
leavesWithNewEntry, | |||||
Boolean(isHoliday), | |||||
isFullTime, | |||||
); | |||||
if (totalHourError) throw Error(totalHourError); | |||||
}, | |||||
[companyHolidays, isFullTime, localLeaveRecords, timesheetRecords], | |||||
); | |||||
const handleSaveLeave = useCallback( | |||||
async (leaveEntry: LeaveEntry, recordDate?: string) => { | |||||
checkTotalHoursForDate(leaveEntry, recordDate); | |||||
const leaveEntriesAtDate = leaveRecords[recordDate!] || []; | |||||
const newLeaveRecords = { | |||||
...leaveRecords, | |||||
[recordDate!]: [ | |||||
...leaveEntriesAtDate.filter((e) => e.id !== leaveEntry.id), | |||||
leaveEntry, | |||||
], | |||||
}; | |||||
const savedLeaveRecords = await saveLeave(newLeaveRecords); | |||||
setLocalLeaveEntries(savedLeaveRecords); | |||||
setLeaveEditModalOpen(false); | |||||
}, | |||||
[checkTotalHoursForDate, leaveRecords], | |||||
); | |||||
return ( | |||||
<Box> | |||||
<StyledFullCalendar | |||||
plugins={[dayGridPlugin, interactionPlugin]} | |||||
initialView="dayGridMonth" | |||||
buttonText={{ today: t("Today") }} | |||||
events={[...holidays, ...timeEntries, ...leaveEntries]} | |||||
eventClick={handleEventClick} | |||||
dateClick={handleDateClick} | |||||
/> | |||||
<LeaveEditModal | |||||
modalSx={{ maxWidth: 400 }} | |||||
leaveTypes={leaveTypes} | |||||
open={leaveEditModalOpen} | |||||
onClose={closeLeaveEditModal} | |||||
onSave={handleSaveLeave} | |||||
{...leaveEditModalProps} | |||||
/> | |||||
</Box> | |||||
); | |||||
}; | |||||
export default LeaveCalendar; |
@@ -1,46 +1,16 @@ | |||||
import React, { useCallback, useEffect, useMemo } from "react"; | |||||
import useIsMobile from "@/app/utils/useIsMobile"; | |||||
import React from "react"; | |||||
import FullscreenModal from "../FullscreenModal"; | |||||
import { | import { | ||||
Box, | Box, | ||||
Button, | |||||
Card, | Card, | ||||
CardActions, | |||||
CardContent, | CardContent, | ||||
Modal, | Modal, | ||||
ModalProps, | |||||
SxProps, | SxProps, | ||||
Typography, | Typography, | ||||
} from "@mui/material"; | } from "@mui/material"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { Check, Close } from "@mui/icons-material"; | |||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { | |||||
RecordLeaveInput, | |||||
RecordTimesheetInput, | |||||
saveLeave, | |||||
} from "@/app/api/timesheets/actions"; | |||||
import dayjs from "dayjs"; | |||||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
import LeaveTable from "../LeaveTable"; | |||||
import { LeaveType } from "@/app/api/timesheets"; | |||||
import FullscreenModal from "../FullscreenModal"; | |||||
import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; | |||||
import useIsMobile from "@/app/utils/useIsMobile"; | |||||
import { HolidaysResult } from "@/app/api/holidays"; | |||||
import { | |||||
DAILY_NORMAL_MAX_HOURS, | |||||
TIMESHEET_DAILY_MAX_HOURS, | |||||
validateLeaveRecord, | |||||
} from "@/app/api/timesheets/utils"; | |||||
import ErrorAlert from "../ErrorAlert"; | |||||
interface Props { | |||||
isOpen: boolean; | |||||
onClose: () => void; | |||||
defaultLeaveRecords?: RecordLeaveInput; | |||||
leaveTypes: LeaveType[]; | |||||
timesheetRecords: RecordTimesheetInput; | |||||
companyHolidays: HolidaysResult[]; | |||||
} | |||||
import LeaveCalendar, { Props as LeaveCalendarProps } from "./LeaveCalendar"; | |||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
position: "absolute", | position: "absolute", | ||||
@@ -52,167 +22,58 @@ const modalSx: SxProps = { | |||||
maxWidth: 1400, | maxWidth: 1400, | ||||
}; | }; | ||||
interface Props extends LeaveCalendarProps { | |||||
open: boolean; | |||||
onClose: () => void; | |||||
} | |||||
const LeaveModal: React.FC<Props> = ({ | const LeaveModal: React.FC<Props> = ({ | ||||
isOpen, | |||||
open, | |||||
onClose, | onClose, | ||||
defaultLeaveRecords, | |||||
timesheetRecords, | |||||
leaveTypes, | leaveTypes, | ||||
companyHolidays, | companyHolidays, | ||||
allProjects, | |||||
leaveRecords, | |||||
timesheetRecords, | |||||
isFullTime, | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
const isMobile = useIsMobile(); | |||||
const defaultValues = useMemo(() => { | |||||
const today = dayjs(); | |||||
return Array(7) | |||||
.fill(undefined) | |||||
.reduce<RecordLeaveInput>((acc, _, index) => { | |||||
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
return { | |||||
...acc, | |||||
[date]: defaultLeaveRecords?.[date] ?? [], | |||||
}; | |||||
}, {}); | |||||
}, [defaultLeaveRecords]); | |||||
const formProps = useForm<RecordLeaveInput>({ defaultValues }); | |||||
useEffect(() => { | |||||
formProps.reset(defaultValues); | |||||
}, [defaultValues, formProps]); | |||||
const onSubmit = useCallback<SubmitHandler<RecordLeaveInput>>( | |||||
async (data) => { | |||||
const errors = validateLeaveRecord( | |||||
data, | |||||
timesheetRecords, | |||||
companyHolidays, | |||||
); | |||||
if (errors) { | |||||
Object.keys(errors).forEach((date) => | |||||
formProps.setError(date, { | |||||
message: errors[date], | |||||
}), | |||||
); | |||||
return; | |||||
} | |||||
const savedRecords = await saveLeave(data); | |||||
const today = dayjs(); | |||||
const newFormValues = Array(7) | |||||
.fill(undefined) | |||||
.reduce<RecordLeaveInput>((acc, _, index) => { | |||||
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
return { | |||||
...acc, | |||||
[date]: savedRecords[date] ?? [], | |||||
}; | |||||
}, {}); | |||||
formProps.reset(newFormValues); | |||||
onClose(); | |||||
}, | |||||
[companyHolidays, formProps, onClose, timesheetRecords], | |||||
); | |||||
const onCancel = useCallback(() => { | |||||
formProps.reset(defaultValues); | |||||
onClose(); | |||||
}, [defaultValues, formProps, onClose]); | |||||
const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
(_, reason) => { | |||||
if (reason !== "backdropClick") { | |||||
onCancel(); | |||||
} | |||||
}, | |||||
[onCancel], | |||||
); | |||||
const errorComponent = ( | |||||
<ErrorAlert | |||||
errors={Object.keys(formProps.formState.errors).map((date) => { | |||||
const error = formProps.formState.errors[date]?.message; | |||||
return error | |||||
? `${date}: ${t(error, { | |||||
TIMESHEET_DAILY_MAX_HOURS, | |||||
DAILY_NORMAL_MAX_HOURS, | |||||
})}` | |||||
: undefined; | |||||
})} | |||||
const title = t("Record leave"); | |||||
const content = ( | |||||
<LeaveCalendar | |||||
isFullTime={isFullTime} | |||||
leaveTypes={leaveTypes} | |||||
companyHolidays={companyHolidays} | |||||
allProjects={allProjects} | |||||
leaveRecords={leaveRecords} | |||||
timesheetRecords={timesheetRecords} | |||||
/> | /> | ||||
); | ); | ||||
const matches = useIsMobile(); | |||||
return ( | |||||
<FormProvider {...formProps}> | |||||
{!matches ? ( | |||||
// Desktop version | |||||
<Modal open={isOpen} onClose={onModalClose}> | |||||
<Card sx={modalSx}> | |||||
<CardContent | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Record Leave")} | |||||
</Typography> | |||||
<Box | |||||
sx={{ | |||||
marginInline: -3, | |||||
marginBlock: 4, | |||||
}} | |||||
> | |||||
<LeaveTable | |||||
companyHolidays={companyHolidays} | |||||
leaveTypes={leaveTypes} | |||||
timesheetRecords={timesheetRecords} | |||||
/> | |||||
</Box> | |||||
{errorComponent} | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={onCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button variant="contained" startIcon={<Check />} type="submit"> | |||||
{t("Save")} | |||||
</Button> | |||||
</CardActions> | |||||
</CardContent> | |||||
</Card> | |||||
</Modal> | |||||
) : ( | |||||
// Mobile version | |||||
<FullscreenModal | |||||
open={isOpen} | |||||
onClose={onModalClose} | |||||
closeModal={onCancel} | |||||
> | |||||
<Box | |||||
display="flex" | |||||
flexDirection="column" | |||||
gap={2} | |||||
height="100%" | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Typography variant="h6" padding={2} flex="none"> | |||||
{t("Record Leave")} | |||||
</Typography> | |||||
<MobileLeaveTable | |||||
companyHolidays={companyHolidays} | |||||
leaveTypes={leaveTypes} | |||||
timesheetRecords={timesheetRecords} | |||||
errorComponent={errorComponent} | |||||
/> | |||||
return isMobile ? ( | |||||
<FullscreenModal open={open} onClose={onClose} closeModal={onClose}> | |||||
<Box display="flex" flexDirection="column" gap={2} height="100%"> | |||||
<Typography variant="h6" flex="none" padding={2}> | |||||
{title} | |||||
</Typography> | |||||
<Box paddingInline={2}>{content}</Box> | |||||
</Box> | |||||
</FullscreenModal> | |||||
) : ( | |||||
<Modal open={open} onClose={onClose}> | |||||
<Card sx={modalSx}> | |||||
<CardContent> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{title} | |||||
</Typography> | |||||
<Box maxHeight={900} overflow="scroll"> | |||||
{content} | |||||
</Box> | </Box> | ||||
</FullscreenModal> | |||||
)} | |||||
</FormProvider> | |||||
</CardContent> | |||||
</Card> | |||||
</Modal> | |||||
); | ); | ||||
}; | }; | ||||
@@ -1,5 +1,3 @@ | |||||
import { Grid } from "@mui/material"; | |||||
interface Props { | interface Props { | ||||
width?: number; | width?: number; | ||||
height?: number; | height?: number; | ||||
@@ -16,7 +14,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
<g clipPath="url(#a)"> | <g clipPath="url(#a)"> | ||||
<path id="logo" | <path id="logo" | ||||
fill="#89ba17" stroke="#89ba17" stroke-width="1" | |||||
fill="#89ba17" stroke="#89ba17" strokeWidth="1" | |||||
d="M 98.00,125.00 | d="M 98.00,125.00 | ||||
C 92.11,126.67 84.23,126.00 78.00,126.00 | C 92.11,126.67 84.23,126.00 78.00,126.00 | ||||
68.19,126.00 48.68,126.75 40.00,125.00 | 68.19,126.00 48.68,126.75 40.00,125.00 | ||||
@@ -66,7 +64,7 @@ const Logo: React.FC<Props> = ({ width, height }) => { | |||||
41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | 41.00,156.00 39.00,156.00 39.00,156.00 Z" /> | ||||
<path id="word" | <path id="word" | ||||
fill="#111927" stroke="#111927" stroke-width="1" | |||||
fill="#111927" stroke="#111927" strokeWidth="1" | |||||
d="M 273.00,64.00 | d="M 273.00,64.00 | ||||
C 273.00,64.00 279.96,66.35 279.96,66.35 | C 273.00,64.00 279.96,66.35 279.96,66.35 | ||||
283.26,67.45 289.15,67.63 290.83,63.79 | 283.26,67.45 289.15,67.63 290.83,63.79 | ||||
@@ -67,16 +67,16 @@ import { | |||||
MAINTAIN_GROUP, | MAINTAIN_GROUP, | ||||
MAINTAIN_HOLIDAY, | MAINTAIN_HOLIDAY, | ||||
VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | ||||
GENERATE_LATE_START_REPORTS, | |||||
GENERATE_LATE_START_REPORT, | |||||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
GENERATE_COST_AND_EXPENSE_REPORT, | |||||
GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
} from "@/middleware"; | } from "@/middleware"; | ||||
import { SessionWithAbilities } from "../AppBar/NavigationToggle"; | import { SessionWithAbilities } from "../AppBar/NavigationToggle"; | ||||
import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
@@ -190,16 +190,16 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
label: "Analysis Report", | label: "Analysis Report", | ||||
path: "", | path: "", | ||||
isHidden: ![ | isHidden: ![ | ||||
GENERATE_LATE_START_REPORTS, | |||||
GENERATE_LATE_START_REPORT, | |||||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
GENERATE_COST_AND_EXPENSE_REPORT, | |||||
GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
].some((ability) => | ].some((ability) => | ||||
abilities!.includes(ability), | abilities!.includes(ability), | ||||
), | ), | ||||
@@ -208,7 +208,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
icon: <Analytics />, | icon: <Analytics />, | ||||
label: "Late Start Report", | label: "Late Start Report", | ||||
path: "/analytics/LateStartReport", | path: "/analytics/LateStartReport", | ||||
isHidden: ![GENERATE_LATE_START_REPORTS].some((ability) => | |||||
isHidden: ![GENERATE_LATE_START_REPORT].some((ability) => | |||||
abilities!.includes(ability), | abilities!.includes(ability), | ||||
), | ), | ||||
}, | }, | ||||
@@ -232,7 +232,7 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||||
icon: <Analytics />, | icon: <Analytics />, | ||||
label: "Cost and Expense Report", | label: "Cost and Expense Report", | ||||
path: "/analytics/CostandExpenseReport", | path: "/analytics/CostandExpenseReport", | ||||
isHidden: ![GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => | |||||
isHidden: ![GENERATE_COST_AND_EXPENSE_REPORT].some((ability) => | |||||
abilities!.includes(ability), | abilities!.includes(ability), | ||||
), | ), | ||||
}, | }, | ||||
@@ -0,0 +1,196 @@ | |||||
import { | |||||
RecordLeaveInput, | |||||
RecordTimesheetInput, | |||||
} from "@/app/api/timesheets/actions"; | |||||
import { | |||||
Box, | |||||
Card, | |||||
CardActionArea, | |||||
CardContent, | |||||
Stack, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import union from "lodash/union"; | |||||
import { useCallback, useMemo } from "react"; | |||||
import dayjs, { Dayjs } from "dayjs"; | |||||
import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
import { HolidaysResult } from "@/app/api/holidays"; | |||||
import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import pickBy from "lodash/pickBy"; | |||||
interface Props { | |||||
currentMonth: Dayjs; | |||||
timesheet: RecordTimesheetInput; | |||||
leaves: RecordLeaveInput; | |||||
companyHolidays: HolidaysResult[]; | |||||
onDateSelect: (date: string) => void; | |||||
} | |||||
const MonthlySummary: React.FC<Props> = ({ | |||||
timesheet, | |||||
leaves, | |||||
currentMonth, | |||||
companyHolidays, | |||||
onDateSelect, | |||||
}) => { | |||||
const { | |||||
t, | |||||
i18n: { language }, | |||||
} = useTranslation("home"); | |||||
const timesheetForCurrentMonth = useMemo(() => { | |||||
return pickBy(timesheet, (_, date) => { | |||||
return currentMonth.isSame(dayjs(date), "month"); | |||||
}); | |||||
}, [currentMonth, timesheet]); | |||||
const leavesForCurrentMonth = useMemo(() => { | |||||
return pickBy(leaves, (_, date) => { | |||||
return currentMonth.isSame(dayjs(date), "month"); | |||||
}); | |||||
}, [currentMonth, leaves]); | |||||
const days = useMemo(() => { | |||||
return union( | |||||
Object.keys(timesheetForCurrentMonth), | |||||
Object.keys(leavesForCurrentMonth), | |||||
); | |||||
}, [timesheetForCurrentMonth, leavesForCurrentMonth]).sort(); | |||||
const makeSelectDate = useCallback( | |||||
(date: string) => () => { | |||||
onDateSelect(date); | |||||
}, | |||||
[onDateSelect], | |||||
); | |||||
return ( | |||||
<Stack | |||||
gap={2} | |||||
marginBlockEnd={2} | |||||
minWidth={{ sm: 375 }} | |||||
maxHeight={{ sm: 500 }} | |||||
> | |||||
<Typography variant="overline">{t("Monthly Summary")}</Typography> | |||||
<Box sx={{ overflowY: "scroll" }} flex={1}> | |||||
{days.map((day, index) => { | |||||
const dayJsObj = dayjs(day); | |||||
const holiday = getHolidayForDate(day, companyHolidays); | |||||
const isHoliday = | |||||
holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const ls = leavesForCurrentMonth[day]; | |||||
const leaveHours = | |||||
ls?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | |||||
const ts = timesheetForCurrentMonth[day]; | |||||
const timesheetNormalHours = | |||||
ts?.reduce((acc, entry) => acc + (entry.inputHours || 0), 0) || 0; | |||||
const timesheetOtHours = | |||||
ts?.reduce((acc, entry) => acc + (entry.otHours || 0), 0) || 0; | |||||
const timesheetHours = timesheetNormalHours + timesheetOtHours; | |||||
return ( | |||||
<Card | |||||
key={`${day}-${index}`} | |||||
sx={{ marginBlockEnd: 2, marginInline: 2 }} | |||||
> | |||||
<CardActionArea onClick={makeSelectDate(day)}> | |||||
<CardContent sx={{ padding: 3 }}> | |||||
<Typography | |||||
variant="overline" | |||||
component="div" | |||||
sx={{ | |||||
color: isHoliday ? "error.main" : undefined, | |||||
}} | |||||
> | |||||
{shortDateFormatter(language).format(dayJsObj.toDate())} | |||||
{holiday && ( | |||||
<Typography | |||||
marginInlineStart={1} | |||||
variant="caption" | |||||
>{`(${holiday.title})`}</Typography> | |||||
)} | |||||
</Typography> | |||||
<Stack spacing={1}> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
flexWrap: "wrap", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="body2"> | |||||
{t("Timesheet Hours")} | |||||
</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(timesheetHours)} | |||||
</Typography> | |||||
</Box> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
flexWrap: "wrap", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="body2"> | |||||
{t("Leave Hours")} | |||||
</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(leaveHours)} | |||||
</Typography> | |||||
</Box> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
flexWrap: "wrap", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="body2"> | |||||
{t("Daily Total Hours")} | |||||
</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(timesheetHours + leaveHours)} | |||||
</Typography> | |||||
</Box> | |||||
</Stack> | |||||
</CardContent> | |||||
</CardActionArea> | |||||
</Card> | |||||
); | |||||
})} | |||||
</Box> | |||||
<Typography variant="overline"> | |||||
{`${t("Total Monthly Work Hours")}: ${manhourFormatter.format( | |||||
Object.values(timesheetForCurrentMonth) | |||||
.flatMap((entries) => entries) | |||||
.map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) | |||||
.reduce((acc, cur) => { | |||||
return acc + cur; | |||||
}, 0), | |||||
)}`} | |||||
</Typography> | |||||
<Typography variant="overline"> | |||||
{`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format( | |||||
Object.values(leavesForCurrentMonth) | |||||
.flatMap((entries) => entries) | |||||
.map((entry) => entry.inputHours) | |||||
.reduce((acc, cur) => { | |||||
return acc + cur; | |||||
}, 0), | |||||
)}`} | |||||
</Typography> | |||||
</Stack> | |||||
); | |||||
}; | |||||
export default MonthlySummary; |
@@ -26,6 +26,7 @@ export interface Props { | |||||
timesheet: RecordTimesheetInput; | timesheet: RecordTimesheetInput; | ||||
leaves: RecordLeaveInput; | leaves: RecordLeaveInput; | ||||
onDateSelect: (date: string) => void; | onDateSelect: (date: string) => void; | ||||
onMonthChange: (day: Dayjs) => void; | |||||
} | } | ||||
const getColor = ( | const getColor = ( | ||||
@@ -72,6 +73,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||||
timesheet, | timesheet, | ||||
leaves, | leaves, | ||||
onDateSelect, | onDateSelect, | ||||
onMonthChange, | |||||
}) => { | }) => { | ||||
const { | const { | ||||
i18n: { language }, | i18n: { language }, | ||||
@@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC<Props> = ({ | |||||
> | > | ||||
<DateCalendar | <DateCalendar | ||||
onChange={onChange} | onChange={onChange} | ||||
onMonthChange={onMonthChange} | |||||
disableFuture | disableFuture | ||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
slots={{ day: EntryDay as any }} | slots={{ day: EntryDay as any }} | ||||
@@ -20,12 +20,17 @@ import { ProjectWithTasks } from "@/app/api/projects"; | |||||
import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
import useIsMobile from "@/app/utils/useIsMobile"; | import useIsMobile from "@/app/utils/useIsMobile"; | ||||
import FullscreenModal from "../FullscreenModal"; | import FullscreenModal from "../FullscreenModal"; | ||||
import MonthlySummary from "./MonthlySummary"; | |||||
import { HolidaysResult } from "@/app/api/holidays"; | |||||
import dayjs from "dayjs"; | |||||
interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> { | |||||
interface Props | |||||
extends Omit<PastEntryCalendarProps, "onDateSelect" | "onMonthChange"> { | |||||
open: boolean; | open: boolean; | ||||
handleClose: () => void; | handleClose: () => void; | ||||
leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
companyHolidays: HolidaysResult[]; | |||||
} | } | ||||
const Indicator = styled(Box)(() => ({ | const Indicator = styled(Box)(() => ({ | ||||
@@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
const [selectedDate, setSelectedDate] = useState(""); | const [selectedDate, setSelectedDate] = useState(""); | ||||
const [currentMonth, setMonthChange] = useState(dayjs()); | |||||
const clearDate = useCallback(() => { | const clearDate = useCallback(() => { | ||||
setSelectedDate(""); | setSelectedDate(""); | ||||
@@ -54,40 +60,52 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
handleClose(); | handleClose(); | ||||
}, [handleClose]); | }, [handleClose]); | ||||
const content = selectedDate ? ( | |||||
<> | |||||
<PastEntryList | |||||
date={selectedDate} | |||||
timesheet={timesheet} | |||||
leaves={leaves} | |||||
allProjects={allProjects} | |||||
leaveTypes={leaveTypes} | |||||
/> | |||||
</> | |||||
) : ( | |||||
<> | |||||
<Stack marginBlockEnd={2}> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "info.light" }} /> | |||||
<Typography variant="caption">{t("Has timesheet entry")}</Typography> | |||||
</Box> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "warning.light" }} /> | |||||
<Typography variant="caption">{t("Has leave entry")}</Typography> | |||||
</Box> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "success.light" }} /> | |||||
<Typography variant="caption"> | |||||
{t("Has both timesheet and leave entry")} | |||||
</Typography> | |||||
</Box> | |||||
</Stack> | |||||
<PastEntryCalendar | |||||
timesheet={timesheet} | |||||
leaves={leaves} | |||||
onDateSelect={setSelectedDate} | |||||
/> | |||||
</> | |||||
const content = ( | |||||
<Box sx={{ display: "flex", flexDirection: { xs: "column", sm: "row" } }}> | |||||
<Box> | |||||
<Stack marginBlockEnd={2}> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "info.light" }} /> | |||||
<Typography variant="caption"> | |||||
{t("Has timesheet entry")} | |||||
</Typography> | |||||
</Box> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "warning.light" }} /> | |||||
<Typography variant="caption">{t("Has leave entry")}</Typography> | |||||
</Box> | |||||
<Box display="flex" alignItems="center" gap={1}> | |||||
<Indicator sx={{ backgroundColor: "success.light" }} /> | |||||
<Typography variant="caption"> | |||||
{t("Has both timesheet and leave entry")} | |||||
</Typography> | |||||
</Box> | |||||
</Stack> | |||||
<PastEntryCalendar | |||||
timesheet={timesheet} | |||||
leaves={leaves} | |||||
onDateSelect={setSelectedDate} | |||||
onMonthChange={setMonthChange} | |||||
/> | |||||
</Box> | |||||
{selectedDate ? ( | |||||
<PastEntryList | |||||
date={selectedDate} | |||||
timesheet={timesheet} | |||||
leaves={leaves} | |||||
allProjects={allProjects} | |||||
leaveTypes={leaveTypes} | |||||
/> | |||||
) : ( | |||||
<MonthlySummary | |||||
currentMonth={currentMonth} | |||||
timesheet={timesheet} | |||||
leaves={leaves} | |||||
companyHolidays={[]} | |||||
onDateSelect={setSelectedDate} | |||||
/> | |||||
)} | |||||
</Box> | |||||
); | ); | ||||
const isMobile = useIsMobile(); | const isMobile = useIsMobile(); | ||||
@@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
startIcon={<ArrowBack />} | startIcon={<ArrowBack />} | ||||
onClick={clearDate} | onClick={clearDate} | ||||
> | > | ||||
{t("Back")} | |||||
{t("Back to Monthly Summary")} | |||||
</Button> | </Button> | ||||
)} | )} | ||||
</Box> | </Box> | ||||
</Box> | </Box> | ||||
</FullscreenModal> | </FullscreenModal> | ||||
) : ( | ) : ( | ||||
<Dialog onClose={onClose} open={open}> | |||||
<Dialog onClose={onClose} open={open} maxWidth="md"> | |||||
<DialogTitle>{t("Past Entries")}</DialogTitle> | <DialogTitle>{t("Past Entries")}</DialogTitle> | ||||
<DialogContent>{content}</DialogContent> | <DialogContent>{content}</DialogContent> | ||||
{selectedDate && ( | {selectedDate && ( | ||||
@@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({ | |||||
startIcon={<ArrowBack />} | startIcon={<ArrowBack />} | ||||
onClick={clearDate} | onClick={clearDate} | ||||
> | > | ||||
{t("Back")} | |||||
{t("Back to Monthly Summary")} | |||||
</Button> | </Button> | ||||
</DialogActions> | </DialogActions> | ||||
)} | )} | ||||
@@ -57,7 +57,12 @@ const PastEntryList: React.FC<Props> = ({ | |||||
const dayJsObj = dayjs(date); | const dayJsObj = dayjs(date); | ||||
return ( | return ( | ||||
<Stack gap={2} marginBlockEnd={2} minWidth={{ sm: 375 }}> | |||||
<Stack | |||||
gap={2} | |||||
marginBlockEnd={2} | |||||
minWidth={{ sm: 375 }} | |||||
maxHeight={{ sm: 500 }} | |||||
> | |||||
<Typography | <Typography | ||||
variant="overline" | variant="overline" | ||||
color={dayJsObj.day() === 0 ? "error.main" : undefined} | color={dayJsObj.day() === 0 ? "error.main" : undefined} | ||||
@@ -94,17 +99,25 @@ const PastEntryList: React.FC<Props> = ({ | |||||
leaveTypeMap={leaveTypeMap} | leaveTypeMap={leaveTypeMap} | ||||
/> | /> | ||||
))} | ))} | ||||
<Typography | |||||
variant="overline" | |||||
> | |||||
{t("Total Work Hours")}: {manhourFormatter.format(timeEntries.map(entry => (entry.inputHours ?? 0) + (entry.otHours ?? 0)).reduce((acc, cur) => { return acc + cur }, 0))} | |||||
</Typography> | |||||
<Typography | |||||
variant="overline" | |||||
> | |||||
{t("Total Leave Hours")}: {manhourFormatter.format(leaveEntries.map(entry => entry.inputHours).reduce((acc, cur) => { return acc + cur }, 0))} | |||||
</Typography> | |||||
</Box> | </Box> | ||||
<Typography variant="overline"> | |||||
{`${t("Total Work Hours")}: ${manhourFormatter.format( | |||||
timeEntries | |||||
.map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) | |||||
.reduce((acc, cur) => { | |||||
return acc + cur; | |||||
}, 0), | |||||
)}`} | |||||
</Typography> | |||||
<Typography variant="overline"> | |||||
{`${t("Total Leave Hours")}: ${manhourFormatter.format( | |||||
leaveEntries | |||||
.map((entry) => entry.inputHours) | |||||
.reduce((acc, cur) => { | |||||
return acc + cur; | |||||
}, 0), | |||||
)}`} | |||||
</Typography> | |||||
</Stack> | </Stack> | ||||
); | ); | ||||
}; | }; | ||||
@@ -409,7 +409,7 @@ const ProgressByClient: React.FC<Props> = () => { | |||||
const spentManhours = chartProjectSpentHour[dataPointIndex]; | const spentManhours = chartProjectSpentHour[dataPointIndex]; | ||||
const value = series[seriesIndex][dataPointIndex]; | const value = series[seriesIndex][dataPointIndex]; | ||||
const tooltipContent = ` | const tooltipContent = ` | ||||
<div style="width: 250px;"> | |||||
<div style="width: auto;"> | |||||
<span style="font-weight: bold;">${projectCode} - ${projectName}</span> | <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | ||||
<br> | <br> | ||||
Budget Manhours: ${budgetManhours} hours | Budget Manhours: ${budgetManhours} hours | ||||
@@ -492,7 +492,7 @@ const ProgressByTeam: React.FC = () => { | |||||
const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; | const spentManhours = currentPageProjectSpentManhourList[dataPointIndex]; | ||||
const value = series[seriesIndex][dataPointIndex]; | const value = series[seriesIndex][dataPointIndex]; | ||||
const tooltipContent = ` | const tooltipContent = ` | ||||
<div style="width: 100%;"> | |||||
<div style="width: auto;"> | |||||
<span style="font-weight: bold;">${projectCode} - ${projectName}</span> | <span style="font-weight: bold;">${projectCode} - ${projectName}</span> | ||||
<br> | <br> | ||||
Budget Manhours: ${budgetManhours} hours | Budget Manhours: ${budgetManhours} hours | ||||
@@ -954,7 +954,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
className="text-sm font-medium ml-5" | className="text-sm font-medium ml-5" | ||||
style={{ color: "#898d8d" }} | style={{ color: "#898d8d" }} | ||||
> | > | ||||
Accounts Receivable | |||||
Remaining Budget | |||||
</div> | </div> | ||||
<div | <div | ||||
className="text-lg font-medium ml-5 mb-2" | className="text-lg font-medium ml-5 mb-2" | ||||
@@ -362,7 +362,7 @@ const StaffUtilization: React.FC<Props> = ({ abilities, staff }) => { | |||||
const startCount = weeklyPlanned[i].startCount | const startCount = weeklyPlanned[i].startCount | ||||
const endCount = weeklyPlanned[i].endCount | const endCount = weeklyPlanned[i].endCount | ||||
for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { | for (var j = 0; j < weeklyPlanned[i].searchDuration; j++) { | ||||
if (j >= startCount && j < endCount) { | |||||
if (j >= startCount && j <= endCount) { | |||||
weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) | weeklyPlannedSubList.push(weeklyPlanned[i].AverageManhours) | ||||
} else { | } else { | ||||
weeklyPlannedSubList.push(0) | weeklyPlannedSubList.push(0) | ||||
@@ -503,7 +503,8 @@ const StaffUtilization: React.FC<Props> = ({ abilities, staff }) => { | |||||
const fetchMonthlyUnsubmittedData = async () => { | const fetchMonthlyUnsubmittedData = async () => { | ||||
const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); | |||||
const fetchResult = await fetchMonthlyUnsubmit(teamUnsubmitTeamId, unsubmitMonthlyFromValue.startOf('month').format('YYYY-MM-DD'), unsubmitMonthlyToValue.endOf('month').format('YYYY-MM-DD'), holidayDates); | |||||
const result = [] | const result = [] | ||||
const staffList = [] | const staffList = [] | ||||
var maxValue = 5 | var maxValue = 5 | ||||
@@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
}, {}); | }, {}); | ||||
}, [assignedProjects]); | }, [assignedProjects]); | ||||
const { getValues, setValue, clearErrors } = | |||||
const { getValues, setValue, clearErrors, setError } = | |||||
useFormContext<RecordTimeLeaveInput>(); | useFormContext<RecordTimeLeaveInput>(); | ||||
const currentEntries = getValues(day); | const currentEntries = getValues(day); | ||||
@@ -393,7 +393,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
params.row._error?.[ | params.row._error?.[ | ||||
params.field as keyof Omit<TimeLeaveEntry, "type"> | params.field as keyof Omit<TimeLeaveEntry, "type"> | ||||
]; | ]; | ||||
const content = <GridEditInputCell {...params} />; | |||||
const content = ( | |||||
<GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||||
); | |||||
return errorMessage ? ( | return errorMessage ? ( | ||||
<Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | ||||
<Box width="100%">{content}</Box> | <Box width="100%">{content}</Box> | ||||
@@ -423,7 +425,9 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
params.row._error?.[ | params.row._error?.[ | ||||
params.field as keyof Omit<TimeLeaveEntry, "type"> | params.field as keyof Omit<TimeLeaveEntry, "type"> | ||||
]; | ]; | ||||
const content = <GridEditInputCell {...params} />; | |||||
const content = ( | |||||
<GridEditInputCell {...params} inputProps={{ min: 0 }} /> | |||||
); | |||||
return errorMessage ? ( | return errorMessage ? ( | ||||
<Tooltip title={t(errorMessage)}> | <Tooltip title={t(errorMessage)}> | ||||
<Box width="100%">{content}</Box> | <Box width="100%">{content}</Box> | ||||
@@ -486,8 +490,13 @@ const TimeLeaveInputTable: React.FC<Props> = ({ | |||||
.filter((e): e is TimeLeaveEntry => Boolean(e)); | .filter((e): e is TimeLeaveEntry => Boolean(e)); | ||||
setValue(day, newEntries); | setValue(day, newEntries); | ||||
clearErrors(day); | |||||
}, [getValues, entries, setValue, day, clearErrors]); | |||||
if (entries.some((e) => e._isNew)) { | |||||
setError(day, { message: "There are some unsaved entries." }); | |||||
} else { | |||||
clearErrors(day); | |||||
} | |||||
}, [getValues, entries, setValue, day, clearErrors, setError]); | |||||
const hasOutOfPlannedStages = entries.some( | const hasOutOfPlannedStages = entries.some( | ||||
(entry) => entry._isPlanned !== undefined && !entry._isPlanned, | (entry) => entry._isPlanned !== undefined && !entry._isPlanned, | ||||
@@ -49,6 +49,7 @@ interface Props { | |||||
companyHolidays: HolidaysResult[]; | companyHolidays: HolidaysResult[]; | ||||
fastEntryEnabled?: boolean; | fastEntryEnabled?: boolean; | ||||
leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
isFullTime: boolean; | |||||
} | } | ||||
const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
@@ -71,6 +72,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
companyHolidays, | companyHolidays, | ||||
fastEntryEnabled, | fastEntryEnabled, | ||||
leaveTypes, | leaveTypes, | ||||
isFullTime | |||||
}) => { | }) => { | ||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
@@ -106,7 +108,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | const onSubmit = useCallback<SubmitHandler<RecordTimeLeaveInput>>( | ||||
async (data) => { | async (data) => { | ||||
const errors = validateTimeLeaveRecord(data, companyHolidays); | |||||
const errors = validateTimeLeaveRecord(data, companyHolidays, isFullTime); | |||||
if (errors) { | if (errors) { | ||||
Object.keys(errors).forEach((date) => | Object.keys(errors).forEach((date) => | ||||
formProps.setError(date, { | formProps.setError(date, { | ||||
@@ -131,7 +133,7 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
formProps.reset(newFormValues); | formProps.reset(newFormValues); | ||||
onClose(); | onClose(); | ||||
}, | }, | ||||
[companyHolidays, formProps, onClose], | |||||
[companyHolidays, formProps, onClose, isFullTime], | |||||
); | ); | ||||
const onCancel = useCallback(() => { | const onCancel = useCallback(() => { | ||||
@@ -347,6 +347,10 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
if (!date) { | if (!date) { | ||||
throw Error("Invalid date"); | throw Error("Invalid date"); | ||||
} | } | ||||
const dayJsObj = dayjs(date); | |||||
const holiday = getHolidayForDate(date, companyHolidays); | |||||
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
const intStaffId = parseInt(selectedStaff.id); | const intStaffId = parseInt(selectedStaff.id); | ||||
const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | ||||
const timesheets = | const timesheets = | ||||
@@ -360,7 +364,11 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
leaves, | leaves, | ||||
"id", | "id", | ||||
); | ); | ||||
totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||||
totalHourError = checkTotalHours( | |||||
timesheets, | |||||
leavesWithNewEntry, | |||||
Boolean(isHoliday), | |||||
); | |||||
} else { | } else { | ||||
// newEntry is a timesheet entry | // newEntry is a timesheet entry | ||||
const timesheetsWithNewEntry = unionBy( | const timesheetsWithNewEntry = unionBy( | ||||
@@ -368,11 +376,15 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
timesheets, | timesheets, | ||||
"id", | "id", | ||||
); | ); | ||||
totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves); | |||||
totalHourError = checkTotalHours( | |||||
timesheetsWithNewEntry, | |||||
leaves, | |||||
Boolean(isHoliday), | |||||
); | |||||
} | } | ||||
if (totalHourError) throw Error(totalHourError); | if (totalHourError) throw Error(totalHourError); | ||||
}, | }, | ||||
[localTeamLeaves, localTeamTimesheets, selectedStaff.id], | |||||
[localTeamLeaves, localTeamTimesheets, selectedStaff, companyHolidays], | |||||
); | ); | ||||
const handleSave = useCallback( | const handleSave = useCallback( | ||||
@@ -1,223 +0,0 @@ | |||||
import React, { useCallback, useEffect, useMemo } from "react"; | |||||
import { | |||||
Box, | |||||
Button, | |||||
Card, | |||||
CardActions, | |||||
CardContent, | |||||
Modal, | |||||
ModalProps, | |||||
SxProps, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import TimesheetTable from "../TimesheetTable"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Check, Close } from "@mui/icons-material"; | |||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
import { | |||||
RecordLeaveInput, | |||||
RecordTimesheetInput, | |||||
saveTimesheet, | |||||
} from "@/app/api/timesheets/actions"; | |||||
import dayjs from "dayjs"; | |||||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||||
import FullscreenModal from "../FullscreenModal"; | |||||
import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; | |||||
import useIsMobile from "@/app/utils/useIsMobile"; | |||||
import { HolidaysResult } from "@/app/api/holidays"; | |||||
import { | |||||
DAILY_NORMAL_MAX_HOURS, | |||||
TIMESHEET_DAILY_MAX_HOURS, | |||||
validateTimesheet, | |||||
} from "@/app/api/timesheets/utils"; | |||||
import ErrorAlert from "../ErrorAlert"; | |||||
interface Props { | |||||
isOpen: boolean; | |||||
onClose: () => void; | |||||
allProjects: ProjectWithTasks[]; | |||||
assignedProjects: AssignedProject[]; | |||||
defaultTimesheets?: RecordTimesheetInput; | |||||
leaveRecords: RecordLeaveInput; | |||||
companyHolidays: HolidaysResult[]; | |||||
fastEntryEnabled?: boolean; | |||||
} | |||||
const modalSx: SxProps = { | |||||
position: "absolute", | |||||
top: "50%", | |||||
left: "50%", | |||||
transform: "translate(-50%, -50%)", | |||||
width: { xs: "calc(100% - 2rem)", sm: "90%" }, | |||||
maxHeight: "90%", | |||||
maxWidth: 1400, | |||||
}; | |||||
const TimesheetModal: React.FC<Props> = ({ | |||||
isOpen, | |||||
onClose, | |||||
allProjects, | |||||
assignedProjects, | |||||
defaultTimesheets, | |||||
leaveRecords, | |||||
companyHolidays, | |||||
fastEntryEnabled, | |||||
}) => { | |||||
const { t } = useTranslation("home"); | |||||
const defaultValues = useMemo(() => { | |||||
const today = dayjs(); | |||||
return Array(7) | |||||
.fill(undefined) | |||||
.reduce<RecordTimesheetInput>((acc, _, index) => { | |||||
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
return { | |||||
...acc, | |||||
[date]: defaultTimesheets?.[date] ?? [], | |||||
}; | |||||
}, {}); | |||||
}, [defaultTimesheets]); | |||||
const formProps = useForm<RecordTimesheetInput>({ defaultValues }); | |||||
useEffect(() => { | |||||
formProps.reset(defaultValues); | |||||
}, [defaultValues, formProps]); | |||||
const onSubmit = useCallback<SubmitHandler<RecordTimesheetInput>>( | |||||
async (data) => { | |||||
const errors = validateTimesheet(data, leaveRecords, companyHolidays); | |||||
if (errors) { | |||||
Object.keys(errors).forEach((date) => | |||||
formProps.setError(date, { | |||||
message: errors[date], | |||||
}), | |||||
); | |||||
return; | |||||
} | |||||
const savedRecords = await saveTimesheet(data); | |||||
const today = dayjs(); | |||||
const newFormValues = Array(7) | |||||
.fill(undefined) | |||||
.reduce<RecordTimesheetInput>((acc, _, index) => { | |||||
const date = today.subtract(index, "day").format(INPUT_DATE_FORMAT); | |||||
return { | |||||
...acc, | |||||
[date]: savedRecords[date] ?? [], | |||||
}; | |||||
}, {}); | |||||
formProps.reset(newFormValues); | |||||
onClose(); | |||||
}, | |||||
[companyHolidays, formProps, leaveRecords, onClose], | |||||
); | |||||
const onCancel = useCallback(() => { | |||||
formProps.reset(defaultValues); | |||||
onClose(); | |||||
}, [defaultValues, formProps, onClose]); | |||||
const onModalClose = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
(_, reason) => { | |||||
if (reason !== "backdropClick") { | |||||
onClose(); | |||||
} | |||||
}, | |||||
[onClose], | |||||
); | |||||
const errorComponent = ( | |||||
<ErrorAlert | |||||
errors={Object.keys(formProps.formState.errors).map((date) => { | |||||
const error = formProps.formState.errors[date]?.message; | |||||
return error | |||||
? `${date}: ${t(error, { | |||||
TIMESHEET_DAILY_MAX_HOURS, | |||||
DAILY_NORMAL_MAX_HOURS, | |||||
})}` | |||||
: undefined; | |||||
})} | |||||
/> | |||||
); | |||||
const matches = useIsMobile(); | |||||
return ( | |||||
<FormProvider {...formProps}> | |||||
{!matches ? ( | |||||
// Desktop version | |||||
<Modal open={isOpen} onClose={onModalClose}> | |||||
<Card sx={modalSx}> | |||||
<CardContent | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Timesheet Input")} | |||||
</Typography> | |||||
<Box | |||||
sx={{ | |||||
marginInline: -3, | |||||
marginBlock: 4, | |||||
}} | |||||
> | |||||
<TimesheetTable | |||||
companyHolidays={companyHolidays} | |||||
assignedProjects={assignedProjects} | |||||
allProjects={allProjects} | |||||
leaveRecords={leaveRecords} | |||||
fastEntryEnabled={fastEntryEnabled} | |||||
/> | |||||
</Box> | |||||
{errorComponent} | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Close />} | |||||
onClick={onCancel} | |||||
> | |||||
{t("Cancel")} | |||||
</Button> | |||||
<Button variant="contained" startIcon={<Check />} type="submit"> | |||||
{t("Save")} | |||||
</Button> | |||||
</CardActions> | |||||
</CardContent> | |||||
</Card> | |||||
</Modal> | |||||
) : ( | |||||
// Mobile version | |||||
<FullscreenModal | |||||
open={isOpen} | |||||
onClose={onModalClose} | |||||
closeModal={onCancel} | |||||
> | |||||
<Box | |||||
display="flex" | |||||
flexDirection="column" | |||||
gap={2} | |||||
height="100%" | |||||
component="form" | |||||
onSubmit={formProps.handleSubmit(onSubmit)} | |||||
> | |||||
<Typography variant="h6" padding={2} flex="none"> | |||||
{t("Timesheet Input")} | |||||
</Typography> | |||||
<MobileTimesheetTable | |||||
fastEntryEnabled={fastEntryEnabled} | |||||
companyHolidays={companyHolidays} | |||||
assignedProjects={assignedProjects} | |||||
allProjects={allProjects} | |||||
leaveRecords={leaveRecords} | |||||
errorComponent={errorComponent} | |||||
/> | |||||
</Box> | |||||
</FullscreenModal> | |||||
)} | |||||
</FormProvider> | |||||
); | |||||
}; | |||||
export default TimesheetModal; |
@@ -1 +0,0 @@ | |||||
export { default } from "./TimesheetModal"; |
@@ -174,6 +174,7 @@ const FastTimeEntryModal: React.FC<Props> = ({ | |||||
name="projectIds" | name="projectIds" | ||||
render={({ field }) => ( | render={({ field }) => ( | ||||
<ProjectSelect | <ProjectSelect | ||||
includeLeaves={false} | |||||
error={Boolean(formState.errors.projectIds)} | error={Boolean(formState.errors.projectIds)} | ||||
multiple | multiple | ||||
allProjects={allProjectsWithFastEntry} | allProjects={allProjectsWithFastEntry} | ||||
@@ -1,82 +1,171 @@ | |||||
import React from "react"; | |||||
import { Box, Card, CardContent, Grid, Typography } from "@mui/material"; | |||||
import React, { useCallback, useState } from "react"; | |||||
import { | |||||
Box, | |||||
Card, | |||||
CardContent, | |||||
Grid, | |||||
ToggleButton, | |||||
ToggleButtonGroup, | |||||
ToggleButtonProps, | |||||
Tooltip, | |||||
Typography, | |||||
} from "@mui/material"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { manhourFormatter } from "@/app/utils/formatUtil"; | import { manhourFormatter } from "@/app/utils/formatUtil"; | ||||
import { AssignedProject } from "@/app/api/projects"; | import { AssignedProject } from "@/app/api/projects"; | ||||
import { TableRows, ViewModule, TableChart } from "@mui/icons-material"; | |||||
import ProjectTable from "./ProjectTable"; | |||||
interface Props { | |||||
export interface Props { | |||||
projects: AssignedProject[]; | projects: AssignedProject[]; | ||||
maintainNormalStaffWorkspaceAbility?: boolean; | maintainNormalStaffWorkspaceAbility?: boolean; | ||||
maintainManagementStaffWorkspaceAbility?: boolean; | maintainManagementStaffWorkspaceAbility?: boolean; | ||||
} | } | ||||
const ProjectGrid: React.FC<Props> = ({ projects, maintainNormalStaffWorkspaceAbility, maintainManagementStaffWorkspaceAbility }) => { | |||||
const ProjectGrid: React.FC<Props> = ({ | |||||
projects, | |||||
maintainNormalStaffWorkspaceAbility, | |||||
maintainManagementStaffWorkspaceAbility, | |||||
}) => { | |||||
const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
const [view, setView] = useState<"grid" | "list" | "table">("grid"); | |||||
const handleViewChange = useCallback< | |||||
NonNullable<ToggleButtonProps["onChange"]> | |||||
>((e, value) => { | |||||
if (value) { | |||||
setView(value); | |||||
} | |||||
}, []); | |||||
return ( | return ( | ||||
<Box> | <Box> | ||||
<Grid container columns={{ xs: 4, sm: 8, md: 12, lg: 16 }} spacing={2}> | |||||
{projects.map((project, idx) => ( | |||||
<Grid key={`${project.code}${idx}`} item xs={4}> | |||||
<Card> | |||||
<CardContent> | |||||
<Typography variant="overline">{project.code}</Typography> | |||||
<Typography | |||||
variant="h6" | |||||
sx={{ | |||||
overflow: "hidden", | |||||
textOverflow: "ellipsis", | |||||
whiteSpace: "nowrap", | |||||
marginBlockEnd: 3, | |||||
}} | |||||
> | |||||
{project.name} | |||||
</Typography> | |||||
{/* Hours Spent */} | |||||
{(Boolean(maintainNormalStaffWorkspaceAbility) || Boolean(maintainManagementStaffWorkspaceAbility)) && <><Typography variant="subtitle2">{t("Hours Spent:")}</Typography> | |||||
<Box | |||||
<ToggleButtonGroup | |||||
color="primary" | |||||
value={view} | |||||
exclusive | |||||
onChange={handleViewChange} | |||||
sx={{ marginBlockEnd: 2 }} | |||||
> | |||||
<ToggleButton value="grid"> | |||||
<Tooltip title={t("Grid view")}> | |||||
<ViewModule /> | |||||
</Tooltip> | |||||
</ToggleButton> | |||||
<ToggleButton value="list"> | |||||
<Tooltip title={t("List view")}> | |||||
<TableRows /> | |||||
</Tooltip> | |||||
</ToggleButton> | |||||
<ToggleButton value="table"> | |||||
<Tooltip title={t("Table view")}> | |||||
<TableChart /> | |||||
</Tooltip> | |||||
</ToggleButton> | |||||
</ToggleButtonGroup> | |||||
{view === "table" ? ( | |||||
<ProjectTable | |||||
projects={projects} | |||||
maintainManagementStaffWorkspaceAbility={ | |||||
maintainManagementStaffWorkspaceAbility | |||||
} | |||||
maintainNormalStaffWorkspaceAbility={ | |||||
maintainNormalStaffWorkspaceAbility | |||||
} | |||||
/> | |||||
) : ( | |||||
<Grid | |||||
container | |||||
columns={view === "list" ? 4 : { xs: 4, sm: 8, md: 12, lg: 16 }} | |||||
spacing={2} | |||||
alignItems="stretch" | |||||
> | |||||
{projects.map((project, idx) => ( | |||||
<Grid key={`${project.code}${idx}`} item xs={4}> | |||||
<Card sx={{ height: "100%" }}> | |||||
<CardContent | |||||
sx={{ | sx={{ | ||||
display: "flex", | display: "flex", | ||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
flexDirection: "column", | |||||
height: "100%", | |||||
}} | }} | ||||
> | > | ||||
<Typography variant="caption">{t("Normal")}</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpent : project.currentStaffHoursSpent)} | |||||
</Typography> | |||||
</Box> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="caption">{t("Others")}</Typography> | |||||
<Typography>{`${manhourFormatter.format( | |||||
Boolean(maintainManagementStaffWorkspaceAbility) ? project.hoursSpentOther : project.currentStaffHoursSpentOther, | |||||
)}`}</Typography> | |||||
</Box></>} | |||||
{/* Hours Allocated */} | |||||
{Boolean(maintainManagementStaffWorkspaceAbility) && <Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}> | |||||
{t("Hours Allocated:")} | |||||
</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(project.hoursAllocated)} | |||||
<Typography variant="overline">{project.code}</Typography> | |||||
<Typography | |||||
variant="h6" | |||||
sx={{ | |||||
marginBlockEnd: 3, | |||||
}} | |||||
> | |||||
{project.name} | |||||
</Typography> | </Typography> | ||||
</Box>} | |||||
</CardContent> | |||||
</Card> | |||||
</Grid> | |||||
))} | |||||
</Grid> | |||||
{/* Spacer */} | |||||
<Box sx={{ flex: 1 }} /> | |||||
{/* Hours Spent */} | |||||
{(Boolean(maintainNormalStaffWorkspaceAbility) || | |||||
Boolean(maintainManagementStaffWorkspaceAbility)) && ( | |||||
<> | |||||
<Typography variant="subtitle2"> | |||||
{t("Hours Spent:")} | |||||
</Typography> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="caption">{t("Normal")}</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format( | |||||
Boolean(maintainManagementStaffWorkspaceAbility) | |||||
? project.hoursSpent | |||||
: project.currentStaffHoursSpent, | |||||
)} | |||||
</Typography> | |||||
</Box> | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography variant="caption">{t("Others")}</Typography> | |||||
<Typography>{`${manhourFormatter.format( | |||||
Boolean(maintainManagementStaffWorkspaceAbility) | |||||
? project.hoursSpentOther | |||||
: project.currentStaffHoursSpentOther, | |||||
)}`}</Typography> | |||||
</Box> | |||||
</> | |||||
)} | |||||
{/* Hours Allocated */} | |||||
{Boolean(maintainManagementStaffWorkspaceAbility) && ( | |||||
<Box | |||||
sx={{ | |||||
display: "flex", | |||||
justifyContent: "space-between", | |||||
alignItems: "baseline", | |||||
}} | |||||
> | |||||
<Typography | |||||
variant="subtitle2" | |||||
sx={{ marginBlockStart: 2 }} | |||||
> | |||||
{t("Hours Allocated:")} | |||||
</Typography> | |||||
<Typography> | |||||
{manhourFormatter.format(project.hoursAllocated)} | |||||
</Typography> | |||||
</Box> | |||||
)} | |||||
</CardContent> | |||||
</Card> | |||||
</Grid> | |||||
))} | |||||
</Grid> | |||||
)} | |||||
</Box> | </Box> | ||||
); | ); | ||||
}; | }; | ||||
@@ -0,0 +1,106 @@ | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Props } from "./ProjectGrid"; | |||||
import { | |||||
Paper, | |||||
Table, | |||||
TableBody, | |||||
TableCell, | |||||
TableContainer, | |||||
TableHead, | |||||
TableRow, | |||||
} from "@mui/material"; | |||||
import { useMemo } from "react"; | |||||
import { AssignedProject } from "@/app/api/projects"; | |||||
import { manhourFormatter } from "@/app/utils/formatUtil"; | |||||
interface Column { | |||||
name: keyof AssignedProject; | |||||
label: string; | |||||
} | |||||
const hourColumns: Array<keyof AssignedProject> = [ | |||||
"currentStaffHoursSpent", | |||||
"currentStaffHoursSpentOther", | |||||
"hoursAllocated", | |||||
"hoursSpent", | |||||
"hoursSpentOther", | |||||
]; | |||||
const ProjectTable: React.FC<Props> = ({ | |||||
projects, | |||||
maintainManagementStaffWorkspaceAbility, | |||||
maintainNormalStaffWorkspaceAbility, | |||||
}) => { | |||||
const { t } = useTranslation("home"); | |||||
const columns = useMemo<Column[]>(() => { | |||||
return [ | |||||
{ name: "code", label: t("Project Code") }, | |||||
{ name: "name", label: t("Project Name") }, | |||||
...(maintainManagementStaffWorkspaceAbility || | |||||
maintainNormalStaffWorkspaceAbility | |||||
? maintainManagementStaffWorkspaceAbility | |||||
? ([ | |||||
{ name: "hoursSpent", label: t("Total Normal Hours Spent") }, | |||||
{ name: "hoursSpentOther", label: t("Total Other Hours Spent") }, | |||||
{ name: "hoursAllocated", label: t("Hours Allocated") }, | |||||
] satisfies Column[]) | |||||
: ([ | |||||
{ | |||||
name: "currentStaffHoursSpent", | |||||
label: t("Normal Hours Spent"), | |||||
}, | |||||
{ | |||||
name: "currentStaffHoursSpentOther", | |||||
label: t("Other Hours Spent"), | |||||
}, | |||||
] satisfies Column[]) | |||||
: []), | |||||
]; | |||||
}, [ | |||||
maintainManagementStaffWorkspaceAbility, | |||||
maintainNormalStaffWorkspaceAbility, | |||||
t, | |||||
]); | |||||
return ( | |||||
<Paper sx={{ overflow: "hidden" }}> | |||||
<TableContainer> | |||||
<Table> | |||||
<TableHead> | |||||
<TableRow> | |||||
{columns.map((column, idx) => ( | |||||
<TableCell key={`${column.name.toString()}-${idx}`}> | |||||
{column.label} | |||||
</TableCell> | |||||
))} | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{projects.map((project) => { | |||||
return ( | |||||
<TableRow hover tabIndex={-1} key={project.id}> | |||||
{columns.map((column, idx) => { | |||||
const columnName = column.name; | |||||
const needsFormatting = hourColumns.includes(columnName); | |||||
return ( | |||||
<TableCell key={`${columnName.toString()}-${idx}`}> | |||||
{needsFormatting | |||||
? manhourFormatter.format( | |||||
project[columnName] as number, | |||||
) | |||||
: project[columnName]?.toString()} | |||||
</TableCell> | |||||
); | |||||
})} | |||||
</TableRow> | |||||
); | |||||
})} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
</Paper> | |||||
); | |||||
}; | |||||
export default ProjectTable; |
@@ -4,7 +4,12 @@ import React, { useCallback, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import { CalendarMonth, EditCalendar, MoreTime } from "@mui/icons-material"; | |||||
import { | |||||
CalendarMonth, | |||||
EditCalendar, | |||||
Luggage, | |||||
MoreTime, | |||||
} from "@mui/icons-material"; | |||||
import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | import { Menu, MenuItem, SxProps, Typography } from "@mui/material"; | ||||
import AssignedProjects from "./AssignedProjects"; | import AssignedProjects from "./AssignedProjects"; | ||||
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
@@ -19,6 +24,7 @@ import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal" | |||||
import { HolidaysResult } from "@/app/api/holidays"; | import { HolidaysResult } from "@/app/api/holidays"; | ||||
import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | import { TimesheetAmendmentModal } from "../TimesheetAmendment/TimesheetAmendmentModal"; | ||||
import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | import TimeLeaveModal from "../TimeLeaveModal/TimeLeaveModal"; | ||||
import LeaveModal from "../LeaveModal"; | |||||
export interface Props { | export interface Props { | ||||
leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
@@ -32,6 +38,7 @@ export interface Props { | |||||
fastEntryEnabled: boolean; | fastEntryEnabled: boolean; | ||||
maintainNormalStaffWorkspaceAbility: boolean; | maintainNormalStaffWorkspaceAbility: boolean; | ||||
maintainManagementStaffWorkspaceAbility: boolean; | maintainManagementStaffWorkspaceAbility: boolean; | ||||
isFullTime: boolean; | |||||
} | } | ||||
const menuItemSx: SxProps = { | const menuItemSx: SxProps = { | ||||
@@ -51,10 +58,12 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
fastEntryEnabled, | fastEntryEnabled, | ||||
maintainNormalStaffWorkspaceAbility, | maintainNormalStaffWorkspaceAbility, | ||||
maintainManagementStaffWorkspaceAbility, | maintainManagementStaffWorkspaceAbility, | ||||
isFullTime, | |||||
}) => { | }) => { | ||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||||
const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); | const [isTimeLeaveModalVisible, setTimeLeaveModalVisible] = useState(false); | ||||
const [isLeaveCalendarVisible, setLeaveCalendarVisible] = useState(false); | |||||
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | const [isPastEventModalVisible, setPastEventModalVisible] = useState(false); | ||||
const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | const [isTimesheetAmendmentVisible, setisTimesheetAmendmentVisible] = | ||||
useState(false); | useState(false); | ||||
@@ -81,6 +90,15 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
setTimeLeaveModalVisible(false); | setTimeLeaveModalVisible(false); | ||||
}, []); | }, []); | ||||
const handleOpenLeaveCalendarButton = useCallback(() => { | |||||
setAnchorEl(null); | |||||
setLeaveCalendarVisible(true); | |||||
}, []); | |||||
const handleCloseLeaveCalendarButton = useCallback(() => { | |||||
setLeaveCalendarVisible(false); | |||||
}, []); | |||||
const handlePastEventClick = useCallback(() => { | const handlePastEventClick = useCallback(() => { | ||||
setAnchorEl(null); | setAnchorEl(null); | ||||
setPastEventModalVisible(true); | setPastEventModalVisible(true); | ||||
@@ -136,6 +154,10 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
<MoreTime /> | <MoreTime /> | ||||
{t("Enter Timesheet")} | {t("Enter Timesheet")} | ||||
</MenuItem> | </MenuItem> | ||||
<MenuItem onClick={handleOpenLeaveCalendarButton} sx={menuItemSx}> | |||||
<Luggage /> | |||||
{t("Record Leave")} | |||||
</MenuItem> | |||||
<MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | <MenuItem onClick={handlePastEventClick} sx={menuItemSx}> | ||||
<CalendarMonth /> | <CalendarMonth /> | ||||
{t("View Past Entries")} | {t("View Past Entries")} | ||||
@@ -154,6 +176,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
leaves={defaultLeaveRecords} | leaves={defaultLeaveRecords} | ||||
allProjects={allProjects} | allProjects={allProjects} | ||||
leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
companyHolidays={holidays} | |||||
/> | /> | ||||
<TimeLeaveModal | <TimeLeaveModal | ||||
fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
@@ -165,6 +188,17 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
timesheetRecords={defaultTimesheets} | timesheetRecords={defaultTimesheets} | ||||
leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
isFullTime={isFullTime} | |||||
/> | |||||
<LeaveModal | |||||
open={isLeaveCalendarVisible} | |||||
onClose={handleCloseLeaveCalendarButton} | |||||
leaveTypes={leaveTypes} | |||||
companyHolidays={holidays} | |||||
allProjects={allProjects} | |||||
leaveRecords={defaultLeaveRecords} | |||||
timesheetRecords={defaultTimesheets} | |||||
isFullTime={isFullTime} | |||||
/> | /> | ||||
{assignedProjects.length > 0 ? ( | {assignedProjects.length > 0 ? ( | ||||
<AssignedProjects | <AssignedProjects | ||||
@@ -11,8 +11,12 @@ import { | |||||
fetchTimesheets, | fetchTimesheets, | ||||
} from "@/app/api/timesheets"; | } from "@/app/api/timesheets"; | ||||
import { fetchHolidays } from "@/app/api/holidays"; | import { fetchHolidays } from "@/app/api/holidays"; | ||||
import { getUserAbilities } from "@/app/utils/commonUtil"; | |||||
import { MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, MAINTAIN_NORMAL_STAFF_WORKSPACE, MAINTAIN_MANAGEMENT_STAFF_WORKSPACE } from "@/middleware"; | |||||
import { getUserAbilities, getUserStaff } from "@/app/utils/commonUtil"; | |||||
import { | |||||
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
} from "@/middleware"; | |||||
const UserWorkspaceWrapper: React.FC = async () => { | const UserWorkspaceWrapper: React.FC = async () => { | ||||
const [ | const [ | ||||
@@ -25,6 +29,7 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
leaveTypes, | leaveTypes, | ||||
holidays, | holidays, | ||||
abilities, | abilities, | ||||
userStaff, | |||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchTeamMemberLeaves(), | fetchTeamMemberLeaves(), | ||||
fetchTeamMemberTimesheets(), | fetchTeamMemberTimesheets(), | ||||
@@ -34,15 +39,24 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
fetchLeaves(), | fetchLeaves(), | ||||
fetchLeaveTypes(), | fetchLeaveTypes(), | ||||
fetchHolidays(), | fetchHolidays(), | ||||
getUserAbilities() | |||||
getUserAbilities(), | |||||
getUserStaff(), | |||||
]); | ]); | ||||
const fastEntryEnabled = abilities.includes(MAINTAIN_TIMESHEET_FAST_TIME_ENTRY) | |||||
const maintainNormalStaffWorkspaceAbility = abilities.includes(MAINTAIN_NORMAL_STAFF_WORKSPACE) | |||||
const maintainManagementStaffWorkspaceAbility = abilities.includes(MAINTAIN_MANAGEMENT_STAFF_WORKSPACE) | |||||
const fastEntryEnabled = abilities.includes( | |||||
MAINTAIN_TIMESHEET_FAST_TIME_ENTRY, | |||||
); | |||||
const maintainNormalStaffWorkspaceAbility = abilities.includes( | |||||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | |||||
); | |||||
const maintainManagementStaffWorkspaceAbility = abilities.includes( | |||||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | |||||
); | |||||
const isFullTime = userStaff?.employType === "FT"; | |||||
return ( | return ( | ||||
<UserWorkspacePage | <UserWorkspacePage | ||||
isFullTime={isFullTime} | |||||
teamLeaves={teamLeaves} | teamLeaves={teamLeaves} | ||||
teamTimesheets={teamTimesheets} | teamTimesheets={teamTimesheets} | ||||
allProjects={allProjects} | allProjects={allProjects} | ||||
@@ -54,7 +68,9 @@ const UserWorkspaceWrapper: React.FC = async () => { | |||||
// Change to access check | // Change to access check | ||||
fastEntryEnabled={fastEntryEnabled} | fastEntryEnabled={fastEntryEnabled} | ||||
maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | maintainNormalStaffWorkspaceAbility={maintainNormalStaffWorkspaceAbility} | ||||
maintainManagementStaffWorkspaceAbility={maintainManagementStaffWorkspaceAbility} | |||||
maintainManagementStaffWorkspaceAbility={ | |||||
maintainManagementStaffWorkspaceAbility | |||||
} | |||||
/> | /> | ||||
); | ); | ||||
}; | }; | ||||
@@ -6,10 +6,11 @@ export interface SessionStaff { | |||||
id: number; | id: number; | ||||
teamId: number; | teamId: number; | ||||
isTeamLead: boolean; | isTeamLead: boolean; | ||||
employType: string | null; | |||||
} | } | ||||
export interface SessionWithTokens extends Session { | export interface SessionWithTokens extends Session { | ||||
staff?: SessionStaff; | staff?: SessionStaff; | ||||
role?: String; | |||||
role?: string; | |||||
abilities?: string[]; | abilities?: string[]; | ||||
accessToken?: string; | accessToken?: string; | ||||
refreshToken?: string; | refreshToken?: string; | ||||
@@ -60,14 +61,14 @@ export const authOptions: AuthOptions = { | |||||
session({ session, token }) { | session({ session, token }) { | ||||
const sessionWithToken: SessionWithTokens = { | const sessionWithToken: SessionWithTokens = { | ||||
...session, | ...session, | ||||
role: token.role as String, | |||||
role: token.role as string, | |||||
// Add the data from the token to the session | // Add the data from the token to the session | ||||
abilities: (token.abilities as ability[]).map( | abilities: (token.abilities as ability[]).map( | ||||
(item: ability) => item.actionSubjectCombo, | (item: ability) => item.actionSubjectCombo, | ||||
) as string[], | ) as string[], | ||||
accessToken: token.accessToken as string | undefined, | accessToken: token.accessToken as string | undefined, | ||||
refreshToken: token.refreshToken as string | undefined, | refreshToken: token.refreshToken as string | undefined, | ||||
staff: token.staff as SessionStaff | |||||
staff: token.staff as SessionStaff, | |||||
}; | }; | ||||
// console.log(sessionWithToken) | // console.log(sessionWithToken) | ||||
return sessionWithToken; | return sessionWithToken; | ||||
@@ -59,16 +59,16 @@ export const [ | |||||
VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING, | ||||
MAINTAIN_NORMAL_STAFF_WORKSPACE, | MAINTAIN_NORMAL_STAFF_WORKSPACE, | ||||
MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | MAINTAIN_MANAGEMENT_STAFF_WORKSPACE, | ||||
GENERATE_LATE_START_REPORTS, | |||||
GENERATE_LATE_START_REPORT, | |||||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
GENERATE_COST_AND_EXPENSE_REPORT, | |||||
GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
GENERATE_CROSS_TEAM_CHARGE_REPORT, | |||||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
] = [ | ] = [ | ||||
'MAINTAIN_USER', | 'MAINTAIN_USER', | ||||
'MAINTAIN_TIMESHEET', | 'MAINTAIN_TIMESHEET', | ||||
@@ -109,16 +109,16 @@ export const [ | |||||
'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', | 'VIEW_PROJECT_RESOURCE_CONSUMPTION_RANKING', | ||||
'MAINTAIN_NORMAL_STAFF_WORKSPACE', | 'MAINTAIN_NORMAL_STAFF_WORKSPACE', | ||||
'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', | 'MAINTAIN_MANAGEMENT_STAFF_WORKSPACE', | ||||
'GENERATE_LATE_START_REPORTS', | |||||
'GENERATE_PROJECT_POTENTIAL_DELAY_REPORT', | |||||
'GENERATE_RESOURCE_OVERCONSUMPTION_REPORT', | |||||
'GENERATE_COST_ANT_EXPENSE_REPORT', | |||||
'GENERATE_PROJECT_COMPLETION_REPORT', | |||||
'GENERATE_PROJECT_P&L_REPORT', | |||||
'GENERATE_FINANCIAL_STATUS_REPORT', | |||||
'GENERATE_PROJECT_CASH_FLOW_REPORT', | |||||
'GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||||
'GENERATE_CROSS_TEAM_CHARGE_REPORT', | |||||
'G_LATE_START_REPORT', | |||||
'G_PROJECT_POTENTIAL_DELAY_REPORT', | |||||
'G_RESOURCE_OVERCONSUMPTION_REPORT', | |||||
'G_COST_AND_EXPENSE_REPORT', | |||||
'G_PROJECT_COMPLETION_REPORT', | |||||
'G_PROJECT_P&L_REPORT', | |||||
'G_FINANCIAL_STATUS_REPORT', | |||||
'G_PROJECT_CASH_FLOW_REPORT', | |||||
'G_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT', | |||||
'G_CROSS_TEAM_CHARGE_REPORT' | |||||
] | ] | ||||
const PRIVATE_ROUTES = [ | const PRIVATE_ROUTES = [ | ||||
@@ -243,20 +243,21 @@ export default async function middleware( | |||||
if (req.nextUrl.pathname.startsWith('/analytics')) { | if (req.nextUrl.pathname.startsWith('/analytics')) { | ||||
isAuth = [ | isAuth = [ | ||||
GENERATE_LATE_START_REPORTS, | |||||
GENERATE_LATE_START_REPORT, | |||||
GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | GENERATE_PROJECT_POTENTIAL_DELAY_REPORT, | ||||
GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | GENERATE_RESOURCE_OVERCONSUMPTION_REPORT, | ||||
GENERATE_COST_ANT_EXPENSE_REPORT, | |||||
GENERATE_COST_AND_EXPENSE_REPORT, | |||||
GENERATE_PROJECT_COMPLETION_REPORT, | GENERATE_PROJECT_COMPLETION_REPORT, | ||||
GENERATE_PROJECT_PANDL_REPORT, | GENERATE_PROJECT_PANDL_REPORT, | ||||
GENERATE_FINANCIAL_STATUS_REPORT, | GENERATE_FINANCIAL_STATUS_REPORT, | ||||
GENERATE_PROJECT_CASH_FLOW_REPORT, | GENERATE_PROJECT_CASH_FLOW_REPORT, | ||||
GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT, | ||||
GENERATE_CROSS_TEAM_CHARGE_REPORT,].some((ability) => abilities.includes(ability)); | |||||
GENERATE_CROSS_TEAM_CHARGE_REPORT | |||||
].some((ability) => abilities.includes(ability)); | |||||
} | } | ||||
if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/LateStartReport')) { | ||||
isAuth = [GENERATE_LATE_START_REPORTS].some((ability) => abilities.includes(ability)); | |||||
isAuth = [GENERATE_LATE_START_REPORT].some((ability) => abilities.includes(ability)); | |||||
} | } | ||||
if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/ProjectPotentialDelayReport')) { | ||||
@@ -268,7 +269,7 @@ export default async function middleware( | |||||
} | } | ||||
if (req.nextUrl.pathname.startsWith('/analytics/CostandExpenseReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/CostandExpenseReport')) { | ||||
isAuth = [GENERATE_COST_ANT_EXPENSE_REPORT].some((ability) => abilities.includes(ability)); | |||||
isAuth = [GENERATE_COST_AND_EXPENSE_REPORT].some((ability) => abilities.includes(ability)); | |||||
} | } | ||||
if (req.nextUrl.pathname.startsWith('/analytics/ProjectCompletionReport')) { | if (req.nextUrl.pathname.startsWith('/analytics/ProjectCompletionReport')) { | ||||