@@ -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} | ||||