| @@ -10,6 +10,7 @@ export interface ResourceSummaryResult { | |||||
| customerCode: string; | customerCode: string; | ||||
| customerName: string; | customerName: string; | ||||
| customerCodeAndName: string; | customerCodeAndName: string; | ||||
| subsidiaryCodeAndName: string; | |||||
| } | } | ||||
| export const preloadProjects = () => { | export const preloadProjects = () => { | ||||
| @@ -94,6 +94,34 @@ export const saveMemberLeave = async (data: { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export const deleteMemberEntry = async (data: { | |||||
| staffId: number; | |||||
| entryId: number; | |||||
| }) => { | |||||
| return serverFetchJson<RecordTimesheetInput>( | |||||
| `${BASE_API_URL}/timesheets/deleteMemberEntry`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const deleteMemberLeave = async (data: { | |||||
| staffId: number; | |||||
| entryId: number; | |||||
| }) => { | |||||
| return serverFetchJson<RecordLeaveInput>( | |||||
| `${BASE_API_URL}/timesheets/deleteMemberLeave`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| }; | |||||
| export const revalidateCacheAfterAmendment = () => { | export const revalidateCacheAfterAmendment = () => { | ||||
| revalidatePath("/(main)/home"); | revalidatePath("/(main)/home"); | ||||
| }; | }; | ||||
| @@ -28,7 +28,7 @@ export const validateTimeEntry = ( | |||||
| if (!entry.inputHours && !entry.otHours) { | if (!entry.inputHours && !entry.otHours) { | ||||
| error[isHoliday ? "otHours" : "inputHours"] = "Required"; | error[isHoliday ? "otHours" : "inputHours"] = "Required"; | ||||
| } else if (entry.inputHours && isHoliday) { | } else if (entry.inputHours && isHoliday) { | ||||
| error.inputHours = "Cannot input normal hours for holidays"; | |||||
| error.inputHours = "Cannot input normal hours on holidays"; | |||||
| } else if (entry.inputHours && entry.inputHours <= 0) { | } else if (entry.inputHours && entry.inputHours <= 0) { | ||||
| error.inputHours = | error.inputHours = | ||||
| "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}"; | "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}"; | ||||
| @@ -55,16 +55,31 @@ export const validateTimeEntry = ( | |||||
| return Object.keys(error).length > 0 ? error : undefined; | return Object.keys(error).length > 0 ? error : undefined; | ||||
| }; | }; | ||||
| export const isValidLeaveEntry = (entry: Partial<LeaveEntry>): string => { | |||||
| export type LeaveEntryError = { | |||||
| [field in keyof LeaveEntry]?: string; | |||||
| }; | |||||
| export const validateLeaveEntry = ( | |||||
| entry: Partial<LeaveEntry>, | |||||
| isHoliday: boolean, | |||||
| ): LeaveEntryError | undefined => { | |||||
| // Test for errrors | // Test for errrors | ||||
| let error: keyof LeaveEntry | "" = ""; | |||||
| const error: LeaveEntryError = {}; | |||||
| if (!entry.leaveTypeId) { | if (!entry.leaveTypeId) { | ||||
| error = "leaveTypeId"; | |||||
| } else if (!entry.inputHours || !(entry.inputHours >= 0)) { | |||||
| error = "inputHours"; | |||||
| error.leaveTypeId = "Required"; | |||||
| } else if (entry.inputHours && isHoliday) { | |||||
| error.inputHours = "Cannot input normal hours on holidays"; | |||||
| } else if (!entry.inputHours) { | |||||
| error.inputHours = "Required"; | |||||
| } else if ( | |||||
| entry.inputHours && | |||||
| (entry.inputHours <= 0 || entry.inputHours > DAILY_NORMAL_MAX_HOURS) | |||||
| ) { | |||||
| error.inputHours = | |||||
| "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}"; | |||||
| } | } | ||||
| return error; | |||||
| return Object.keys(error).length > 0 ? error : undefined; | |||||
| }; | }; | ||||
| export const validateTimesheet = ( | export const validateTimesheet = ( | ||||
| @@ -95,27 +110,10 @@ export const validateTimesheet = ( | |||||
| } | } | ||||
| // Check total hours | // Check total hours | ||||
| const leaves = leaveRecords[date]; | |||||
| const leaveHours = | |||||
| leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | |||||
| const totalInputHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.inputHours || 0); | |||||
| }, 0); | |||||
| const totalOtHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.otHours || 0); | |||||
| }, 0); | |||||
| if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | |||||
| errors[date] = | |||||
| "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 ( | |||||
| totalInputHours + totalOtHours + leaveHours > | |||||
| TIMESHEET_DAILY_MAX_HOURS | |||||
| ) { | |||||
| errors[date] = | |||||
| "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; | |||||
| const leaves = leaveRecords[date] || []; | |||||
| const totalHourError = checkTotalHours(timeEntries, leaves); | |||||
| if (totalHourError) { | |||||
| errors[date] = totalHourError; | |||||
| } | } | ||||
| }); | }); | ||||
| @@ -125,48 +123,64 @@ export const validateTimesheet = ( | |||||
| export const validateLeaveRecord = ( | export const validateLeaveRecord = ( | ||||
| leaveRecords: RecordLeaveInput, | leaveRecords: RecordLeaveInput, | ||||
| timesheet: RecordTimesheetInput, | timesheet: RecordTimesheetInput, | ||||
| companyHolidays: HolidaysResult[], | |||||
| ): { [date: string]: string } | undefined => { | ): { [date: string]: string } | undefined => { | ||||
| const errors: { [date: string]: string } = {}; | 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) => { | Object.keys(leaveRecords).forEach((date) => { | ||||
| const leaves = leaveRecords[date]; | const leaves = leaveRecords[date]; | ||||
| // Check each leave entry | // Check each leave entry | ||||
| for (const entry of leaves) { | for (const entry of leaves) { | ||||
| const entryError = isValidLeaveEntry(entry); | |||||
| const entryError = validateLeaveEntry(entry, holidays.has(date)); | |||||
| if (entryError) { | if (entryError) { | ||||
| errors[date] = "There are errors in the entries"; | errors[date] = "There are errors in the entries"; | ||||
| return; | |||||
| } | } | ||||
| } | } | ||||
| // Check total hours | // Check total hours | ||||
| const timeEntries = timesheet[date] || []; | const timeEntries = timesheet[date] || []; | ||||
| const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | |||||
| const totalInputHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.inputHours || 0); | |||||
| }, 0); | |||||
| const totalOtHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.otHours || 0); | |||||
| }, 0); | |||||
| if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | |||||
| errors[date] = | |||||
| "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 ( | |||||
| totalInputHours + totalOtHours + leaveHours > | |||||
| TIMESHEET_DAILY_MAX_HOURS | |||||
| ) { | |||||
| errors[date] = | |||||
| "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; | |||||
| const totalHourError = checkTotalHours(timeEntries, leaves); | |||||
| if (totalHourError) { | |||||
| errors[date] = totalHourError; | |||||
| } | } | ||||
| }); | }); | ||||
| return Object.keys(errors).length > 0 ? errors : undefined; | return Object.keys(errors).length > 0 ? errors : undefined; | ||||
| }; | }; | ||||
| export const checkTotalHours = ( | |||||
| timeEntries: TimeEntry[], | |||||
| leaves: LeaveEntry[], | |||||
| ): string | undefined => { | |||||
| const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0); | |||||
| const totalInputHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.inputHours || 0); | |||||
| }, 0); | |||||
| const totalOtHours = timeEntries.reduce((acc, entry) => { | |||||
| return acc + (entry.otHours || 0); | |||||
| }, 0); | |||||
| if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) { | |||||
| return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours."; | |||||
| } else if ( | |||||
| totalInputHours + totalOtHours + leaveHours > | |||||
| TIMESHEET_DAILY_MAX_HOURS | |||||
| ) { | |||||
| return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}"; | |||||
| } | |||||
| }; | |||||
| export const DAILY_NORMAL_MAX_HOURS = 8; | export const DAILY_NORMAL_MAX_HOURS = 8; | ||||
| export const LEAVE_DAILY_MAX_HOURS = 8; | |||||
| export const TIMESHEET_DAILY_MAX_HOURS = 20; | export const TIMESHEET_DAILY_MAX_HOURS = 20; | ||||
| @@ -81,7 +81,11 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| const onSubmit = useCallback<SubmitHandler<RecordLeaveInput>>( | const onSubmit = useCallback<SubmitHandler<RecordLeaveInput>>( | ||||
| async (data) => { | async (data) => { | ||||
| const errors = validateLeaveRecord(data, timesheetRecords); | |||||
| const errors = validateLeaveRecord( | |||||
| data, | |||||
| timesheetRecords, | |||||
| companyHolidays, | |||||
| ); | |||||
| if (errors) { | if (errors) { | ||||
| Object.keys(errors).forEach((date) => | Object.keys(errors).forEach((date) => | ||||
| formProps.setError(date, { | formProps.setError(date, { | ||||
| @@ -106,7 +110,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| formProps.reset(newFormValues); | formProps.reset(newFormValues); | ||||
| onClose(); | onClose(); | ||||
| }, | }, | ||||
| [formProps, onClose, timesheetRecords, username], | |||||
| [companyHolidays, formProps, onClose, timesheetRecords, username], | |||||
| ); | ); | ||||
| const onCancel = useCallback(() => { | const onCancel = useCallback(() => { | ||||
| @@ -1,6 +1,6 @@ | |||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import { LeaveEntry } from "@/app/api/timesheets/actions"; | import { LeaveEntry } from "@/app/api/timesheets/actions"; | ||||
| import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils"; | |||||
| import { DAILY_NORMAL_MAX_HOURS } from "@/app/api/timesheets/utils"; | |||||
| import { shortDateFormatter } from "@/app/utils/formatUtil"; | import { shortDateFormatter } from "@/app/utils/formatUtil"; | ||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | ||||
| import { Check, Delete } from "@mui/icons-material"; | import { Check, Delete } from "@mui/icons-material"; | ||||
| @@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next"; | |||||
| export interface Props extends Omit<ModalProps, "children"> { | export interface Props extends Omit<ModalProps, "children"> { | ||||
| onSave: (leaveEntry: LeaveEntry, recordDate?: string) => Promise<void>; | onSave: (leaveEntry: LeaveEntry, recordDate?: string) => Promise<void>; | ||||
| onDelete?: () => void; | |||||
| onDelete?: () => Promise<void>; | |||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| defaultValues?: Partial<LeaveEntry>; | defaultValues?: Partial<LeaveEntry>; | ||||
| modalSx?: SxProps; | modalSx?: SxProps; | ||||
| @@ -59,7 +59,7 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| } = useTranslation("home"); | } = useTranslation("home"); | ||||
| const { register, control, reset, getValues, trigger, formState } = | |||||
| const { register, control, reset, getValues, trigger, formState, setError } = | |||||
| useForm<LeaveEntry>({ | useForm<LeaveEntry>({ | ||||
| defaultValues: { | defaultValues: { | ||||
| leaveTypeId: leaveTypes[0].id, | leaveTypeId: leaveTypes[0].id, | ||||
| @@ -73,10 +73,16 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| const saveHandler = useCallback(async () => { | const saveHandler = useCallback(async () => { | ||||
| const valid = await trigger(); | const valid = await trigger(); | ||||
| if (valid) { | if (valid) { | ||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | |||||
| try { | |||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | |||||
| } catch (e) { | |||||
| setError("root", { | |||||
| message: e instanceof Error ? e.message : "Unknown error", | |||||
| }); | |||||
| } | |||||
| } | } | ||||
| }, [getValues, onSave, recordDate, reset, trigger]); | |||||
| }, [getValues, onSave, recordDate, reset, setError, trigger]); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | ||||
| (...args) => { | (...args) => { | ||||
| @@ -121,12 +127,19 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| fullWidth | fullWidth | ||||
| {...register("inputHours", { | {...register("inputHours", { | ||||
| setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), | setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), | ||||
| validate: (value) => | |||||
| (0 < value && value <= LEAVE_DAILY_MAX_HOURS) || | |||||
| t( | |||||
| "Input hours should be between 0 and {{LEAVE_DAILY_MAX_HOURS}}", | |||||
| { LEAVE_DAILY_MAX_HOURS }, | |||||
| ), | |||||
| validate: (value) => { | |||||
| if (isHoliday) { | |||||
| return t("Cannot input normal hours on holidays"); | |||||
| } | |||||
| return ( | |||||
| (0 < value && value <= DAILY_NORMAL_MAX_HOURS) || | |||||
| t( | |||||
| "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}", | |||||
| { DAILY_NORMAL_MAX_HOURS }, | |||||
| ) | |||||
| ); | |||||
| }, | |||||
| })} | })} | ||||
| error={Boolean(formState.errors.inputHours)} | error={Boolean(formState.errors.inputHours)} | ||||
| helperText={formState.errors.inputHours?.message} | helperText={formState.errors.inputHours?.message} | ||||
| @@ -138,6 +151,11 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| rows={2} | rows={2} | ||||
| {...register("remark")} | {...register("remark")} | ||||
| /> | /> | ||||
| {formState.errors.root?.message && ( | |||||
| <Typography variant="caption" color="error"> | |||||
| {t(formState.errors.root.message, { DAILY_NORMAL_MAX_HOURS })} | |||||
| </Typography> | |||||
| )} | |||||
| <Box display="flex" justifyContent="flex-end" gap={1}> | <Box display="flex" justifyContent="flex-end" gap={1}> | ||||
| {onDelete && ( | {onDelete && ( | ||||
| <Button | <Button | ||||
| @@ -1,10 +1,14 @@ | |||||
| import { Add, Check, Close, Delete } from "@mui/icons-material"; | import { Add, Check, Close, Delete } from "@mui/icons-material"; | ||||
| import { Box, Button, Typography } from "@mui/material"; | |||||
| import { Box, Button, Tooltip, Typography } from "@mui/material"; | |||||
| import { | import { | ||||
| FooterPropsOverrides, | FooterPropsOverrides, | ||||
| GridActionsCellItem, | GridActionsCellItem, | ||||
| GridCellParams, | |||||
| GridColDef, | GridColDef, | ||||
| GridEditInputCell, | |||||
| GridEditSingleSelectCell, | |||||
| GridEventListener, | GridEventListener, | ||||
| GridRenderEditCellParams, | |||||
| GridRowId, | GridRowId, | ||||
| GridRowModel, | GridRowModel, | ||||
| GridRowModes, | GridRowModes, | ||||
| @@ -21,7 +25,11 @@ import { manhourFormatter } from "@/app/utils/formatUtil"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import isBetween from "dayjs/plugin/isBetween"; | import isBetween from "dayjs/plugin/isBetween"; | ||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import { isValidLeaveEntry } from "@/app/api/timesheets/utils"; | |||||
| import { | |||||
| DAILY_NORMAL_MAX_HOURS, | |||||
| LeaveEntryError, | |||||
| validateLeaveEntry, | |||||
| } from "@/app/api/timesheets/utils"; | |||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | ||||
| dayjs.extend(isBetween); | dayjs.extend(isBetween); | ||||
| @@ -35,11 +43,11 @@ interface Props { | |||||
| type LeaveEntryRow = Partial< | type LeaveEntryRow = Partial< | ||||
| LeaveEntry & { | LeaveEntry & { | ||||
| _isNew: boolean; | _isNew: boolean; | ||||
| _error: string; | |||||
| _error: LeaveEntryError; | |||||
| } | } | ||||
| >; | >; | ||||
| const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| const EntryInputTable: React.FC<Props> = ({ day, leaveTypes, isHoliday }) => { | |||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| const { getValues, setValue, clearErrors } = | const { getValues, setValue, clearErrors } = | ||||
| @@ -67,12 +75,12 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| "", | "", | ||||
| ) as LeaveEntryRow; | ) as LeaveEntryRow; | ||||
| const error = isValidLeaveEntry(row); | |||||
| const error = validateLeaveEntry(row, isHoliday); | |||||
| apiRef.current.updateRows([{ id, _error: error }]); | apiRef.current.updateRows([{ id, _error: error }]); | ||||
| return !error; | return !error; | ||||
| }, | }, | ||||
| [apiRef], | |||||
| [apiRef, isHoliday], | |||||
| ); | ); | ||||
| const handleCancel = useCallback( | const handleCancel = useCallback( | ||||
| @@ -163,6 +171,20 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| width: 200, | width: 200, | ||||
| editable: true, | editable: true, | ||||
| type: "singleSelect", | type: "singleSelect", | ||||
| renderEditCell(params: GridRenderEditCellParams<LeaveEntryRow>) { | |||||
| const errorMessage = | |||||
| params.row._error?.[params.field as keyof LeaveEntry]; | |||||
| const content = ( | |||||
| <GridEditSingleSelectCell variant="outlined" {...params} /> | |||||
| ); | |||||
| return errorMessage ? ( | |||||
| <Tooltip title={t(errorMessage)} placement="top"> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | |||||
| content | |||||
| ); | |||||
| }, | |||||
| valueOptions() { | valueOptions() { | ||||
| return leaveTypes.map((p) => ({ value: p.id, label: p.name })); | return leaveTypes.map((p) => ({ value: p.id, label: p.name })); | ||||
| }, | }, | ||||
| @@ -176,6 +198,18 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| width: 150, | width: 150, | ||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| renderEditCell(params: GridRenderEditCellParams<LeaveEntryRow>) { | |||||
| const errorMessage = | |||||
| params.row._error?.[params.field as keyof LeaveEntry]; | |||||
| const content = <GridEditInputCell {...params} />; | |||||
| return errorMessage ? ( | |||||
| <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | |||||
| content | |||||
| ); | |||||
| }, | |||||
| valueParser(value) { | valueParser(value) { | ||||
| return value ? roundToNearestQuarter(value) : value; | return value ? roundToNearestQuarter(value) : value; | ||||
| }, | }, | ||||
| @@ -248,16 +282,10 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| onRowEditStop={handleEditStop} | onRowEditStop={handleEditStop} | ||||
| processRowUpdate={processRowUpdate} | processRowUpdate={processRowUpdate} | ||||
| columns={columns} | columns={columns} | ||||
| getCellClassName={(params) => { | |||||
| getCellClassName={(params: GridCellParams<LeaveEntryRow>) => { | |||||
| let classname = ""; | let classname = ""; | ||||
| if (params.row._error === params.field) { | |||||
| if (params.row._error?.[params.field as keyof LeaveEntry]) { | |||||
| classname = "hasError"; | classname = "hasError"; | ||||
| } else if ( | |||||
| params.field === "taskGroupId" && | |||||
| params.row.isPlanned !== undefined && | |||||
| !params.row.isPlanned | |||||
| ) { | |||||
| classname = "hasWarning"; | |||||
| } | } | ||||
| return classname; | return classname; | ||||
| }} | }} | ||||
| @@ -50,9 +50,9 @@ const MobileLeaveEntry: React.FC<Props> = ({ | |||||
| const openEditModal = useCallback( | const openEditModal = useCallback( | ||||
| (defaultValues?: LeaveEntry) => () => { | (defaultValues?: LeaveEntry) => () => { | ||||
| setEditModalProps({ | setEditModalProps({ | ||||
| defaultValues, | |||||
| defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
| onDelete: defaultValues | onDelete: defaultValues | ||||
| ? () => { | |||||
| ? async () => { | |||||
| setValue( | setValue( | ||||
| date, | date, | ||||
| currentEntries.filter((entry) => entry.id !== defaultValues.id), | currentEntries.filter((entry) => entry.id !== defaultValues.id), | ||||
| @@ -139,6 +139,7 @@ const MobileLeaveEntry: React.FC<Props> = ({ | |||||
| open={editModalOpen} | open={editModalOpen} | ||||
| onClose={closeEditModal} | onClose={closeEditModal} | ||||
| onSave={onSaveEntry} | onSave={onSaveEntry} | ||||
| isHoliday={Boolean(isHoliday)} | |||||
| {...editModalProps} | {...editModalProps} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| @@ -97,8 +97,8 @@ const ProjectCashFlow: React.FC = () => { | |||||
| cumulativeIncome.push(cashFlowMonthlyChartData[0].incomeList[i].cumulativeIncome) | cumulativeIncome.push(cashFlowMonthlyChartData[0].incomeList[i].cumulativeIncome) | ||||
| } | } | ||||
| for (var i = 0; i < cashFlowMonthlyChartData[0].expenditureList.length; i++) { | for (var i = 0; i < cashFlowMonthlyChartData[0].expenditureList.length; i++) { | ||||
| if (rightMax < cashFlowMonthlyChartData[0].incomeList[i].income || rightMax < cashFlowMonthlyChartData[0].expenditureList[i].expenditure){ | |||||
| rightMax = Math.max(cashFlowMonthlyChartData[0].incomeList[i].income,cashFlowMonthlyChartData[0].expenditureList[i].expenditure) | |||||
| if (rightMax < cashFlowMonthlyChartData[0].incomeList[i].cumulativeIncome || rightMax < cashFlowMonthlyChartData[0].expenditureList[i].cumulativeExpenditure){ | |||||
| rightMax = Math.max(cashFlowMonthlyChartData[0].incomeList[i].cumulativeIncome,cashFlowMonthlyChartData[0].expenditureList[i].cumulativeExpenditure) | |||||
| } | } | ||||
| monthlyExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].expenditure) | monthlyExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].expenditure) | ||||
| cumulativeExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].cumulativeExpenditure) | cumulativeExpenditure.push(cashFlowMonthlyChartData[0].expenditureList[i].cumulativeExpenditure) | ||||
| @@ -141,11 +141,12 @@ const ProjectCashFlow: React.FC = () => { | |||||
| } | } | ||||
| monthlyAnticipateIncome.push(cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome) | monthlyAnticipateIncome.push(cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome) | ||||
| } | } | ||||
| setMonthlyAnticipateIncomeList(monthlyAnticipateIncome) | |||||
| } else { | } else { | ||||
| setMonthlyAnticipateIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) | setMonthlyAnticipateIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) | ||||
| setMonthlyAnticipateExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) | setMonthlyAnticipateExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) | ||||
| } | } | ||||
| setMonthlyAnticipateIncomeList(monthlyAnticipateIncome) | |||||
| console.log(cashFlowAnticipateData) | console.log(cashFlowAnticipateData) | ||||
| if(cashFlowAnticipateData.length !== 0){ | if(cashFlowAnticipateData.length !== 0){ | ||||
| if (cashFlowAnticipateData[0].anticipateExpenditureList.length !== 0) { | if (cashFlowAnticipateData[0].anticipateExpenditureList.length !== 0) { | ||||
| @@ -175,7 +176,7 @@ const ProjectCashFlow: React.FC = () => { | |||||
| } | } | ||||
| setMonthlyAnticipateExpenditureList(result) | setMonthlyAnticipateExpenditureList(result) | ||||
| for (var i = 0; i < monthlyAnticipateIncome.length; i++) { | for (var i = 0; i < monthlyAnticipateIncome.length; i++) { | ||||
| if (anticipateLeftMax < monthlyAnticipateIncome[i] || result[i]){ | |||||
| if (anticipateLeftMax < monthlyAnticipateIncome[i] || anticipateLeftMax < result[i]){ | |||||
| anticipateLeftMax = Math.max(monthlyAnticipateIncome[i],result[i]) | anticipateLeftMax = Math.max(monthlyAnticipateIncome[i],result[i]) | ||||
| } | } | ||||
| setMonthlyAnticipateLeftMax(anticipateLeftMax) | setMonthlyAnticipateLeftMax(anticipateLeftMax) | ||||
| @@ -432,6 +433,13 @@ const ProjectCashFlow: React.FC = () => { | |||||
| }; | }; | ||||
| const anticipateOptions: ApexOptions = { | const anticipateOptions: ApexOptions = { | ||||
| tooltip: { | |||||
| y: { | |||||
| formatter: function(val) { | |||||
| return val.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); | |||||
| } | |||||
| } | |||||
| }, | |||||
| chart: { | chart: { | ||||
| height: 350, | height: 350, | ||||
| type: "line", | type: "line", | ||||
| @@ -135,6 +135,10 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| subStageList.push(subStageResult[i].plannedResources) | subStageList.push(subStageResult[i].plannedResources) | ||||
| subStageList.push(subStageResult[i].actualResourcesSpent) | subStageList.push(subStageResult[i].actualResourcesSpent) | ||||
| } | } | ||||
| if (i === subStageResult.length-1) { | |||||
| subStageSecondLayerList.push(subStageList) | |||||
| subStageFullList.push(subStageSecondLayerList) | |||||
| } | |||||
| } | } | ||||
| console.log(subStageFullList) | console.log(subStageFullList) | ||||
| @@ -157,6 +161,9 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| mainStageList.push(mainStageResult[i].plannedResources) | mainStageList.push(mainStageResult[i].plannedResources) | ||||
| mainStageList.push(mainStageResult[i].actualResourcesSpent) | mainStageList.push(mainStageResult[i].actualResourcesSpent) | ||||
| } | } | ||||
| if (i === mainStageResult.length - 1) { | |||||
| mainStageFullList.push(createData(mainStageList,subStageFullList[arrayNumber])) | |||||
| } | |||||
| } | } | ||||
| setRows(mainStageFullList) | setRows(mainStageFullList) | ||||
| } | } | ||||
| @@ -238,20 +245,20 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| </IconButton> | </IconButton> | ||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell style={{fontSize:10}}>{row.stage}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.taskCount}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g1Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g1Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g2Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g2Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g3Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g3Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g4Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g4Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g5Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.g5Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.totalPlanned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:10}}>{row.totalActual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.stage}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.taskCount}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g1Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g1Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g2Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g2Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g3Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g3Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g4Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g4Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g5Planned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.g5Actual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.totalPlanned.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}}>{row.totalActual.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| {row.task.map((taskRow:any) => ( | {row.task.map((taskRow:any) => ( | ||||
| <TableRow key={taskRow[0]} style={{backgroundColor:"#f0f3f7"}}> | <TableRow key={taskRow[0]} style={{backgroundColor:"#f0f3f7"}}> | ||||
| @@ -274,7 +281,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[0]}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[0]}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -287,7 +294,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[1]}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[1]}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -300,7 +307,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[4].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[4].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -313,7 +320,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[5].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[5].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -326,7 +333,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[6].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[6].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -339,7 +346,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[7].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[7].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -352,7 +359,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[8].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[8].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -365,7 +372,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[9].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[9].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -378,7 +385,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[10].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[10].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -391,7 +398,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[11].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[11].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -404,7 +411,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[12].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[12].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -417,7 +424,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[13].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[13].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -430,7 +437,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[2].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[2].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -443,7 +450,7 @@ const ProjectResourceSummary: React.FC = () => { | |||||
| <Table size="small" aria-label="tasks"> | <Table size="small" aria-label="tasks"> | ||||
| <TableBody> | <TableBody> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{fontSize:10}} colSpan={1}>{taskRow[3].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| <TableCell style={{fontSize:13}} colSpan={1}>{taskRow[3].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| @@ -666,11 +673,11 @@ const columns2 = [ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {/* <div style={{display:"inline-block",width:"99%",marginLeft:10}}> | {/* <div style={{display:"inline-block",width:"99%",marginLeft:10}}> | ||||
| <CustomDatagrid rows={projectResourcesRows} columns={columns2} columnWidth={200} dataGridHeight={480} pageSize={100} columnGroupingModel={columnGroupingModel} sx={{fontSize:10}}/> | |||||
| <CustomDatagrid rows={projectResourcesRows} columns={columns2} columnWidth={200} dataGridHeight={480} pageSize={100} columnGroupingModel={columnGroupingModel} sx={{fontSize:13}}/> | |||||
| </div> */} | </div> */} | ||||
| {/* <div style={{display:"inline-block",width:"99%",marginLeft:10, marginRight:10, marginTop:10}}> */} | {/* <div style={{display:"inline-block",width:"99%",marginLeft:10, marginRight:10, marginTop:10}}> */} | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table sx={{ minWidth: 650 }} aria-label="simple table"> | |||||
| <Table sx={{ minWidth: 650, maxWidth:1080}} aria-label="simple table"> | |||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell align="center" colSpan={3}> | <TableCell align="center" colSpan={3}> | ||||
| @@ -697,20 +704,20 @@ const columns2 = [ | |||||
| </TableRow> | </TableRow> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell style={{width:"5%"}}/> | <TableCell style={{width:"5%"}}/> | ||||
| <TableCell style={{fontSize:10, width:"20%"}}>Stage</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Task Count</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:10, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, minWidth:300}}>Stage</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Task Count</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Planned</TableCell> | |||||
| <TableCell style={{fontSize:13, width:"5%"}}>Actual</TableCell> | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| @@ -58,6 +58,7 @@ const ProjectResourceSummarySearch: React.FC<Props> = ({ projects }) => { | |||||
| { name: "projectCode", label: t("Project Code") }, | { name: "projectCode", label: t("Project Code") }, | ||||
| { name: "projectName", label: t("Project Name") }, | { name: "projectName", label: t("Project Name") }, | ||||
| { name: "customerCodeAndName", label: t("Client Code And Name") }, | { name: "customerCodeAndName", label: t("Client Code And Name") }, | ||||
| { name: "subsidiaryCodeAndName", label: t("Subsidiary Code And Name")} | |||||
| ], | ], | ||||
| [onTaskClick, t], | [onTaskClick, t], | ||||
| // [t], | // [t], | ||||
| @@ -20,6 +20,8 @@ import { ProjectWithTasks } from "@/app/api/projects"; | |||||
| import { | import { | ||||
| LeaveEntry, | LeaveEntry, | ||||
| TimeEntry, | TimeEntry, | ||||
| deleteMemberEntry, | |||||
| deleteMemberLeave, | |||||
| saveMemberEntry, | saveMemberEntry, | ||||
| saveMemberLeave, | saveMemberLeave, | ||||
| } from "@/app/api/timesheets/actions"; | } from "@/app/api/timesheets/actions"; | ||||
| @@ -29,6 +31,8 @@ import TimesheetEditModal, { | |||||
| import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | import { Props as LeaveEditModalProps } from "../LeaveTable/LeaveEditModal"; | ||||
| import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | import LeaveEditModal from "../LeaveTable/LeaveEditModal"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { checkTotalHours } from "@/app/api/timesheets/utils"; | |||||
| import unionBy from "lodash/unionBy"; | |||||
| export interface Props { | export interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| @@ -119,13 +123,30 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| const openEditModal = useCallback( | const openEditModal = useCallback( | ||||
| (defaultValues?: TimeEntry, recordDate?: string, isHoliday?: boolean) => { | (defaultValues?: TimeEntry, recordDate?: string, isHoliday?: boolean) => { | ||||
| setEditModalProps({ | setEditModalProps({ | ||||
| defaultValues, | |||||
| defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
| recordDate, | recordDate, | ||||
| isHoliday, | isHoliday, | ||||
| onDelete: defaultValues | |||||
| ? async () => { | |||||
| const intStaffId = parseInt(selectedStaff.id); | |||||
| const newMemberTimesheets = await deleteMemberEntry({ | |||||
| staffId: intStaffId, | |||||
| entryId: defaultValues.id, | |||||
| }); | |||||
| setLocalTeamTimesheets((timesheets) => ({ | |||||
| ...timesheets, | |||||
| [intStaffId]: { | |||||
| ...timesheets[intStaffId], | |||||
| timeEntries: newMemberTimesheets, | |||||
| }, | |||||
| })); | |||||
| setEditModalOpen(false); | |||||
| } | |||||
| : undefined, | |||||
| }); | }); | ||||
| setEditModalOpen(true); | setEditModalOpen(true); | ||||
| }, | }, | ||||
| [], | |||||
| [selectedStaff.id], | |||||
| ); | ); | ||||
| const closeEditModal = useCallback(() => { | const closeEditModal = useCallback(() => { | ||||
| @@ -141,13 +162,30 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| const openLeaveEditModal = useCallback( | const openLeaveEditModal = useCallback( | ||||
| (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { | (defaultValues?: LeaveEntry, recordDate?: string, isHoliday?: boolean) => { | ||||
| setLeaveEditModalProps({ | setLeaveEditModalProps({ | ||||
| defaultValues, | |||||
| defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
| recordDate, | recordDate, | ||||
| isHoliday, | isHoliday, | ||||
| onDelete: defaultValues | |||||
| ? async () => { | |||||
| const intStaffId = parseInt(selectedStaff.id); | |||||
| const newMemberLeaves = await deleteMemberLeave({ | |||||
| staffId: intStaffId, | |||||
| entryId: defaultValues.id, | |||||
| }); | |||||
| setLocalTeamLeaves((leaves) => ({ | |||||
| ...leaves, | |||||
| [intStaffId]: { | |||||
| ...leaves[intStaffId], | |||||
| leaveEntries: newMemberLeaves, | |||||
| }, | |||||
| })); | |||||
| setLeaveEditModalOpen(false); | |||||
| } | |||||
| : undefined, | |||||
| }); | }); | ||||
| setLeaveEditModalOpen(true); | setLeaveEditModalOpen(true); | ||||
| }, | }, | ||||
| [], | |||||
| [selectedStaff.id], | |||||
| ); | ); | ||||
| const closeLeaveEditModal = useCallback(() => { | const closeLeaveEditModal = useCallback(() => { | ||||
| @@ -260,10 +298,44 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| [companyHolidays, openEditModal], | [companyHolidays, openEditModal], | ||||
| ); | ); | ||||
| const checkTotalHoursForDate = useCallback( | |||||
| (newEntry: TimeEntry | LeaveEntry, date?: string) => { | |||||
| if (!date) { | |||||
| throw Error("Invalid date"); | |||||
| } | |||||
| const intStaffId = parseInt(selectedStaff.id); | |||||
| const leaves = localTeamLeaves[intStaffId].leaveEntries[date] || []; | |||||
| const timesheets = | |||||
| localTeamTimesheets[intStaffId].timeEntries[date] || []; | |||||
| let totalHourError; | |||||
| if ((newEntry as LeaveEntry).leaveTypeId) { | |||||
| // newEntry is a leave entry | |||||
| const leavesWithNewEntry = unionBy( | |||||
| [newEntry as LeaveEntry], | |||||
| leaves, | |||||
| "id", | |||||
| ); | |||||
| totalHourError = checkTotalHours(timesheets, leavesWithNewEntry); | |||||
| } else { | |||||
| // newEntry is a timesheet entry | |||||
| const timesheetsWithNewEntry = unionBy( | |||||
| [newEntry as TimeEntry], | |||||
| timesheets, | |||||
| "id", | |||||
| ); | |||||
| totalHourError = checkTotalHours(timesheetsWithNewEntry, leaves); | |||||
| } | |||||
| if (totalHourError) throw Error(totalHourError); | |||||
| }, | |||||
| [localTeamLeaves, localTeamTimesheets, selectedStaff.id], | |||||
| ); | |||||
| const handleSave = useCallback( | const handleSave = useCallback( | ||||
| async (timeEntry: TimeEntry, recordDate?: string) => { | async (timeEntry: TimeEntry, recordDate?: string) => { | ||||
| // TODO: should be fine, but can handle parse error | // TODO: should be fine, but can handle parse error | ||||
| const intStaffId = parseInt(selectedStaff.id); | const intStaffId = parseInt(selectedStaff.id); | ||||
| checkTotalHoursForDate(timeEntry, recordDate); | |||||
| const newMemberTimesheets = await saveMemberEntry({ | const newMemberTimesheets = await saveMemberEntry({ | ||||
| staffId: intStaffId, | staffId: intStaffId, | ||||
| entry: timeEntry, | entry: timeEntry, | ||||
| @@ -278,12 +350,13 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| })); | })); | ||||
| setEditModalOpen(false); | setEditModalOpen(false); | ||||
| }, | }, | ||||
| [selectedStaff.id], | |||||
| [checkTotalHoursForDate, selectedStaff.id], | |||||
| ); | ); | ||||
| const handleSaveLeave = useCallback( | const handleSaveLeave = useCallback( | ||||
| async (leaveEntry: LeaveEntry, recordDate?: string) => { | async (leaveEntry: LeaveEntry, recordDate?: string) => { | ||||
| const intStaffId = parseInt(selectedStaff.id); | const intStaffId = parseInt(selectedStaff.id); | ||||
| checkTotalHoursForDate(leaveEntry, recordDate); | |||||
| const newMemberLeaves = await saveMemberLeave({ | const newMemberLeaves = await saveMemberLeave({ | ||||
| staffId: intStaffId, | staffId: intStaffId, | ||||
| recordDate, | recordDate, | ||||
| @@ -298,7 +371,7 @@ const TimesheetAmendment: React.FC<Props> = ({ | |||||
| })); | })); | ||||
| setLeaveEditModalOpen(false); | setLeaveEditModalOpen(false); | ||||
| }, | }, | ||||
| [selectedStaff.id], | |||||
| [checkTotalHoursForDate, selectedStaff.id], | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| @@ -328,7 +328,7 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| const content = <GridEditInputCell {...params} />; | const content = <GridEditInputCell {...params} />; | ||||
| return errorMessage ? ( | return errorMessage ? ( | ||||
| <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | <Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}> | ||||
| {content} | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | </Tooltip> | ||||
| ) : ( | ) : ( | ||||
| content | content | ||||
| @@ -352,7 +352,9 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| params.row._error?.[params.field as keyof TimeEntry]; | params.row._error?.[params.field as keyof TimeEntry]; | ||||
| const content = <GridEditInputCell {...params} />; | const content = <GridEditInputCell {...params} />; | ||||
| return errorMessage ? ( | return errorMessage ? ( | ||||
| <Tooltip title={t(errorMessage)}>{content}</Tooltip> | |||||
| <Tooltip title={t(errorMessage)}> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | ) : ( | ||||
| content | content | ||||
| ); | ); | ||||
| @@ -375,7 +377,9 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| params.row._error?.[params.field as keyof TimeEntry]; | params.row._error?.[params.field as keyof TimeEntry]; | ||||
| const content = <GridEditInputCell {...params} />; | const content = <GridEditInputCell {...params} />; | ||||
| return errorMessage ? ( | return errorMessage ? ( | ||||
| <Tooltip title={t(errorMessage)}>{content}</Tooltip> | |||||
| <Tooltip title={t(errorMessage)}> | |||||
| <Box width="100%">{content}</Box> | |||||
| </Tooltip> | |||||
| ) : ( | ) : ( | ||||
| content | content | ||||
| ); | ); | ||||
| @@ -207,7 +207,7 @@ const FastTimeEntryModal: React.FC<Props> = ({ | |||||
| validate: (value) => { | validate: (value) => { | ||||
| if (value) { | if (value) { | ||||
| if (isHoliday) { | if (isHoliday) { | ||||
| return t("Cannot input normal hours for holidays"); | |||||
| return t("Cannot input normal hours on holidays"); | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -60,9 +60,9 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||||
| const openEditModal = useCallback( | const openEditModal = useCallback( | ||||
| (defaultValues?: TimeEntry) => () => { | (defaultValues?: TimeEntry) => () => { | ||||
| setEditModalProps({ | setEditModalProps({ | ||||
| defaultValues, | |||||
| defaultValues: defaultValues ? { ...defaultValues } : undefined, | |||||
| onDelete: defaultValues | onDelete: defaultValues | ||||
| ? () => { | |||||
| ? async () => { | |||||
| setValue( | setValue( | ||||
| date, | date, | ||||
| currentEntries.filter((entry) => entry.id !== defaultValues.id), | currentEntries.filter((entry) => entry.id !== defaultValues.id), | ||||
| @@ -27,7 +27,7 @@ import { DAILY_NORMAL_MAX_HOURS } from "@/app/api/timesheets/utils"; | |||||
| export interface Props extends Omit<ModalProps, "children"> { | export interface Props extends Omit<ModalProps, "children"> { | ||||
| onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise<void>; | onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise<void>; | ||||
| onDelete?: () => void; | |||||
| onDelete?: () => Promise<void>; | |||||
| defaultValues?: Partial<TimeEntry>; | defaultValues?: Partial<TimeEntry>; | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| @@ -94,6 +94,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| getValues, | getValues, | ||||
| setValue, | setValue, | ||||
| trigger, | trigger, | ||||
| setError, | |||||
| formState, | formState, | ||||
| watch, | watch, | ||||
| } = useForm<TimeEntry>(); | } = useForm<TimeEntry>(); | ||||
| @@ -105,10 +106,16 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| const saveHandler = useCallback(async () => { | const saveHandler = useCallback(async () => { | ||||
| const valid = await trigger(); | const valid = await trigger(); | ||||
| if (valid) { | if (valid) { | ||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | |||||
| try { | |||||
| await onSave(getValues(), recordDate); | |||||
| reset({ id: Date.now() }); | |||||
| } catch (e) { | |||||
| setError("root", { | |||||
| message: e instanceof Error ? e.message : "Unknown error", | |||||
| }); | |||||
| } | |||||
| } | } | ||||
| }, [getValues, onSave, recordDate, reset, trigger]); | |||||
| }, [getValues, onSave, recordDate, reset, setError, trigger]); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | ||||
| (...args) => { | (...args) => { | ||||
| @@ -227,7 +234,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| validate: (value) => { | validate: (value) => { | ||||
| if (value) { | if (value) { | ||||
| if (isHoliday) { | if (isHoliday) { | ||||
| return t("Cannot input normal hours for holidays"); | |||||
| return t("Cannot input normal hours on holidays"); | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -268,6 +275,11 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| })} | })} | ||||
| helperText={formState.errors.remark?.message} | helperText={formState.errors.remark?.message} | ||||
| /> | /> | ||||
| {formState.errors.root?.message && ( | |||||
| <Typography variant="caption" color="error"> | |||||
| {t(formState.errors.root.message, { DAILY_NORMAL_MAX_HOURS })} | |||||
| </Typography> | |||||
| )} | |||||
| <Box display="flex" justifyContent="flex-end" gap={1}> | <Box display="flex" justifyContent="flex-end" gap={1}> | ||||
| {onDelete && ( | {onDelete && ( | ||||
| <Button | <Button | ||||