Sfoglia il codice sorgente

Highlight holidays and round manhours

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 anno fa
parent
commit
4ce14145bd
17 ha cambiato i file con 148 aggiunte e 25 eliminazioni
  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 Vedi File

@@ -1,4 +1,10 @@
import Holidays from "date-holidays";
import { HolidaysResult } from "../api/holidays";
import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport";
import { INPUT_DATE_FORMAT } from "./formatUtil";

dayjs.extend(arraySupport);

const hd = new Holidays("HK");

@@ -47,3 +53,20 @@ export const getPublicHolidaysForNYears = (years: number = 1) => {
});
});
};

export const getHolidayForDate = (
date: string,
companyHolidays: HolidaysResult[] = [],
) => {
const currentYearHolidays: { date: string; title: string }[] = companyHolidays
.map((h) => ({
title: h.name,
// Dayjs use 0-index for months, but not our API
date: dayjs([h.date[0], h.date[1] - 1, h.date[2]]).format(
INPUT_DATE_FORMAT,
),
}))
.concat(getPublicHolidaysForNYears(1).concat());

return currentYearHolidays.find((h) => h.date === date);
};

+ 3
- 0
src/app/utils/manhourUtils.ts Vedi 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 Vedi File

@@ -20,9 +20,12 @@ import {
LEAVE_DAILY_MAX_HOURS,
TIMESHEET_DAILY_MAX_HOURS,
} from "@/app/api/timesheets/utils";
import { HolidaysResult } from "@/app/api/holidays";
import { getHolidayForDate } from "@/app/utils/holidayUtils";

