| 作者 | SHA1 | 備註 | 提交日期 |
|---|---|---|---|
|
|
56799174cc | Allow temporary saving for timesheet entry | 1 年之前 |
|
|
a20b375dce | Optional remarks for miscellaneous tasks | 1 年之前 |
| @@ -47,7 +47,7 @@ export const validateTimeEntry = ( | |||||
| } else { | } else { | ||||
| if (entry.taskGroupId && !entry.taskId) { | if (entry.taskGroupId && !entry.taskId) { | ||||
| error.taskId = "Required"; | error.taskId = "Required"; | ||||
| } else if (!entry.remark) { | |||||
| } else if (!entry.taskGroupId && !entry.remark) { | |||||
| error.remark = "Required for non-billable tasks"; | error.remark = "Required for non-billable tasks"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -133,7 +133,10 @@ export const validateTimeLeaveRecord = ( | |||||
| } | } | ||||
| }); | }); | ||||
| return Object.keys(errors).length > 0 ? errors : undefined; | |||||
| const hasErrors = Object.keys(errors).length > 0; | |||||
| const temporarilySaveable = isTemporarilySaveable(records, errors, holidays); | |||||
| return !temporarilySaveable && hasErrors ? errors : undefined; | |||||
| }; | }; | ||||
| export const checkTotalHours = ( | export const checkTotalHours = ( | ||||
| @@ -180,3 +183,35 @@ export const checkTotalHours = ( | |||||
| export const DAILY_NORMAL_MAX_HOURS = 8; | export const DAILY_NORMAL_MAX_HOURS = 8; | ||||
| export const TIMESHEET_DAILY_MAX_HOURS = 20; | export const TIMESHEET_DAILY_MAX_HOURS = 20; | ||||
| export const isTemporarilySaveable = ( | |||||
| records: RecordTimeLeaveInput, | |||||
| errors: { [date: string]: string }, | |||||
| holidays: Set<string>, | |||||
| ): boolean => { | |||||
| const filledDates = Object.keys(records) | |||||
| .reduce<{ date: string; hasFilled: boolean }[]>((acc, date) => { | |||||
| const dayJsObj = dayjs(date); | |||||
| const isHoliday = | |||||
| holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| if (isHoliday) { | |||||
| return acc; | |||||
| } | |||||
| return [...acc, { date, hasFilled: !Boolean(errors[date]) }]; | |||||
| }, []) | |||||
| .sort((a, b) => dayjs(a.date).diff(dayjs(b.date))); | |||||
| const isConsecutivelyFilled = filledDates.every((currentDate, index) => { | |||||
| if (index === 0) { | |||||
| return true; | |||||
| } | |||||
| if (currentDate.hasFilled && !filledDates[index - 1].hasFilled) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| return isConsecutivelyFilled; | |||||
| }; | |||||
| @@ -33,6 +33,7 @@ interface Props<EntryComponentProps = object> { | |||||
| >; | >; | ||||
| entryComponentProps: EntryComponentProps; | entryComponentProps: EntryComponentProps; | ||||
| errorComponent?: React.ReactNode; | errorComponent?: React.ReactNode; | ||||
| onSubmit?: () => void; | |||||
| } | } | ||||
| function DateHoursList<EntryTableProps>({ | function DateHoursList<EntryTableProps>({ | ||||
| @@ -43,6 +44,7 @@ function DateHoursList<EntryTableProps>({ | |||||
| entryComponentProps, | entryComponentProps, | ||||
| companyHolidays, | companyHolidays, | ||||
| errorComponent, | errorComponent, | ||||
| onSubmit, | |||||
| }: Props<EntryTableProps>) { | }: Props<EntryTableProps>) { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -240,7 +242,12 @@ function DateHoursList<EntryTableProps>({ | |||||
| {t("Done")} | {t("Done")} | ||||
| </Button> | </Button> | ||||
| ) : ( | ) : ( | ||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| onClick={onSubmit} | |||||
| > | |||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| @@ -237,7 +237,14 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| > | > | ||||
| {t("Cancel")} | {t("Cancel")} | ||||
| </Button> | </Button> | ||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| onClick={() => { | |||||
| formProps.clearErrors(); | |||||
| }} | |||||
| > | |||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||
| @@ -277,6 +284,9 @@ const TimeLeaveModal: React.FC<Props> = ({ | |||||
| miscTasks, | miscTasks, | ||||
| }} | }} | ||||
| errorComponent={errorComponent} | errorComponent={errorComponent} | ||||
| onSubmit={() => { | |||||
| formProps.clearErrors(); | |||||
| }} | |||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| </FullscreenModal> | </FullscreenModal> | ||||
| @@ -304,7 +304,7 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| error={Boolean(formState.errors.remark)} | error={Boolean(formState.errors.remark)} | ||||
| {...register("remark", { | {...register("remark", { | ||||
| validate: (value) => | validate: (value) => | ||||
| Boolean(projectId || value) || | |||||
| Boolean(projectId || taskGroupId || value) || | |||||
| t("Required for non-billable tasks"), | t("Required for non-billable tasks"), | ||||
| })} | })} | ||||
| helperText={formState.errors.remark?.message} | helperText={formState.errors.remark?.message} | ||||