Browse Source

Highlight holidays and round manhours

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 year ago
parent
commit
4ce14145bd
17 changed files with 148 additions and 25 deletions
  1. +23
    -0
      src/app/utils/holidayUtils.ts
  2. +3
    -0
      src/app/utils/manhourUtils.ts
  3. +16
    -1
      src/components/DateHoursTable/DateHoursList.tsx
  4. +18
    -3
      src/components/DateHoursTable/DateHoursTable.tsx
  5. +5
    -0
      src/components/LeaveModal/LeaveModal.tsx
  6. +2
    -1
      src/components/LeaveTable/LeaveEditModal.tsx
  7. +4
    -0
      src/components/LeaveTable/LeaveEntryTable.tsx
  8. +8
    -1
      src/components/LeaveTable/LeaveTable.tsx
  9. +20
    -12
      src/components/LeaveTable/MobileLeaveEntry.tsx
  10. +5
    -1
      src/components/LeaveTable/MobileLeaveTable.tsx
  11. +5
    -0
      src/components/TimesheetModal/TimesheetModal.tsx
  12. +7
    -0
      src/components/TimesheetTable/EntryInputTable.tsx
  13. +14
    -1
      src/components/TimesheetTable/MobileTimesheetEntry.tsx
  14. +5
    -1
      src/components/TimesheetTable/MobileTimesheetTable.tsx
  15. +6
    -3
      src/components/TimesheetTable/TimesheetEditModal.tsx
  16. +4
    -0
      src/components/TimesheetTable/TimesheetTable.tsx
  17. +3
    -1
      src/components/UserWorkspacePage/UserWorkspacePage.tsx

+ 23
- 0
src/app/utils/holidayUtils.ts View File

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

+ 3
- 0
src/app/utils/manhourUtils.ts View File

@@ -0,0 +1,3 @@
export const roundToNearestQuarter = (n: number): number => {
return Math.round(n / 0.25) * 0.25;
};

+ 16
- 1
src/components/DateHoursTable/DateHoursList.tsx View File

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


+ 18
- 3
src/components/DateHoursTable/DateHoursTable.tsx View File

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


+ 5
- 0
src/components/LeaveModal/LeaveModal.tsx View File

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


+ 2
- 1
src/components/LeaveTable/LeaveEditModal.tsx View File

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


+ 4
- 0
src/components/LeaveTable/LeaveEntryTable.tsx View File

@@ -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);
}, },


+ 8
- 1
src/components/LeaveTable/LeaveTable.tsx View File

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


+ 20
- 12
src/components/LeaveTable/MobileLeaveEntry.tsx View File

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


+ 5
- 1
src/components/LeaveTable/MobileLeaveTable.tsx View File

@@ -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 }}
/> />
); );
}; };


+ 5
- 0
src/components/TimesheetModal/TimesheetModal.tsx View File

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


+ 7
- 0
src/components/TimesheetTable/EntryInputTable.tsx View File

@@ -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);
}, },


+ 14
- 1
src/components/TimesheetTable/MobileTimesheetEntry.tsx View File

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


+ 5
- 1
src/components/TimesheetTable/MobileTimesheetTable.tsx View File

@@ -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 }}
/> />
); );
}; };


+ 6
- 3
src/components/TimesheetTable/TimesheetEditModal.tsx View File

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


+ 4
- 0
src/components/TimesheetTable/TimesheetTable.tsx View File

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


+ 3
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx View File

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


Loading…
Cancel
Save