interface Props<EntryComponentProps = object> {
days: string[];
companyHolidays: HolidaysResult[];
leaveEntries: RecordLeaveInput;
timesheetEntries: RecordTimesheetInput;
EntryComponent: React.FunctionComponent<
@@ -37,6 +40,7 @@ function DateHoursList<EntryTableProps>({
timesheetEntries,
EntryComponent,
entryComponentProps,
companyHolidays,
}: Props<EntryTableProps>) {
const {
t,
@@ -69,6 +73,11 @@ function DateHoursList<EntryTableProps>({
<Box overflow="scroll" flex={1}>
{days.map((day, index) => {
const dayJsObj = dayjs(day);

const holiday = getHolidayForDate(day, companyHolidays);
const isHoliday =
holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const leaves = leaveEntries[day];
const leaveHours =
leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
@@ -97,10 +106,16 @@ function DateHoursList<EntryTableProps>({
variant="overline"
component="div"
sx={{
color: dayJsObj.day() === 0 ? "error.main" : undefined,
color: isHoliday ? "error.main" : undefined,
}}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
{holiday && (
<Typography
marginInlineStart={1}
variant="caption"
>{`(${holiday.title})`}</Typography>
)}
</Typography>
<Stack spacing={1}>
<Box


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

@@ -15,6 +15,7 @@ import {
TableHead,
TableRow,
Tooltip,
Typography,
} from "@mui/material";
import dayjs from "dayjs";
import React, { useState } from "react";
@@ -23,11 +24,14 @@ import {
LEAVE_DAILY_MAX_HOURS,
TIMESHEET_DAILY_MAX_HOURS,
} from "@/app/api/timesheets/utils";
import { HolidaysResult } from "@/app/api/holidays";
import { getHolidayForDate } from "@/app/utils/holidayUtils";

interface Props<EntryTableProps = object> {
days: string[];
leaveEntries: RecordLeaveInput;
timesheetEntries: RecordTimesheetInput;
companyHolidays: HolidaysResult[];
EntryTableComponent: React.FunctionComponent<
EntryTableProps & { day: string }
>;
@@ -40,6 +44,7 @@ function DateHoursTable<EntryTableProps>({
entryTableProps,
leaveEntries,
timesheetEntries,
companyHolidays,
}: Props<EntryTableProps>) {
const { t } = useTranslation("home");

@@ -61,6 +66,7 @@ function DateHoursTable<EntryTableProps>({
<DayRow
key={`${day}${index}`}
day={day}
companyHolidays={companyHolidays}
leaveEntries={leaveEntries}
timesheetEntries={timesheetEntries}
EntryTableComponent={EntryTableComponent}
@@ -80,8 +86,10 @@ function DayRow<EntryTableProps>({
timesheetEntries,
entryTableProps,
EntryTableComponent,
companyHolidays
}: {
day: string;
companyHolidays: HolidaysResult[];
leaveEntries: RecordLeaveInput;
timesheetEntries: RecordTimesheetInput;
EntryTableComponent: React.FunctionComponent<
@@ -96,6 +104,9 @@ function DayRow<EntryTableProps>({
const dayJsObj = dayjs(day);
const [open, setOpen] = useState(false);

const holiday = getHolidayForDate(day, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const leaves = leaveEntries[day];
const leaveHours =
leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
@@ -125,10 +136,14 @@ function DayRow<EntryTableProps>({
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton>
</TableCell>
<TableCell
sx={{ color: dayJsObj.day() === 0 ? "error.main" : undefined }}
>
<TableCell sx={{ color: isHoliday ? "error.main" : undefined }}>
{shortDateFormatter(language).format(dayJsObj.toDate())}
{holiday && (
<Typography
display="block"
variant="caption"
>{`(${holiday.title})`}</Typography>
)}
</TableCell>
{/* Timesheet */}
<TableCell>{manhourFormatter.format(timesheetHours)}</TableCell>


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

@@ -25,6 +25,7 @@ import { LeaveType } from "@/app/api/timesheets";
import FullscreenModal from "../FullscreenModal";
import MobileLeaveTable from "../LeaveTable/MobileLeaveTable";
import useIsMobile from "@/app/utils/useIsMobile";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
isOpen: boolean;
@@ -33,6 +34,7 @@ interface Props {
defaultLeaveRecords?: RecordLeaveInput;
leaveTypes: LeaveType[];
timesheetRecords: RecordTimesheetInput;
companyHolidays: HolidaysResult[];
}

const modalSx: SxProps = {
@@ -52,6 +54,7 @@ const LeaveModal: React.FC<Props> = ({
defaultLeaveRecords,
timesheetRecords,
leaveTypes,
companyHolidays,
}) => {
const { t } = useTranslation("home");

@@ -127,6 +130,7 @@ const LeaveModal: React.FC<Props> = ({
}}
>
<LeaveTable
companyHolidays={companyHolidays}
leaveTypes={leaveTypes}
timesheetRecords={timesheetRecords}
/>
@@ -165,6 +169,7 @@ const LeaveModal: React.FC<Props> = ({
{t("Record Leave")}
</Typography>
<MobileLeaveTable
companyHolidays={companyHolidays}
leaveTypes={leaveTypes}
timesheetRecords={timesheetRecords}
/>


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

@@ -1,5 +1,6 @@
import { LeaveType } from "@/app/api/timesheets";
import { LeaveEntry } from "@/app/api/timesheets/actions";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
import { Check, Delete } from "@mui/icons-material";
import {
Box,
@@ -98,7 +99,7 @@ const LeaveEditModal: React.FC<Props> = ({
label={t("Hours")}
fullWidth
{...register("inputHours", {
valueAsNumber: true,
setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
validate: (value) => value > 0,
})}
error={Boolean(formState.errors.inputHours)}


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

@@ -22,6 +22,7 @@ import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import { LeaveType } from "@/app/api/timesheets";
import { isValidLeaveEntry } from "@/app/api/timesheets/utils";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";

dayjs.extend(isBetween);

@@ -173,6 +174,9 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => {
width: 150,
editable: true,
type: "number",
valueParser(value) {
return value ? roundToNearestQuarter(value) : value;
},
valueFormatter(params) {
return manhourFormatter.format(params.value);
},


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

@@ -7,19 +7,26 @@ import { useFormContext } from "react-hook-form";
import LeaveEntryTable from "./LeaveEntryTable";
import { LeaveType } from "@/app/api/timesheets";
import DateHoursTable from "../DateHoursTable";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
leaveTypes: LeaveType[];
timesheetRecords: RecordTimesheetInput;
companyHolidays: HolidaysResult[];
}

const LeaveTable: React.FC<Props> = ({ leaveTypes, timesheetRecords }) => {
const LeaveTable: React.FC<Props> = ({
leaveTypes,
timesheetRecords,
companyHolidays,
}) => {
const { watch } = useFormContext<RecordLeaveInput>();
const currentInput = watch();
const days = Object.keys(currentInput);

return (
<DateHoursTable
companyHolidays={companyHolidays}
days={days}
leaveEntries={currentInput}
timesheetEntries={timesheetRecords}


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

@@ -1,33 +1,35 @@
import { LeaveType } from "@/app/api/timesheets";
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 React, { useCallback, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import LeaveEditModal, { Props as LeaveEditModalProps } from "./LeaveEditModal";
import LeaveEntryCard from "./LeaveEntryCard";
import { HolidaysResult } from "@/app/api/holidays";
import { getHolidayForDate } from "@/app/utils/holidayUtils";

interface Props {
date: string;
leaveTypes: LeaveType[];
companyHolidays: HolidaysResult[];
}

const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => {
const MobileLeaveEntry: React.FC<Props> = ({
date,
leaveTypes,
companyHolidays,
}) => {
const {
t,
i18n: { language },
} = useTranslation("home");
const dayJsObj = dayjs(date);
const holiday = getHolidayForDate(date, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const leaveTypeMap = useMemo<{ [id: LeaveType["id"]]: LeaveType }>(() => {
return leaveTypes.reduce(
@@ -91,9 +93,15 @@ const MobileLeaveEntry: React.FC<Props> = ({ date, leaveTypes }) => {
<Typography
paddingInline={2}
variant="overline"
color={dayJsObj.day() === 0 ? "error.main" : undefined}
color={isHoliday ? "error.main" : undefined}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
{holiday && (
<Typography
marginInlineStart={1}
variant="caption"
>{`(${holiday.title})`}</Typography>
)}
</Typography>
<Box
paddingInline={2}


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

@@ -7,15 +7,18 @@ import { useFormContext } from "react-hook-form";
import { LeaveType } from "@/app/api/timesheets";
import MobileLeaveEntry from "./MobileLeaveEntry";
import DateHoursList from "../DateHoursTable/DateHoursList";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
leaveTypes: LeaveType[];
timesheetRecords: RecordTimesheetInput;
companyHolidays: HolidaysResult[];
}

const MobileLeaveTable: React.FC<Props> = ({
timesheetRecords,
leaveTypes,
companyHolidays,
}) => {
const { watch } = useFormContext<RecordLeaveInput>();
const currentInput = watch();
@@ -24,10 +27,11 @@ const MobileLeaveTable: React.FC<Props> = ({
return (
<DateHoursList
days={days}
companyHolidays={companyHolidays}
leaveEntries={currentInput}
timesheetEntries={timesheetRecords}
EntryComponent={MobileLeaveEntry}
entryComponentProps={{ leaveTypes }}
entryComponentProps={{ leaveTypes, companyHolidays }}
/>
);
};


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

@@ -25,6 +25,7 @@ import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import FullscreenModal from "../FullscreenModal";
import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable";
import useIsMobile from "@/app/utils/useIsMobile";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
isOpen: boolean;
@@ -34,6 +35,7 @@ interface Props {
username: string;
defaultTimesheets?: RecordTimesheetInput;
leaveRecords: RecordLeaveInput;
companyHolidays: HolidaysResult[];
}

const modalSx: SxProps = {
@@ -54,6 +56,7 @@ const TimesheetModal: React.FC<Props> = ({
username,
defaultTimesheets,
leaveRecords,
companyHolidays,
}) => {
const { t } = useTranslation("home");

@@ -129,6 +132,7 @@ const TimesheetModal: React.FC<Props> = ({
}}
>
<TimesheetTable
companyHolidays={companyHolidays}
assignedProjects={assignedProjects}
allProjects={allProjects}
leaveRecords={leaveRecords}
@@ -168,6 +172,7 @@ const TimesheetModal: React.FC<Props> = ({
{t("Timesheet Input")}
</Typography>
<MobileTimesheetTable
companyHolidays={companyHolidays}
assignedProjects={assignedProjects}
allProjects={allProjects}
leaveRecords={leaveRecords}


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

@@ -28,6 +28,7 @@ import ProjectSelect from "./ProjectSelect";
import TaskGroupSelect from "./TaskGroupSelect";
import TaskSelect from "./TaskSelect";
import { isValidTimeEntry } from "@/app/api/timesheets/utils";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";

dayjs.extend(isBetween);

@@ -308,6 +309,9 @@ const EntryInputTable: React.FC<Props> = ({
width: 100,
editable: true,
type: "number",
valueParser(value) {
return value ? roundToNearestQuarter(value) : value;
},
valueFormatter(params) {
return manhourFormatter.format(params.value || 0);
},
@@ -318,6 +322,9 @@ const EntryInputTable: React.FC<Props> = ({
width: 150,
editable: true,
type: "number",
valueParser(value) {
return value ? roundToNearestQuarter(value) : value;
},
valueFormatter(params) {
return manhourFormatter.format(params.value || 0);
},


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

@@ -18,17 +18,21 @@ import TimesheetEditModal, {
Props as TimesheetEditModalProps,
} from "./TimesheetEditModal";
import TimeEntryCard from "./TimeEntryCard";
import { HolidaysResult } from "@/app/api/holidays";
import { getHolidayForDate } from "@/app/utils/holidayUtils";

interface Props {
date: string;
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
companyHolidays: HolidaysResult[];
}

const MobileTimesheetEntry: React.FC<Props> = ({
date,
allProjects,
assignedProjects,
companyHolidays,
}) => {
const {
t,
@@ -44,6 +48,9 @@ const MobileTimesheetEntry: React.FC<Props> = ({
}, [allProjects]);

const dayJsObj = dayjs(date);
const holiday = getHolidayForDate(date, companyHolidays);
const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;

const { watch, setValue } = useFormContext<RecordTimesheetInput>();
const currentEntries = watch(date);

@@ -99,9 +106,15 @@ const MobileTimesheetEntry: React.FC<Props> = ({
<Typography
paddingInline={2}
variant="overline"
color={dayJsObj.day() === 0 ? "error.main" : undefined}
color={isHoliday ? "error.main" : undefined}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
{holiday && (
<Typography
marginInlineStart={1}
variant="caption"
>{`(${holiday.title})`}</Typography>
)}
</Typography>
<Box
paddingInline={2}


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

@@ -7,17 +7,20 @@ import { useFormContext } from "react-hook-form";
import DateHoursList from "../DateHoursTable/DateHoursList";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import MobileTimesheetEntry from "./MobileTimesheetEntry";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
leaveRecords: RecordLeaveInput;
companyHolidays: HolidaysResult[];
}

const MobileTimesheetTable: React.FC<Props> = ({
allProjects,
assignedProjects,
leaveRecords,
companyHolidays,
}) => {
const { watch } = useFormContext<RecordTimesheetInput>();
const currentInput = watch();
@@ -26,10 +29,11 @@ const MobileTimesheetTable: React.FC<Props> = ({
return (
<DateHoursList
days={days}
companyHolidays={companyHolidays}
leaveEntries={leaveRecords}
timesheetEntries={currentInput}
EntryComponent={MobileTimesheetEntry}
entryComponentProps={{ allProjects, assignedProjects }}
entryComponentProps={{ allProjects, assignedProjects, companyHolidays }}
/>
);
};


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

@@ -20,6 +20,7 @@ import TaskGroupSelect from "./TaskGroupSelect";
import TaskSelect from "./TaskSelect";
import { TaskGroup } from "@/app/api/tasks";
import uniqBy from "lodash/uniqBy";
import { roundToNearestQuarter } from "@/app/utils/manhourUtils";

export interface Props extends Omit<ModalProps, "children"> {
onSave: (leaveEntry: TimeEntry) => void;
@@ -196,8 +197,9 @@ const TimesheetEditModal: React.FC<Props> = ({
label={t("Hours")}
fullWidth
{...register("inputHours", {
valueAsNumber: true,
validate: (value) => Boolean(value || otHours),
setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
validate: (value) =>
value ? value > 0 : Boolean(value || otHours),
})}
error={Boolean(formState.errors.inputHours)}
/>
@@ -206,7 +208,8 @@ const TimesheetEditModal: React.FC<Props> = ({
label={t("Other Hours")}
fullWidth
{...register("otHours", {
valueAsNumber: true,
setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
validate: (value) => (value ? value > 0 : true),
})}
error={Boolean(formState.errors.otHours)}
/>


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

@@ -7,17 +7,20 @@ import { useFormContext } from "react-hook-form";
import EntryInputTable from "./EntryInputTable";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import DateHoursTable from "../DateHoursTable";
import { HolidaysResult } from "@/app/api/holidays";

interface Props {
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
leaveRecords: RecordLeaveInput;
companyHolidays: HolidaysResult[];
}

const TimesheetTable: React.FC<Props> = ({
allProjects,
assignedProjects,
leaveRecords,
companyHolidays,
}) => {
const { watch } = useFormContext<RecordTimesheetInput>();
const currentInput = watch();
@@ -25,6 +28,7 @@ const TimesheetTable: React.FC<Props> = ({

return (
<DateHoursTable
companyHolidays={companyHolidays}
days={days}
leaveEntries={leaveRecords}
timesheetEntries={currentInput}


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

@@ -37,7 +37,7 @@ const UserWorkspacePage: React.FC<Props> = ({
username,
defaultLeaveRecords,
defaultTimesheets,
holidays
holidays,
}) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false);
@@ -106,6 +106,7 @@ const UserWorkspacePage: React.FC<Props> = ({
leaveTypes={leaveTypes}
/>
<TimesheetModal
companyHolidays={holidays}
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
allProjects={allProjects}
@@ -115,6 +116,7 @@ const UserWorkspacePage: React.FC<Props> = ({
leaveRecords={defaultLeaveRecords}
/>
<LeaveModal
companyHolidays={holidays}
leaveTypes={leaveTypes}
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}


Caricamento…
Annulla
Salva