From 4ce14145bd5e84de2abd132f4d1a024cc22b43f5 Mon Sep 17 00:00:00 2001 From: Wayne Date: Fri, 17 May 2024 18:03:56 +0900 Subject: [PATCH] Highlight holidays and round manhours --- src/app/utils/holidayUtils.ts | 23 +++++++++++++ src/app/utils/manhourUtils.ts | 3 ++ .../DateHoursTable/DateHoursList.tsx | 17 +++++++++- .../DateHoursTable/DateHoursTable.tsx | 21 ++++++++++-- src/components/LeaveModal/LeaveModal.tsx | 5 +++ src/components/LeaveTable/LeaveEditModal.tsx | 3 +- src/components/LeaveTable/LeaveEntryTable.tsx | 4 +++ src/components/LeaveTable/LeaveTable.tsx | 9 +++++- .../LeaveTable/MobileLeaveEntry.tsx | 32 ++++++++++++------- .../LeaveTable/MobileLeaveTable.tsx | 6 +++- .../TimesheetModal/TimesheetModal.tsx | 5 +++ .../TimesheetTable/EntryInputTable.tsx | 7 ++++ .../TimesheetTable/MobileTimesheetEntry.tsx | 15 ++++++++- .../TimesheetTable/MobileTimesheetTable.tsx | 6 +++- .../TimesheetTable/TimesheetEditModal.tsx | 9 ++++-- .../TimesheetTable/TimesheetTable.tsx | 4 +++ .../UserWorkspacePage/UserWorkspacePage.tsx | 4 ++- 17 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 src/app/utils/manhourUtils.ts diff --git a/src/app/utils/holidayUtils.ts b/src/app/utils/holidayUtils.ts index 2171f41..a39cc9a 100644 --- a/src/app/utils/holidayUtils.ts +++ b/src/app/utils/holidayUtils.ts @@ -1,4 +1,10 @@ import Holidays from "date-holidays"; +import { HolidaysResult } from "../api/holidays"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import { INPUT_DATE_FORMAT } from "./formatUtil"; + +dayjs.extend(arraySupport); const hd = new Holidays("HK"); @@ -47,3 +53,20 @@ export const getPublicHolidaysForNYears = (years: number = 1) => { }); }); }; + +export const getHolidayForDate = ( + date: string, + companyHolidays: HolidaysResult[] = [], +) => { + const currentYearHolidays: { date: string; title: string }[] = companyHolidays + .map((h) => ({ + title: h.name, + // Dayjs use 0-index for months, but not our API + date: dayjs([h.date[0], h.date[1] - 1, h.date[2]]).format( + INPUT_DATE_FORMAT, + ), + })) + .concat(getPublicHolidaysForNYears(1).concat()); + + return currentYearHolidays.find((h) => h.date === date); +}; diff --git a/src/app/utils/manhourUtils.ts b/src/app/utils/manhourUtils.ts new file mode 100644 index 0000000..37914ba --- /dev/null +++ b/src/app/utils/manhourUtils.ts @@ -0,0 +1,3 @@ +export const roundToNearestQuarter = (n: number): number => { + return Math.round(n / 0.25) * 0.25; +}; diff --git a/src/components/DateHoursTable/DateHoursList.tsx b/src/components/DateHoursTable/DateHoursList.tsx index 75b991b..685aef9 100644 --- a/src/components/DateHoursTable/DateHoursList.tsx +++ b/src/components/DateHoursTable/DateHoursList.tsx @@ -20,9 +20,12 @@ import { LEAVE_DAILY_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, } from "@/app/api/timesheets/utils"; +import { HolidaysResult } from "@/app/api/holidays"; +import { getHolidayForDate } from "@/app/utils/holidayUtils"; interface Props { days: string[]; + companyHolidays: HolidaysResult[]; leaveEntries: RecordLeaveInput; timesheetEntries: RecordTimesheetInput; EntryComponent: React.FunctionComponent< @@ -37,6 +40,7 @@ function DateHoursList({ timesheetEntries, EntryComponent, entryComponentProps, + companyHolidays, }: Props) { const { t, @@ -69,6 +73,11 @@ function DateHoursList({ {days.map((day, index) => { const dayJsObj = dayjs(day); + + const holiday = getHolidayForDate(day, companyHolidays); + const isHoliday = + holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const leaves = leaveEntries[day]; const leaveHours = leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; @@ -97,10 +106,16 @@ function DateHoursList({ variant="overline" component="div" sx={{ - color: dayJsObj.day() === 0 ? "error.main" : undefined, + color: isHoliday ? "error.main" : undefined, }} > {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} { days: string[]; leaveEntries: RecordLeaveInput; timesheetEntries: RecordTimesheetInput; + companyHolidays: HolidaysResult[]; EntryTableComponent: React.FunctionComponent< EntryTableProps & { day: string } >; @@ -40,6 +44,7 @@ function DateHoursTable({ entryTableProps, leaveEntries, timesheetEntries, + companyHolidays, }: Props) { const { t } = useTranslation("home"); @@ -61,6 +66,7 @@ function DateHoursTable({ ({ timesheetEntries, entryTableProps, EntryTableComponent, + companyHolidays }: { day: string; + companyHolidays: HolidaysResult[]; leaveEntries: RecordLeaveInput; timesheetEntries: RecordTimesheetInput; EntryTableComponent: React.FunctionComponent< @@ -96,6 +104,9 @@ function DayRow({ const dayJsObj = dayjs(day); const [open, setOpen] = useState(false); + const holiday = getHolidayForDate(day, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const leaves = leaveEntries[day]; const leaveHours = leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; @@ -125,10 +136,14 @@ function DayRow({ {open ? : } - + {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} {/* Timesheet */} {manhourFormatter.format(timesheetHours)} diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 31392d1..62c6aa3 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -25,6 +25,7 @@ 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"; interface Props { isOpen: boolean; @@ -33,6 +34,7 @@ interface Props { defaultLeaveRecords?: RecordLeaveInput; leaveTypes: LeaveType[]; timesheetRecords: RecordTimesheetInput; + companyHolidays: HolidaysResult[]; } const modalSx: SxProps = { @@ -52,6 +54,7 @@ const LeaveModal: React.FC = ({ defaultLeaveRecords, timesheetRecords, leaveTypes, + companyHolidays, }) => { const { t } = useTranslation("home"); @@ -127,6 +130,7 @@ const LeaveModal: React.FC = ({ }} > @@ -165,6 +169,7 @@ const LeaveModal: React.FC = ({ {t("Record Leave")} diff --git a/src/components/LeaveTable/LeaveEditModal.tsx b/src/components/LeaveTable/LeaveEditModal.tsx index b708596..e5c7d83 100644 --- a/src/components/LeaveTable/LeaveEditModal.tsx +++ b/src/components/LeaveTable/LeaveEditModal.tsx @@ -1,5 +1,6 @@ import { LeaveType } from "@/app/api/timesheets"; import { LeaveEntry } from "@/app/api/timesheets/actions"; +import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; import { Check, Delete } from "@mui/icons-material"; import { Box, @@ -98,7 +99,7 @@ const LeaveEditModal: React.FC = ({ label={t("Hours")} fullWidth {...register("inputHours", { - valueAsNumber: true, + setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), validate: (value) => value > 0, })} error={Boolean(formState.errors.inputHours)} diff --git a/src/components/LeaveTable/LeaveEntryTable.tsx b/src/components/LeaveTable/LeaveEntryTable.tsx index 9fb8172..d612112 100644 --- a/src/components/LeaveTable/LeaveEntryTable.tsx +++ b/src/components/LeaveTable/LeaveEntryTable.tsx @@ -22,6 +22,7 @@ import dayjs from "dayjs"; import isBetween from "dayjs/plugin/isBetween"; import { LeaveType } from "@/app/api/timesheets"; import { isValidLeaveEntry } from "@/app/api/timesheets/utils"; +import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; dayjs.extend(isBetween); @@ -173,6 +174,9 @@ const EntryInputTable: React.FC = ({ day, leaveTypes }) => { width: 150, editable: true, type: "number", + valueParser(value) { + return value ? roundToNearestQuarter(value) : value; + }, valueFormatter(params) { return manhourFormatter.format(params.value); }, diff --git a/src/components/LeaveTable/LeaveTable.tsx b/src/components/LeaveTable/LeaveTable.tsx index ce71d12..4e36f44 100644 --- a/src/components/LeaveTable/LeaveTable.tsx +++ b/src/components/LeaveTable/LeaveTable.tsx @@ -7,19 +7,26 @@ import { useFormContext } from "react-hook-form"; import LeaveEntryTable from "./LeaveEntryTable"; import { LeaveType } from "@/app/api/timesheets"; import DateHoursTable from "../DateHoursTable"; +import { HolidaysResult } from "@/app/api/holidays"; interface Props { leaveTypes: LeaveType[]; timesheetRecords: RecordTimesheetInput; + companyHolidays: HolidaysResult[]; } -const LeaveTable: React.FC = ({ leaveTypes, timesheetRecords }) => { +const LeaveTable: React.FC = ({ + leaveTypes, + timesheetRecords, + companyHolidays, +}) => { const { watch } = useFormContext(); const currentInput = watch(); const days = Object.keys(currentInput); return ( = ({ date, leaveTypes }) => { +const MobileLeaveEntry: React.FC = ({ + date, + leaveTypes, + companyHolidays, +}) => { const { t, i18n: { language }, } = useTranslation("home"); const dayJsObj = dayjs(date); + const holiday = getHolidayForDate(date, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; const leaveTypeMap = useMemo<{ [id: LeaveType["id"]]: LeaveType }>(() => { return leaveTypes.reduce( @@ -91,9 +93,15 @@ const MobileLeaveEntry: React.FC = ({ date, leaveTypes }) => { {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} = ({ timesheetRecords, leaveTypes, + companyHolidays, }) => { const { watch } = useFormContext(); const currentInput = watch(); @@ -24,10 +27,11 @@ const MobileLeaveTable: React.FC = ({ return ( ); }; diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index ef1d8a4..8214462 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -25,6 +25,7 @@ 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"; interface Props { isOpen: boolean; @@ -34,6 +35,7 @@ interface Props { username: string; defaultTimesheets?: RecordTimesheetInput; leaveRecords: RecordLeaveInput; + companyHolidays: HolidaysResult[]; } const modalSx: SxProps = { @@ -54,6 +56,7 @@ const TimesheetModal: React.FC = ({ username, defaultTimesheets, leaveRecords, + companyHolidays, }) => { const { t } = useTranslation("home"); @@ -129,6 +132,7 @@ const TimesheetModal: React.FC = ({ }} > = ({ {t("Timesheet Input")} = ({ width: 100, editable: true, type: "number", + valueParser(value) { + return value ? roundToNearestQuarter(value) : value; + }, valueFormatter(params) { return manhourFormatter.format(params.value || 0); }, @@ -318,6 +322,9 @@ const EntryInputTable: React.FC = ({ width: 150, editable: true, type: "number", + valueParser(value) { + return value ? roundToNearestQuarter(value) : value; + }, valueFormatter(params) { return manhourFormatter.format(params.value || 0); }, diff --git a/src/components/TimesheetTable/MobileTimesheetEntry.tsx b/src/components/TimesheetTable/MobileTimesheetEntry.tsx index bbae7a7..d1eaf56 100644 --- a/src/components/TimesheetTable/MobileTimesheetEntry.tsx +++ b/src/components/TimesheetTable/MobileTimesheetEntry.tsx @@ -18,17 +18,21 @@ import TimesheetEditModal, { Props as TimesheetEditModalProps, } from "./TimesheetEditModal"; import TimeEntryCard from "./TimeEntryCard"; +import { HolidaysResult } from "@/app/api/holidays"; +import { getHolidayForDate } from "@/app/utils/holidayUtils"; interface Props { date: string; allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; + companyHolidays: HolidaysResult[]; } const MobileTimesheetEntry: React.FC = ({ date, allProjects, assignedProjects, + companyHolidays, }) => { const { t, @@ -44,6 +48,9 @@ const MobileTimesheetEntry: React.FC = ({ }, [allProjects]); const dayJsObj = dayjs(date); + const holiday = getHolidayForDate(date, companyHolidays); + const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; + const { watch, setValue } = useFormContext(); const currentEntries = watch(date); @@ -99,9 +106,15 @@ const MobileTimesheetEntry: React.FC = ({ {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} = ({ allProjects, assignedProjects, leaveRecords, + companyHolidays, }) => { const { watch } = useFormContext(); const currentInput = watch(); @@ -26,10 +29,11 @@ const MobileTimesheetTable: React.FC = ({ return ( ); }; diff --git a/src/components/TimesheetTable/TimesheetEditModal.tsx b/src/components/TimesheetTable/TimesheetEditModal.tsx index a26453f..b89427b 100644 --- a/src/components/TimesheetTable/TimesheetEditModal.tsx +++ b/src/components/TimesheetTable/TimesheetEditModal.tsx @@ -20,6 +20,7 @@ import TaskGroupSelect from "./TaskGroupSelect"; import TaskSelect from "./TaskSelect"; import { TaskGroup } from "@/app/api/tasks"; import uniqBy from "lodash/uniqBy"; +import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; export interface Props extends Omit { onSave: (leaveEntry: TimeEntry) => void; @@ -196,8 +197,9 @@ const TimesheetEditModal: React.FC = ({ label={t("Hours")} fullWidth {...register("inputHours", { - valueAsNumber: true, - validate: (value) => Boolean(value || otHours), + setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), + validate: (value) => + value ? value > 0 : Boolean(value || otHours), })} error={Boolean(formState.errors.inputHours)} /> @@ -206,7 +208,8 @@ const TimesheetEditModal: React.FC = ({ label={t("Other Hours")} fullWidth {...register("otHours", { - valueAsNumber: true, + setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), + validate: (value) => (value ? value > 0 : true), })} error={Boolean(formState.errors.otHours)} /> diff --git a/src/components/TimesheetTable/TimesheetTable.tsx b/src/components/TimesheetTable/TimesheetTable.tsx index 659c488..3b4bfff 100644 --- a/src/components/TimesheetTable/TimesheetTable.tsx +++ b/src/components/TimesheetTable/TimesheetTable.tsx @@ -7,17 +7,20 @@ import { useFormContext } from "react-hook-form"; import EntryInputTable from "./EntryInputTable"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import DateHoursTable from "../DateHoursTable"; +import { HolidaysResult } from "@/app/api/holidays"; interface Props { allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; leaveRecords: RecordLeaveInput; + companyHolidays: HolidaysResult[]; } const TimesheetTable: React.FC = ({ allProjects, assignedProjects, leaveRecords, + companyHolidays, }) => { const { watch } = useFormContext(); const currentInput = watch(); @@ -25,6 +28,7 @@ const TimesheetTable: React.FC = ({ return ( = ({ username, defaultLeaveRecords, defaultTimesheets, - holidays + holidays, }) => { const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); @@ -106,6 +106,7 @@ const UserWorkspacePage: React.FC = ({ leaveTypes={leaveTypes} /> = ({ leaveRecords={defaultLeaveRecords} />