소스 검색

Add some validation error message for timesheet entry

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 년 전
부모
커밋
46d44db3a7
7개의 변경된 파일111개의 추가작업 그리고 24개의 파일을 삭제
  1. +23
    -9
      src/app/api/timesheets/utils.ts
  2. +12
    -4
      src/components/DateHoursTable/DateHoursTable.tsx
  3. +2
    -1
      src/components/LeaveTable/LeaveEditModal.tsx
  4. +1
    -0
      src/components/LeaveTable/LeaveEntryTable.tsx
  5. +47
    -7
      src/components/TimesheetTable/EntryInputTable.tsx
  6. +1
    -0
      src/components/TimesheetTable/MobileTimesheetEntry.tsx
  7. +25
    -3
      src/components/TimesheetTable/TimesheetEditModal.tsx

+ 23
- 9
src/app/api/timesheets/utils.ts 파일 보기

@@ -1,36 +1,49 @@
import { LeaveEntry, TimeEntry } from "./actions";

export type TimeEntryError = {
[field in keyof TimeEntry]?: string;
};

/**
* @param entry - the time entry
* @returns the field where there is an error, or an empty string if there is none
*/
export const isValidTimeEntry = (entry: Partial<TimeEntry>): string => {
export const validateTimeEntry = (
entry: Partial<TimeEntry>,
isHoliday: boolean,
): TimeEntryError | undefined => {
// Test for errors
let error: keyof TimeEntry | "" = "";
const error: TimeEntryError = {};

// Either normal or other hours need to be inputted
if (!entry.inputHours && !entry.otHours) {
error = "inputHours";
error[isHoliday ? "otHours" : "inputHours"] = "Required";
} else if (entry.inputHours && isHoliday) {
error.inputHours = "Cannot input normal hours for holidays";
} else if (entry.inputHours && entry.inputHours <= 0) {
error = "inputHours";
error.inputHours =
"Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
} else if (entry.inputHours && entry.inputHours > DAILY_NORMAL_MAX_HOURS) {
error.inputHours =
"Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
} else if (entry.otHours && entry.otHours <= 0) {
error = "otHours";
error.otHours = "Hours should be bigger than 0";
}

// If there is a project id, there should also be taskGroupId, taskId, inputHours
if (entry.projectId) {
if (!entry.taskGroupId) {
error = "taskGroupId";
error.taskGroupId = "Required";
} else if (!entry.taskId) {
error = "taskId";
error.taskId = "Required";
}
} else {
if (!entry.remark) {
error = "remark";
error.remark = "Required for non-billable tasks";
}
}

return error;
return Object.keys(error).length > 0 ? error : undefined;
};

export const isValidLeaveEntry = (entry: Partial<LeaveEntry>): string => {
@@ -45,5 +58,6 @@ export const isValidLeaveEntry = (entry: Partial<LeaveEntry>): string => {
return error;
};

export const DAILY_NORMAL_MAX_HOURS = 8;
export const LEAVE_DAILY_MAX_HOURS = 8;
export const TIMESHEET_DAILY_MAX_HOURS = 20;

+ 12
- 4
src/components/DateHoursTable/DateHoursTable.tsx 파일 보기

@@ -33,7 +33,7 @@ interface Props<EntryTableProps = object> {
timesheetEntries: RecordTimesheetInput;
companyHolidays: HolidaysResult[];
EntryTableComponent: React.FunctionComponent<
EntryTableProps & { day: string }
EntryTableProps & { day: string; isHoliday: boolean }
>;
entryTableProps: EntryTableProps;
}
@@ -86,14 +86,14 @@ function DayRow<EntryTableProps>({
timesheetEntries,
entryTableProps,
EntryTableComponent,
companyHolidays
companyHolidays,
}: {
day: string;
companyHolidays: HolidaysResult[];
leaveEntries: RecordLeaveInput;
timesheetEntries: RecordTimesheetInput;
EntryTableComponent: React.FunctionComponent<
EntryTableProps & { day: string }
EntryTableProps & { day: string; isHoliday: boolean }
>;
entryTableProps: EntryTableProps;
}) {
@@ -200,7 +200,15 @@ function DayRow<EntryTableProps>({
colSpan={5}
>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>{<EntryTableComponent day={day} {...entryTableProps} />}</Box>
<Box>
{
<EntryTableComponent
day={day}
isHoliday={Boolean(isHoliday)}
{...entryTableProps}
/>
}
</Box>
</Collapse>
</TableCell>
</TableRow>


+ 2
- 1
src/components/LeaveTable/LeaveEditModal.tsx 파일 보기

@@ -1,5 +1,6 @@
import { LeaveType } from "@/app/api/timesheets";
import { LeaveEntry } from "@/app/api/timesheets/actions";
import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
import { Check, Delete } from "@mui/icons-material";
import {
@@ -100,7 +101,7 @@ const LeaveEditModal: React.FC<Props> = ({
fullWidth
{...register("inputHours", {
setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
validate: (value) => value > 0,
validate: (value) => 0 < value && value <= LEAVE_DAILY_MAX_HOURS,
})}
error={Boolean(formState.errors.inputHours)}
/>


+ 1
- 0
src/components/LeaveTable/LeaveEntryTable.tsx 파일 보기

@@ -28,6 +28,7 @@ dayjs.extend(isBetween);

interface Props {
day: string;
isHoliday: boolean;
leaveTypes: LeaveType[];
}



+ 47
- 7
src/components/TimesheetTable/EntryInputTable.tsx 파일 보기

@@ -1,9 +1,11 @@
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 {
FooterPropsOverrides,
GridActionsCellItem,
GridCellParams,
GridColDef,
GridEditInputCell,
GridEventListener,
GridRenderEditCellParams,
GridRowId,
@@ -27,13 +29,18 @@ import isBetween from "dayjs/plugin/isBetween";
import ProjectSelect from "./ProjectSelect";
import TaskGroupSelect from "./TaskGroupSelect";
import TaskSelect from "./TaskSelect";
import { isValidTimeEntry } from "@/app/api/timesheets/utils";
import {
DAILY_NORMAL_MAX_HOURS,
TimeEntryError,
validateTimeEntry,
} from "@/app/api/timesheets/utils";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";

dayjs.extend(isBetween);

interface Props {
day: string;
isHoliday: boolean;
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
}
@@ -41,7 +48,7 @@ interface Props {
export type TimeEntryRow = Partial<
TimeEntry & {
_isNew: boolean;
_error: string;
_error: TimeEntryError;
isPlanned?: boolean;
}
>;
@@ -50,6 +57,7 @@ const EntryInputTable: React.FC<Props> = ({
day,
allProjects,
assignedProjects,
isHoliday,
}) => {
const { t } = useTranslation("home");
const taskGroupsByProject = useMemo(() => {
@@ -105,7 +113,7 @@ const EntryInputTable: React.FC<Props> = ({
"",
) as TimeEntryRow;

const error = isValidTimeEntry(row);
const error = validateTimeEntry(row, isHoliday);

// Test for warnings
let isPlanned;
@@ -124,7 +132,7 @@ const EntryInputTable: React.FC<Props> = ({
apiRef.current.updateRows([{ id, _error: error, isPlanned }]);
return !error;
},
[apiRef, day, milestonesByProject],
[apiRef, day, isHoliday, milestonesByProject],
);

const handleCancel = useCallback(
@@ -309,6 +317,18 @@ const EntryInputTable: React.FC<Props> = ({
width: 100,
editable: true,
type: "number",
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) {
const errorMessage =
params.row._error?.[params.field as keyof TimeEntry];
const content = <GridEditInputCell {...params} />;
return errorMessage ? (
<Tooltip title={t(errorMessage, { DAILY_NORMAL_MAX_HOURS })}>
{content}
</Tooltip>
) : (
content
);
},
valueParser(value) {
return value ? roundToNearestQuarter(value) : value;
},
@@ -322,6 +342,16 @@ const EntryInputTable: React.FC<Props> = ({
width: 150,
editable: true,
type: "number",
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) {
const errorMessage =
params.row._error?.[params.field as keyof TimeEntry];
const content = <GridEditInputCell {...params} />;
return errorMessage ? (
<Tooltip title={t(errorMessage)}>{content}</Tooltip>
) : (
content
);
},
valueParser(value) {
return value ? roundToNearestQuarter(value) : value;
},
@@ -335,6 +365,16 @@ const EntryInputTable: React.FC<Props> = ({
sortable: false,
flex: 1,
editable: true,
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow>) {
const errorMessage =
params.row._error?.[params.field as keyof TimeEntry];
const content = <GridEditInputCell {...params} />;
return errorMessage ? (
<Tooltip title={t(errorMessage)}>{content}</Tooltip>
) : (
content
);
},
},
],
[
@@ -406,9 +446,9 @@ const EntryInputTable: React.FC<Props> = ({
onRowEditStop={handleEditStop}
processRowUpdate={processRowUpdate}
columns={columns}
getCellClassName={(params) => {
getCellClassName={(params: GridCellParams<TimeEntryRow>) => {
let classname = "";
if (params.row._error === params.field) {
if (params.row._error?.[params.field as keyof TimeEntry]) {
classname = "hasError";
} else if (
params.field === "taskGroupId" &&


+ 1
- 0
src/components/TimesheetTable/MobileTimesheetEntry.tsx 파일 보기

@@ -158,6 +158,7 @@ const MobileTimesheetEntry: React.FC<Props> = ({
open={editModalOpen}
onClose={closeEditModal}
onSave={onSaveEntry}
isHoliday={Boolean(isHoliday)}
{...editModalProps}
/>
</Box>


+ 25
- 3
src/components/TimesheetTable/TimesheetEditModal.tsx 파일 보기

@@ -23,6 +23,7 @@ import { TaskGroup } from "@/app/api/tasks";
import uniqBy from "lodash/uniqBy";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
import { shortDateFormatter } from "@/app/utils/formatUtil";
import { DAILY_NORMAL_MAX_HOURS } from "@/app/api/timesheets/utils";

export interface Props extends Omit<ModalProps, "children"> {
onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise<void>;
@@ -32,6 +33,7 @@ export interface Props extends Omit<ModalProps, "children"> {
assignedProjects: AssignedProject[];
modalSx?: SxProps;
recordDate?: string;
isHoliday?: boolean;
}

const modalSx: SxProps = {
@@ -56,6 +58,7 @@ const TimesheetEditModal: React.FC<Props> = ({
assignedProjects,
modalSx: mSx,
recordDate,
isHoliday,
}) => {
const {
t,
@@ -212,10 +215,26 @@ const TimesheetEditModal: React.FC<Props> = ({
fullWidth
{...register("inputHours", {
setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
validate: (value) =>
value ? value > 0 : Boolean(value || otHours),
validate: (value) => {
if (value) {
if (isHoliday) {
return t("Cannot input normal hours for 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 },
)
);
} else {
return Boolean(value || otHours) || t("Required");
}
},
})}
error={Boolean(formState.errors.inputHours)}
helperText={formState.errors.inputHours?.message}
/>
<TextField
type="number"
@@ -234,8 +253,11 @@ const TimesheetEditModal: React.FC<Props> = ({
rows={2}
error={Boolean(formState.errors.remark)}
{...register("remark", {
validate: (value) => Boolean(projectId || value),
validate: (value) =>
Boolean(projectId || value) ||
t("Required for non-billable tasks"),
})}
helperText={formState.errors.remark?.message}
/>
<Box display="flex" justifyContent="flex-end" gap={1}>
{onDelete && (


불러오는 중...
취소
저장