소스 검색

Past entry monthly summary and making sure entries are saved before submitting in timesheet input

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 년 전
부모
커밋
7439bbad58
6개의 변경된 파일288개의 추가작업 그리고 52개의 파일을 삭제
  1. +196
    -0
      src/components/PastEntryCalendar/MonthlySummary.tsx
  2. +3
    -0
      src/components/PastEntryCalendar/PastEntryCalendar.tsx
  3. +56
    -38
      src/components/PastEntryCalendar/PastEntryCalendarModal.tsx
  4. +24
    -11
      src/components/PastEntryCalendar/PastEntryList.tsx
  5. +8
    -3
      src/components/TimeLeaveModal/TimeLeaveInputTable.tsx
  6. +1
    -0
      src/components/UserWorkspacePage/UserWorkspacePage.tsx

+ 196
- 0
src/components/PastEntryCalendar/MonthlySummary.tsx 파일 보기

@@ -0,0 +1,196 @@
import {
RecordLeaveInput,
RecordTimesheetInput,
} from "@/app/api/timesheets/actions";
import {
Box,
Card,
CardActionArea,
CardContent,
Stack,
Typography,
} from "@mui/material";
import union from "lodash/union";
import { useCallback, useMemo } from "react";
import dayjs, { Dayjs } from "dayjs";
import { getHolidayForDate } from "@/app/utils/holidayUtils";
import { HolidaysResult } from "@/app/api/holidays";
import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil";
import { useTranslation } from "react-i18next";
import pickBy from "lodash/pickBy";

interface Props {
currentMonth: Dayjs;
timesheet: RecordTimesheetInput;
leaves: RecordLeaveInput;
companyHolidays: HolidaysResult[];
onDateSelect: (date: string) => void;
}

const MonthlySummary: React.FC<Props> = ({
timesheet,
leaves,
currentMonth,
companyHolidays,
onDateSelect,
}) => {
const {
t,
i18n: { language },
} = useTranslation("home");

const timesheetForCurrentMonth = useMemo(() => {
return pickBy(timesheet, (_, date) => {
return currentMonth.isSame(dayjs(date), "month");
});
}, [currentMonth, timesheet]);

const leavesForCurrentMonth = useMemo(() => {
return pickBy(leaves, (_, date) => {
return currentMonth.isSame(dayjs(date), "month");
});
}, [currentMonth, leaves]);

const days = useMemo(() => {
return union(
Object.keys(timesheetForCurrentMonth),
Object.keys(leavesForCurrentMonth),
);
}, [timesheetForCurrentMonth, leavesForCurrentMonth]).sort();

const makeSelectDate = useCallback(
(date: string) => () => {
onDateSelect(date);
},
[onDateSelect],
);

return (
<Stack
gap={2}
marginBlockEnd={2}
minWidth={{ sm: 375 }}
maxHeight={{ sm: 500 }}
>
<Typography variant="overline">{t("Monthly Summary")}</Typography>
<Box sx={{ overflowY: "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 ls = leavesForCurrentMonth[day];
const leaveHours =
ls?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;

const ts = timesheetForCurrentMonth[day];
const timesheetNormalHours =
ts?.reduce((acc, entry) => acc + (entry.inputHours || 0), 0) || 0;
const timesheetOtHours =
ts?.reduce((acc, entry) => acc + (entry.otHours || 0), 0) || 0;

const timesheetHours = timesheetNormalHours + timesheetOtHours;

return (
<Card
key={`${day}-${index}`}
sx={{ marginBlockEnd: 2, marginInline: 2 }}
>
<CardActionArea onClick={makeSelectDate(day)}>
<CardContent sx={{ padding: 3 }}>
<Typography
variant="overline"
component="div"
sx={{
color: isHoliday ? "error.main" : undefined,
}}
>
{shortDateFormatter(language).format(dayJsObj.toDate())}
{holiday && (
<Typography
marginInlineStart={1}
variant="caption"
>{`(${holiday.title})`}</Typography>
)}
</Typography>
<Stack spacing={1}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
alignItems: "baseline",
}}
>
<Typography variant="body2">
{t("Timesheet Hours")}
</Typography>
<Typography>
{manhourFormatter.format(timesheetHours)}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
alignItems: "baseline",
}}
>
<Typography variant="body2">
{t("Leave Hours")}
</Typography>
<Typography>
{manhourFormatter.format(leaveHours)}
</Typography>
</Box>

<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
alignItems: "baseline",
}}
>
<Typography variant="body2">
{t("Daily Total Hours")}
</Typography>
<Typography>
{manhourFormatter.format(timesheetHours + leaveHours)}
</Typography>
</Box>
</Stack>
</CardContent>
</CardActionArea>
</Card>
);
})}
</Box>
<Typography variant="overline">
{`${t("Total Monthly Work Hours")}: ${manhourFormatter.format(
Object.values(timesheetForCurrentMonth)
.flatMap((entries) => entries)
.map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0))
.reduce((acc, cur) => {
return acc + cur;
}, 0),
)}`}
</Typography>
<Typography variant="overline">
{`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format(
Object.values(leavesForCurrentMonth)
.flatMap((entries) => entries)
.map((entry) => entry.inputHours)
.reduce((acc, cur) => {
return acc + cur;
}, 0),
)}`}
</Typography>
</Stack>
);
};

