| @@ -1,4 +1,10 @@ | |||||
| import Holidays from "date-holidays"; | 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"); | 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); | |||||
| }; | |||||
| @@ -0,0 +1,3 @@ | |||||
| export const roundToNearestQuarter = (n: number): number => { | |||||
| return Math.round(n / 0.25) * 0.25; | |||||
| }; | |||||
| @@ -20,9 +20,12 @@ import { | |||||
| LEAVE_DAILY_MAX_HOURS, | LEAVE_DAILY_MAX_HOURS, | ||||
| TIMESHEET_DAILY_MAX_HOURS, | TIMESHEET_DAILY_MAX_HOURS, | ||||
| } from "@/app/api/timesheets/utils"; | } from "@/app/api/timesheets/utils"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| interface Props<EntryComponentProps = object> { | interface Props<EntryComponentProps = object> { | ||||
| days: string[]; | days: string[]; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
| timesheetEntries: RecordTimesheetInput; | timesheetEntries: RecordTimesheetInput; | ||||
| EntryComponent: React.FunctionComponent< | EntryComponent: React.FunctionComponent< | ||||
| @@ -37,6 +40,7 @@ function DateHoursList<EntryTableProps>({ | |||||
| timesheetEntries, | timesheetEntries, | ||||
| EntryComponent, | EntryComponent, | ||||
| entryComponentProps, | entryComponentProps, | ||||
| companyHolidays, | |||||
| }: Props<EntryTableProps>) { | }: Props<EntryTableProps>) { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -69,6 +73,11 @@ function DateHoursList<EntryTableProps>({ | |||||
| <Box overflow="scroll" flex={1}> | <Box overflow="scroll" flex={1}> | ||||
| {days.map((day, index) => { | {days.map((day, index) => { | ||||
| const dayJsObj = dayjs(day); | const dayJsObj = dayjs(day); | ||||
| const holiday = getHolidayForDate(day, companyHolidays); | |||||
| const isHoliday = | |||||
| holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const leaves = leaveEntries[day]; | const leaves = leaveEntries[day]; | ||||
| const leaveHours = | const leaveHours = | ||||
| leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | ||||
| @@ -97,10 +106,16 @@ function DateHoursList<EntryTableProps>({ | |||||
| variant="overline" | variant="overline" | ||||
| component="div" | component="div" | ||||
| sx={{ | sx={{ | ||||
| color: dayJsObj.day() === 0 ? "error.main" : undefined, | |||||
| color: isHoliday ? "error.main" : undefined, | |||||
| }} | }} | ||||
| > | > | ||||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | {shortDateFormatter(language).format(dayJsObj.toDate())} | ||||
| {holiday && ( | |||||
| <Typography | |||||
| marginInlineStart={1} | |||||
| variant="caption" | |||||
| >{`(${holiday.title})`}</Typography> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| <Stack spacing={1}> | <Stack spacing={1}> | ||||
| <Box | <Box | ||||
| @@ -15,6 +15,7 @@ import { | |||||
| TableHead, | TableHead, | ||||
| TableRow, | TableRow, | ||||
| Tooltip, | Tooltip, | ||||
| Typography, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import React, { useState } from "react"; | import React, { useState } from "react"; | ||||
| @@ -23,11 +24,14 @@ import { | |||||
| LEAVE_DAILY_MAX_HOURS, | LEAVE_DAILY_MAX_HOURS, | ||||
| TIMESHEET_DAILY_MAX_HOURS, | TIMESHEET_DAILY_MAX_HOURS, | ||||
| } from "@/app/api/timesheets/utils"; | } from "@/app/api/timesheets/utils"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| interface Props<EntryTableProps = object> { | interface Props<EntryTableProps = object> { | ||||
| days: string[]; | days: string[]; | ||||
| leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
| timesheetEntries: RecordTimesheetInput; | timesheetEntries: RecordTimesheetInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| EntryTableComponent: React.FunctionComponent< | EntryTableComponent: React.FunctionComponent< | ||||
| EntryTableProps & { day: string } | EntryTableProps & { day: string } | ||||
| >; | >; | ||||
| @@ -40,6 +44,7 @@ function DateHoursTable<EntryTableProps>({ | |||||
| entryTableProps, | entryTableProps, | ||||
| leaveEntries, | leaveEntries, | ||||
| timesheetEntries, | timesheetEntries, | ||||
| companyHolidays, | |||||
| }: Props<EntryTableProps>) { | }: Props<EntryTableProps>) { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -61,6 +66,7 @@ function DateHoursTable<EntryTableProps>({ | |||||
| <DayRow | <DayRow | ||||
| key={`${day}${index}`} | key={`${day}${index}`} | ||||
| day={day} | day={day} | ||||
| companyHolidays={companyHolidays} | |||||
| leaveEntries={leaveEntries} | leaveEntries={leaveEntries} | ||||
| timesheetEntries={timesheetEntries} | timesheetEntries={timesheetEntries} | ||||
| EntryTableComponent={EntryTableComponent} | EntryTableComponent={EntryTableComponent} | ||||
| @@ -80,8 +86,10 @@ function DayRow<EntryTableProps>({ | |||||
| timesheetEntries, | timesheetEntries, | ||||
| entryTableProps, | entryTableProps, | ||||
| EntryTableComponent, | EntryTableComponent, | ||||
| companyHolidays | |||||
| }: { | }: { | ||||
| day: string; | day: string; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| leaveEntries: RecordLeaveInput; | leaveEntries: RecordLeaveInput; | ||||
| timesheetEntries: RecordTimesheetInput; | timesheetEntries: RecordTimesheetInput; | ||||
| EntryTableComponent: React.FunctionComponent< | EntryTableComponent: React.FunctionComponent< | ||||
| @@ -96,6 +104,9 @@ function DayRow<EntryTableProps>({ | |||||
| const dayJsObj = dayjs(day); | const dayJsObj = dayjs(day); | ||||
| const [open, setOpen] = useState(false); | const [open, setOpen] = useState(false); | ||||
| const holiday = getHolidayForDate(day, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const leaves = leaveEntries[day]; | const leaves = leaveEntries[day]; | ||||
| const leaveHours = | const leaveHours = | ||||
| leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0; | ||||
| @@ -125,10 +136,14 @@ function DayRow<EntryTableProps>({ | |||||
| {open ? <KeyboardArrowUp /> : <KeyboardArrowDown />} | {open ? <KeyboardArrowUp /> : <KeyboardArrowDown />} | ||||
| </IconButton> | </IconButton> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell | |||||
| sx={{ color: dayJsObj.day() === 0 ? "error.main" : undefined }} | |||||
| > | |||||
| <TableCell sx={{ color: isHoliday ? "error.main" : undefined }}> | |||||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | {shortDateFormatter(language).format(dayJsObj.toDate())} | ||||
| {holiday && ( | |||||
| <Typography | |||||
| display="block" | |||||
| variant="caption" | |||||
| >{`(${holiday.title})`}</Typography> | |||||
| )} | |||||
| </TableCell> | </TableCell> | ||||
| {/* Timesheet */} | {/* Timesheet */} | ||||
| <TableCell>{manhourFormatter.format(timesheetHours)}</TableCell> | <TableCell>{manhourFormatter.format(timesheetHours)}</TableCell> | ||||
| @@ -25,6 +25,7 @@ import { LeaveType } from "@/app/api/timesheets"; | |||||
| import FullscreenModal from "../FullscreenModal"; | import FullscreenModal from "../FullscreenModal"; | ||||
| import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; | import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; | ||||
| import useIsMobile from "@/app/utils/useIsMobile"; | import useIsMobile from "@/app/utils/useIsMobile"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| isOpen: boolean; | isOpen: boolean; | ||||
| @@ -33,6 +34,7 @@ interface Props { | |||||
| defaultLeaveRecords?: RecordLeaveInput; | defaultLeaveRecords?: RecordLeaveInput; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| timesheetRecords: RecordTimesheetInput; | timesheetRecords: RecordTimesheetInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -52,6 +54,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| defaultLeaveRecords, | defaultLeaveRecords, | ||||
| timesheetRecords, | timesheetRecords, | ||||
| leaveTypes, | leaveTypes, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -127,6 +130,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| }} | }} | ||||
| > | > | ||||
| <LeaveTable | <LeaveTable | ||||
| companyHolidays={companyHolidays} | |||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| timesheetRecords={timesheetRecords} | timesheetRecords={timesheetRecords} | ||||
| /> | /> | ||||
| @@ -165,6 +169,7 @@ const LeaveModal: React.FC<Props> = ({ | |||||
| {t("Record Leave")} | {t("Record Leave")} | ||||
| </Typography> | </Typography> | ||||
| <MobileLeaveTable | <MobileLeaveTable | ||||
| companyHolidays={companyHolidays} | |||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| timesheetRecords={timesheetRecords} | timesheetRecords={timesheetRecords} | ||||
| /> | /> | ||||
| @@ -1,5 +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 { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | |||||
| import { Check, Delete } from "@mui/icons-material"; | import { Check, Delete } from "@mui/icons-material"; | ||||
| import { | import { | ||||
| Box, | Box, | ||||
| @@ -98,7 +99,7 @@ const LeaveEditModal: React.FC<Props> = ({ | |||||
| label={t("Hours")} | label={t("Hours")} | ||||
| fullWidth | fullWidth | ||||
| {...register("inputHours", { | {...register("inputHours", { | ||||
| valueAsNumber: true, | |||||
| setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), | |||||
| validate: (value) => value > 0, | validate: (value) => value > 0, | ||||
| })} | })} | ||||
| error={Boolean(formState.errors.inputHours)} | error={Boolean(formState.errors.inputHours)} | ||||
| @@ -22,6 +22,7 @@ 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 { isValidLeaveEntry } from "@/app/api/timesheets/utils"; | ||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | |||||
| dayjs.extend(isBetween); | dayjs.extend(isBetween); | ||||
| @@ -173,6 +174,9 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => { | |||||
| width: 150, | width: 150, | ||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| valueParser(value) { | |||||
| return value ? roundToNearestQuarter(value) : value; | |||||
| }, | |||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| return manhourFormatter.format(params.value); | return manhourFormatter.format(params.value); | ||||
| }, | }, | ||||
| @@ -7,19 +7,26 @@ import { useFormContext } from "react-hook-form"; | |||||
| import LeaveEntryTable from "./LeaveEntryTable"; | import LeaveEntryTable from "./LeaveEntryTable"; | ||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import DateHoursTable from "../DateHoursTable"; | import DateHoursTable from "../DateHoursTable"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| timesheetRecords: RecordTimesheetInput; | timesheetRecords: RecordTimesheetInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const LeaveTable: React.FC<Props> = ({ leaveTypes, timesheetRecords }) => { | |||||
| const LeaveTable: React.FC<Props> = ({ | |||||
| leaveTypes, | |||||
| timesheetRecords, | |||||
| companyHolidays, | |||||
| }) => { | |||||
| const { watch } = useFormContext<RecordLeaveInput>(); | const { watch } = useFormContext<RecordLeaveInput>(); | ||||
| const currentInput = watch(); | const currentInput = watch(); | ||||
| const days = Object.keys(currentInput); | const days = Object.keys(currentInput); | ||||
| return ( | return ( | ||||
| <DateHoursTable | <DateHoursTable | ||||
| companyHolidays={companyHolidays} | |||||
| days={days} | days={days} | ||||
| leaveEntries={currentInput} | leaveEntries={currentInput} | ||||
| timesheetEntries={timesheetRecords} | timesheetEntries={timesheetRecords} | ||||
| @@ -1,33 +1,35 @@ | |||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import { LeaveEntry, RecordLeaveInput } from "@/app/api/timesheets/actions"; | import { LeaveEntry, RecordLeaveInput } from "@/app/api/timesheets/actions"; | ||||
| import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
| import { Add, Edit } from "@mui/icons-material"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Card, | |||||
| CardContent, | |||||
| IconButton, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { shortDateFormatter } from "@/app/utils/formatUtil"; | |||||
| import { Add } from "@mui/icons-material"; | |||||
| import { Box, Button, Typography } from "@mui/material"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import React, { useCallback, useMemo, useState } from "react"; | import React, { useCallback, useMemo, useState } from "react"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal"; | import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal"; | ||||
| import LeaveEntryCard from "./LeaveEntryCard"; | import LeaveEntryCard from "./LeaveEntryCard"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| interface Props { | interface Props { | ||||
| date: string; | date: string; | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => { | |||||
| const MobileLeaveEntry: React.FC<Props> = ({ | |||||
| date, | |||||
| leaveTypes, | |||||
| companyHolidays, | |||||
| }) => { | |||||
| const { | const { | ||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| } = useTranslation("home"); | } = useTranslation("home"); | ||||
| const dayJsObj = dayjs(date); | 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 }>(() => { | const leaveTypeMap = useMemo<{ [id: LeaveType["id"]]: LeaveType }>(() => { | ||||
| return leaveTypes.reduce( | return leaveTypes.reduce( | ||||
| @@ -91,9 +93,15 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => { | |||||
| <Typography | <Typography | ||||
| paddingInline={2} | paddingInline={2} | ||||
| variant="overline" | variant="overline" | ||||
| color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||||
| color={isHoliday ? "error.main" : undefined} | |||||
| > | > | ||||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | {shortDateFormatter(language).format(dayJsObj.toDate())} | ||||
| {holiday && ( | |||||
| <Typography | |||||
| marginInlineStart={1} | |||||
| variant="caption" | |||||
| >{`(${holiday.title})`}</Typography> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| <Box | <Box | ||||
| paddingInline={2} | paddingInline={2} | ||||
| @@ -7,15 +7,18 @@ import { useFormContext } from "react-hook-form"; | |||||
| import { LeaveType } from "@/app/api/timesheets"; | import { LeaveType } from "@/app/api/timesheets"; | ||||
| import MobileLeaveEntry from "./MobileLeaveEntry"; | import MobileLeaveEntry from "./MobileLeaveEntry"; | ||||
| import DateHoursList from "../DateHoursTable/DateHoursList"; | import DateHoursList from "../DateHoursTable/DateHoursList"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| leaveTypes: LeaveType[]; | leaveTypes: LeaveType[]; | ||||
| timesheetRecords: RecordTimesheetInput; | timesheetRecords: RecordTimesheetInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const MobileLeaveTable: React.FC<Props> = ({ | const MobileLeaveTable: React.FC<Props> = ({ | ||||
| timesheetRecords, | timesheetRecords, | ||||
| leaveTypes, | leaveTypes, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { watch } = useFormContext<RecordLeaveInput>(); | const { watch } = useFormContext<RecordLeaveInput>(); | ||||
| const currentInput = watch(); | const currentInput = watch(); | ||||
| @@ -24,10 +27,11 @@ const MobileLeaveTable: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <DateHoursList | <DateHoursList | ||||
| days={days} | days={days} | ||||
| companyHolidays={companyHolidays} | |||||
| leaveEntries={currentInput} | leaveEntries={currentInput} | ||||
| timesheetEntries={timesheetRecords} | timesheetEntries={timesheetRecords} | ||||
| EntryComponent={MobileLeaveEntry} | EntryComponent={MobileLeaveEntry} | ||||
| entryComponentProps={{ leaveTypes }} | |||||
| entryComponentProps={{ leaveTypes, companyHolidays }} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -25,6 +25,7 @@ import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | |||||
| import FullscreenModal from "../FullscreenModal"; | import FullscreenModal from "../FullscreenModal"; | ||||
| import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; | import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; | ||||
| import useIsMobile from "@/app/utils/useIsMobile"; | import useIsMobile from "@/app/utils/useIsMobile"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| isOpen: boolean; | isOpen: boolean; | ||||
| @@ -34,6 +35,7 @@ interface Props { | |||||
| username: string; | username: string; | ||||
| defaultTimesheets?: RecordTimesheetInput; | defaultTimesheets?: RecordTimesheetInput; | ||||
| leaveRecords: RecordLeaveInput; | leaveRecords: RecordLeaveInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| @@ -54,6 +56,7 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| username, | username, | ||||
| defaultTimesheets, | defaultTimesheets, | ||||
| leaveRecords, | leaveRecords, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -129,6 +132,7 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| }} | }} | ||||
| > | > | ||||
| <TimesheetTable | <TimesheetTable | ||||
| companyHolidays={companyHolidays} | |||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| leaveRecords={leaveRecords} | leaveRecords={leaveRecords} | ||||
| @@ -168,6 +172,7 @@ const TimesheetModal: React.FC<Props> = ({ | |||||
| {t("Timesheet Input")} | {t("Timesheet Input")} | ||||
| </Typography> | </Typography> | ||||
| <MobileTimesheetTable | <MobileTimesheetTable | ||||
| companyHolidays={companyHolidays} | |||||
| assignedProjects={assignedProjects} | assignedProjects={assignedProjects} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| leaveRecords={leaveRecords} | leaveRecords={leaveRecords} | ||||
| @@ -28,6 +28,7 @@ import ProjectSelect from "./ProjectSelect"; | |||||
| import TaskGroupSelect from "./TaskGroupSelect"; | import TaskGroupSelect from "./TaskGroupSelect"; | ||||
| import TaskSelect from "./TaskSelect"; | import TaskSelect from "./TaskSelect"; | ||||
| import { isValidTimeEntry } from "@/app/api/timesheets/utils"; | import { isValidTimeEntry } from "@/app/api/timesheets/utils"; | ||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | |||||
| dayjs.extend(isBetween); | dayjs.extend(isBetween); | ||||
| @@ -308,6 +309,9 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| width: 100, | width: 100, | ||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| valueParser(value) { | |||||
| return value ? roundToNearestQuarter(value) : value; | |||||
| }, | |||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| return manhourFormatter.format(params.value || 0); | return manhourFormatter.format(params.value || 0); | ||||
| }, | }, | ||||
| @@ -318,6 +322,9 @@ const EntryInputTable: React.FC<Props> = ({ | |||||
| width: 150, | width: 150, | ||||
| editable: true, | editable: true, | ||||
| type: "number", | type: "number", | ||||
| valueParser(value) { | |||||
| return value ? roundToNearestQuarter(value) : value; | |||||
| }, | |||||
| valueFormatter(params) { | valueFormatter(params) { | ||||
| return manhourFormatter.format(params.value || 0); | return manhourFormatter.format(params.value || 0); | ||||
| }, | }, | ||||
| @@ -18,17 +18,21 @@ import TimesheetEditModal, { | |||||
| Props as TimesheetEditModalProps, | Props as TimesheetEditModalProps, | ||||
| } from "./TimesheetEditModal"; | } from "./TimesheetEditModal"; | ||||
| import TimeEntryCard from "./TimeEntryCard"; | import TimeEntryCard from "./TimeEntryCard"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| import { getHolidayForDate } from "@/app/utils/holidayUtils"; | |||||
| interface Props { | interface Props { | ||||
| date: string; | date: string; | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const MobileTimesheetEntry: React.FC<Props> = ({ | const MobileTimesheetEntry: React.FC<Props> = ({ | ||||
| date, | date, | ||||
| allProjects, | allProjects, | ||||
| assignedProjects, | assignedProjects, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -44,6 +48,9 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||||
| }, [allProjects]); | }, [allProjects]); | ||||
| const dayJsObj = dayjs(date); | const dayJsObj = dayjs(date); | ||||
| const holiday = getHolidayForDate(date, companyHolidays); | |||||
| const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; | |||||
| const { watch, setValue } = useFormContext<RecordTimesheetInput>(); | const { watch, setValue } = useFormContext<RecordTimesheetInput>(); | ||||
| const currentEntries = watch(date); | const currentEntries = watch(date); | ||||
| @@ -99,9 +106,15 @@ const MobileTimesheetEntry: React.FC<Props> = ({ | |||||
| <Typography | <Typography | ||||
| paddingInline={2} | paddingInline={2} | ||||
| variant="overline" | variant="overline" | ||||
| color={dayJsObj.day() === 0 ? "error.main" : undefined} | |||||
| color={isHoliday ? "error.main" : undefined} | |||||
| > | > | ||||
| {shortDateFormatter(language).format(dayJsObj.toDate())} | {shortDateFormatter(language).format(dayJsObj.toDate())} | ||||
| {holiday && ( | |||||
| <Typography | |||||
| marginInlineStart={1} | |||||
| variant="caption" | |||||
| >{`(${holiday.title})`}</Typography> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| <Box | <Box | ||||
| paddingInline={2} | paddingInline={2} | ||||
| @@ -7,17 +7,20 @@ import { useFormContext } from "react-hook-form"; | |||||
| import DateHoursList from "../DateHoursTable/DateHoursList"; | import DateHoursList from "../DateHoursTable/DateHoursList"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| import MobileTimesheetEntry from "./MobileTimesheetEntry"; | import MobileTimesheetEntry from "./MobileTimesheetEntry"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| leaveRecords: RecordLeaveInput; | leaveRecords: RecordLeaveInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const MobileTimesheetTable: React.FC<Props> = ({ | const MobileTimesheetTable: React.FC<Props> = ({ | ||||
| allProjects, | allProjects, | ||||
| assignedProjects, | assignedProjects, | ||||
| leaveRecords, | leaveRecords, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { watch } = useFormContext<RecordTimesheetInput>(); | const { watch } = useFormContext<RecordTimesheetInput>(); | ||||
| const currentInput = watch(); | const currentInput = watch(); | ||||
| @@ -26,10 +29,11 @@ const MobileTimesheetTable: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <DateHoursList | <DateHoursList | ||||
| days={days} | days={days} | ||||
| companyHolidays={companyHolidays} | |||||
| leaveEntries={leaveRecords} | leaveEntries={leaveRecords} | ||||
| timesheetEntries={currentInput} | timesheetEntries={currentInput} | ||||
| EntryComponent={MobileTimesheetEntry} | EntryComponent={MobileTimesheetEntry} | ||||
| entryComponentProps={{ allProjects, assignedProjects }} | |||||
| entryComponentProps={{ allProjects, assignedProjects, companyHolidays }} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -20,6 +20,7 @@ import TaskGroupSelect from "./TaskGroupSelect"; | |||||
| import TaskSelect from "./TaskSelect"; | import TaskSelect from "./TaskSelect"; | ||||
| import { TaskGroup } from "@/app/api/tasks"; | import { TaskGroup } from "@/app/api/tasks"; | ||||
| import uniqBy from "lodash/uniqBy"; | import uniqBy from "lodash/uniqBy"; | ||||
| import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; | |||||
| export interface Props extends Omit<ModalProps, "children"> { | export interface Props extends Omit<ModalProps, "children"> { | ||||
| onSave: (leaveEntry: TimeEntry) => void; | onSave: (leaveEntry: TimeEntry) => void; | ||||
| @@ -196,8 +197,9 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| label={t("Hours")} | label={t("Hours")} | ||||
| fullWidth | fullWidth | ||||
| {...register("inputHours", { | {...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)} | error={Boolean(formState.errors.inputHours)} | ||||
| /> | /> | ||||
| @@ -206,7 +208,8 @@ const TimesheetEditModal: React.FC<Props> = ({ | |||||
| label={t("Other Hours")} | label={t("Other Hours")} | ||||
| fullWidth | fullWidth | ||||
| {...register("otHours", { | {...register("otHours", { | ||||
| valueAsNumber: true, | |||||
| setValueAs: (value) => roundToNearestQuarter(parseFloat(value)), | |||||
| validate: (value) => (value ? value > 0 : true), | |||||
| })} | })} | ||||
| error={Boolean(formState.errors.otHours)} | error={Boolean(formState.errors.otHours)} | ||||
| /> | /> | ||||
| @@ -7,17 +7,20 @@ import { useFormContext } from "react-hook-form"; | |||||
| import EntryInputTable from "./EntryInputTable"; | import EntryInputTable from "./EntryInputTable"; | ||||
| import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; | ||||
| import DateHoursTable from "../DateHoursTable"; | import DateHoursTable from "../DateHoursTable"; | ||||
| import { HolidaysResult } from "@/app/api/holidays"; | |||||
| interface Props { | interface Props { | ||||
| allProjects: ProjectWithTasks[]; | allProjects: ProjectWithTasks[]; | ||||
| assignedProjects: AssignedProject[]; | assignedProjects: AssignedProject[]; | ||||
| leaveRecords: RecordLeaveInput; | leaveRecords: RecordLeaveInput; | ||||
| companyHolidays: HolidaysResult[]; | |||||
| } | } | ||||
| const TimesheetTable: React.FC<Props> = ({ | const TimesheetTable: React.FC<Props> = ({ | ||||
| allProjects, | allProjects, | ||||
| assignedProjects, | assignedProjects, | ||||
| leaveRecords, | leaveRecords, | ||||
| companyHolidays, | |||||
| }) => { | }) => { | ||||
| const { watch } = useFormContext<RecordTimesheetInput>(); | const { watch } = useFormContext<RecordTimesheetInput>(); | ||||
| const currentInput = watch(); | const currentInput = watch(); | ||||
| @@ -25,6 +28,7 @@ const TimesheetTable: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <DateHoursTable | <DateHoursTable | ||||
| companyHolidays={companyHolidays} | |||||
| days={days} | days={days} | ||||
| leaveEntries={leaveRecords} | leaveEntries={leaveRecords} | ||||
| timesheetEntries={currentInput} | timesheetEntries={currentInput} | ||||
| @@ -37,7 +37,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| username, | username, | ||||
| defaultLeaveRecords, | defaultLeaveRecords, | ||||
| defaultTimesheets, | defaultTimesheets, | ||||
| holidays | |||||
| holidays, | |||||
| }) => { | }) => { | ||||
| const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); | ||||
| const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); | ||||
| @@ -106,6 +106,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| /> | /> | ||||
| <TimesheetModal | <TimesheetModal | ||||
| companyHolidays={holidays} | |||||
| isOpen={isTimeheetModalVisible} | isOpen={isTimeheetModalVisible} | ||||
| onClose={handleCloseTimesheetModal} | onClose={handleCloseTimesheetModal} | ||||
| allProjects={allProjects} | allProjects={allProjects} | ||||
| @@ -115,6 +116,7 @@ const UserWorkspacePage: React.FC<Props> = ({ | |||||
| leaveRecords={defaultLeaveRecords} | leaveRecords={defaultLeaveRecords} | ||||
| /> | /> | ||||
| <LeaveModal | <LeaveModal | ||||
| companyHolidays={holidays} | |||||
| leaveTypes={leaveTypes} | leaveTypes={leaveTypes} | ||||
| isOpen={isLeaveModalVisible} | isOpen={isLeaveModalVisible} | ||||
| onClose={handleCloseLeaveModal} | onClose={handleCloseLeaveModal} | ||||