export default MonthlySummary;

+ 3
- 0
src/components/PastEntryCalendar/PastEntryCalendar.tsx 파일 보기

@@ -26,6 +26,7 @@ export interface Props {
timesheet: RecordTimesheetInput;
leaves: RecordLeaveInput;
onDateSelect: (date: string) => void;
onMonthChange: (day: Dayjs) => void;
}

const getColor = (
@@ -72,6 +73,7 @@ const PastEntryCalendar: React.FC<Props> = ({
timesheet,
leaves,
onDateSelect,
onMonthChange,
}) => {
const {
i18n: { language },
@@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC<Props> = ({
>
<DateCalendar
onChange={onChange}
onMonthChange={onMonthChange}
disableFuture
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slots={{ day: EntryDay as any }}


+ 56
- 38
src/components/PastEntryCalendar/PastEntryCalendarModal.tsx 파일 보기

@@ -20,12 +20,17 @@ import { ProjectWithTasks } from "@/app/api/projects";
import { LeaveType } from "@/app/api/timesheets";
import useIsMobile from "@/app/utils/useIsMobile";
import FullscreenModal from "../FullscreenModal";
import MonthlySummary from "./MonthlySummary";
import { HolidaysResult } from "@/app/api/holidays";
import dayjs from "dayjs";

interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> {
interface Props
extends Omit<PastEntryCalendarProps, "onDateSelect" | "onMonthChange"> {
open: boolean;
handleClose: () => void;
leaveTypes: LeaveType[];
allProjects: ProjectWithTasks[];
companyHolidays: HolidaysResult[];
}

const Indicator = styled(Box)(() => ({
@@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({
const { t } = useTranslation("home");

const [selectedDate, setSelectedDate] = useState("");
const [currentMonth, setMonthChange] = useState(dayjs());

const clearDate = useCallback(() => {
setSelectedDate("");
@@ -54,40 +60,52 @@ const PastEntryCalendarModal: React.FC<Props> = ({
handleClose();
}, [handleClose]);

const content = selectedDate ? (
<>
<PastEntryList
date={selectedDate}
timesheet={timesheet}
leaves={leaves}
allProjects={allProjects}
leaveTypes={leaveTypes}
/>
</>
) : (
<>
<Stack marginBlockEnd={2}>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "info.light" }} />
<Typography variant="caption">{t("Has timesheet entry")}</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "warning.light" }} />
<Typography variant="caption">{t("Has leave entry")}</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "success.light" }} />
<Typography variant="caption">
{t("Has both timesheet and leave entry")}
</Typography>
</Box>
</Stack>
<PastEntryCalendar
timesheet={timesheet}
leaves={leaves}
onDateSelect={setSelectedDate}
/>
</>
const content = (
<Box sx={{ display: "flex", flexDirection: { xs: "column", sm: "row" } }}>
<Box>
<Stack marginBlockEnd={2}>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "info.light" }} />
<Typography variant="caption">
{t("Has timesheet entry")}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "warning.light" }} />
<Typography variant="caption">{t("Has leave entry")}</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "success.light" }} />
<Typography variant="caption">
{t("Has both timesheet and leave entry")}
</Typography>
</Box>
</Stack>
<PastEntryCalendar
timesheet={timesheet}
leaves={leaves}
onDateSelect={setSelectedDate}
onMonthChange={setMonthChange}
/>
</Box>
{selectedDate ? (
<PastEntryList
date={selectedDate}
timesheet={timesheet}
leaves={leaves}
allProjects={allProjects}
leaveTypes={leaveTypes}
/>
) : (
<MonthlySummary
currentMonth={currentMonth}
timesheet={timesheet}
leaves={leaves}
companyHolidays={[]}
onDateSelect={setSelectedDate}
/>
)}
</Box>
);

const isMobile = useIsMobile();
@@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC<Props> = ({
startIcon={<ArrowBack />}
onClick={clearDate}
>
{t("Back")}
{t("Back to Monthly Summary")}
</Button>
)}
</Box>
</Box>
</FullscreenModal>
) : (
<Dialog onClose={onClose} open={open}>
<Dialog onClose={onClose} open={open} maxWidth="md">
<DialogTitle>{t("Past Entries")}</DialogTitle>
<DialogContent>{content}</DialogContent>
{selectedDate && (
@@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC<Props> = ({
startIcon={<ArrowBack />}
onClick={clearDate}
>
{t("Back")}
{t("Back to Monthly Summary")}
</Button>
</DialogActions>
)}


+ 24
- 11
src/components/PastEntryCalendar/PastEntryList.tsx 파일 보기

@@ -57,7 +57,12 @@ const PastEntryList: React.FC<Props> = ({
const dayJsObj = dayjs(date);

return (
<Stack gap={2} marginBlockEnd={2} minWidth={{ sm: 375 }}>
<Stack
gap={2}
marginBlockEnd={2}
minWidth={{ sm: 375 }}
maxHeight={{ sm: 500 }}
>
<Typography
variant="overline"
color={dayJsObj.day() === 0 ? "error.main" : undefined}
@@ -94,17 +99,25 @@ const PastEntryList: React.FC<Props> = ({
leaveTypeMap={leaveTypeMap}
/>
))}
<Typography
variant="overline"
>
{t("Total Work Hours")}: {manhourFormatter.format(timeEntries.map(entry => (entry.inputHours ?? 0) + (entry.otHours ?? 0)).reduce((acc, cur) => { return acc + cur }, 0))}
</Typography>
<Typography
variant="overline"
>
{t("Total Leave Hours")}: {manhourFormatter.format(leaveEntries.map(entry => entry.inputHours).reduce((acc, cur) => { return acc + cur }, 0))}
</Typography>
</Box>
<Typography variant="overline">
{`${t("Total Work Hours")}: ${manhourFormatter.format(
timeEntries
.map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0))
.reduce((acc, cur) => {
return acc + cur;
}, 0),
)}`}
</Typography>
<Typography variant="overline">
{`${t("Total Leave Hours")}: ${manhourFormatter.format(
leaveEntries
.map((entry) => entry.inputHours)
.reduce((acc, cur) => {
return acc + cur;
}, 0),
)}`}
</Typography>
</Stack>
);
};


+ 8
- 3
src/components/TimeLeaveModal/TimeLeaveInputTable.tsx 파일 보기

@@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC<Props> = ({
}, {});
}, [assignedProjects]);

const { getValues, setValue, clearErrors } =
const { getValues, setValue, clearErrors, setError } =
useFormContext<RecordTimeLeaveInput>();
const currentEntries = getValues(day);

@@ -486,8 +486,13 @@ const TimeLeaveInputTable: React.FC<Props> = ({
.filter((e): e is TimeLeaveEntry => Boolean(e));

setValue(day, newEntries);
clearErrors(day);
}, [getValues, entries, setValue, day, clearErrors]);

if (entries.some((e) => e._isNew)) {
setError(day, { message: "There are some unsaved entries." });
} else {
clearErrors(day);
}
}, [getValues, entries, setValue, day, clearErrors, setError]);

const hasOutOfPlannedStages = entries.some(
(entry) => entry._isPlanned !== undefined && !entry._isPlanned,


+ 1
- 0
src/components/UserWorkspacePage/UserWorkspacePage.tsx 파일 보기

@@ -154,6 +154,7 @@ const UserWorkspacePage: React.FC<Props> = ({
leaves={defaultLeaveRecords}
allProjects={allProjects}
leaveTypes={leaveTypes}
companyHolidays={holidays}
/>
<TimeLeaveModal
fastEntryEnabled={fastEntryEnabled}


불러오는 중...
취소
저장