diff --git a/src/components/PastEntryCalendar/MonthlySummary.tsx b/src/components/PastEntryCalendar/MonthlySummary.tsx new file mode 100644 index 0000000..797000a --- /dev/null +++ b/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 = ({ + 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 ( + + {t("Monthly Summary")} + + {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 ( + + + + + {shortDateFormatter(language).format(dayJsObj.toDate())} + {holiday && ( + {`(${holiday.title})`} + )} + + + + + {t("Timesheet Hours")} + + + {manhourFormatter.format(timesheetHours)} + + + + + {t("Leave Hours")} + + + {manhourFormatter.format(leaveHours)} + + + + + + {t("Daily Total Hours")} + + + {manhourFormatter.format(timesheetHours + leaveHours)} + + + + + + + ); + })} + + + {`${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), + )}`} + + + {`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format( + Object.values(leavesForCurrentMonth) + .flatMap((entries) => entries) + .map((entry) => entry.inputHours) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + + + ); +}; + +export default MonthlySummary; diff --git a/src/components/PastEntryCalendar/PastEntryCalendar.tsx b/src/components/PastEntryCalendar/PastEntryCalendar.tsx index a9e6a14..91c0012 100644 --- a/src/components/PastEntryCalendar/PastEntryCalendar.tsx +++ b/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 = ({ timesheet, leaves, onDateSelect, + onMonthChange, }) => { const { i18n: { language }, @@ -88,6 +90,7 @@ const PastEntryCalendar: React.FC = ({ > { +interface Props + extends Omit { open: boolean; handleClose: () => void; leaveTypes: LeaveType[]; allProjects: ProjectWithTasks[]; + companyHolidays: HolidaysResult[]; } const Indicator = styled(Box)(() => ({ @@ -45,6 +50,7 @@ const PastEntryCalendarModal: React.FC = ({ 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 = ({ handleClose(); }, [handleClose]); - const content = selectedDate ? ( - <> - - - ) : ( - <> - - - - {t("Has timesheet entry")} - - - - {t("Has leave entry")} - - - - - {t("Has both timesheet and leave entry")} - - - - - + const content = ( + + + + + + + {t("Has timesheet entry")} + + + + + {t("Has leave entry")} + + + + + {t("Has both timesheet and leave entry")} + + + + + + {selectedDate ? ( + + ) : ( + + )} + ); const isMobile = useIsMobile(); @@ -115,14 +133,14 @@ const PastEntryCalendarModal: React.FC = ({ startIcon={} onClick={clearDate} > - {t("Back")} + {t("Back to Monthly Summary")} )} ) : ( - + {t("Past Entries")} {content} {selectedDate && ( @@ -132,7 +150,7 @@ const PastEntryCalendarModal: React.FC = ({ startIcon={} onClick={clearDate} > - {t("Back")} + {t("Back to Monthly Summary")} )} diff --git a/src/components/PastEntryCalendar/PastEntryList.tsx b/src/components/PastEntryCalendar/PastEntryList.tsx index 35fbb3c..a2a5d9c 100644 --- a/src/components/PastEntryCalendar/PastEntryList.tsx +++ b/src/components/PastEntryCalendar/PastEntryList.tsx @@ -57,7 +57,12 @@ const PastEntryList: React.FC = ({ const dayJsObj = dayjs(date); return ( - + = ({ leaveTypeMap={leaveTypeMap} /> ))} - - {t("Total Work Hours")}: {manhourFormatter.format(timeEntries.map(entry => (entry.inputHours ?? 0) + (entry.otHours ?? 0)).reduce((acc, cur) => { return acc + cur }, 0))} - - - {t("Total Leave Hours")}: {manhourFormatter.format(leaveEntries.map(entry => entry.inputHours).reduce((acc, cur) => { return acc + cur }, 0))} - + + {`${t("Total Work Hours")}: ${manhourFormatter.format( + timeEntries + .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0)) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + + + {`${t("Total Leave Hours")}: ${manhourFormatter.format( + leaveEntries + .map((entry) => entry.inputHours) + .reduce((acc, cur) => { + return acc + cur; + }, 0), + )}`} + ); }; diff --git a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx index d102ce0..b858a60 100644 --- a/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx +++ b/src/components/TimeLeaveModal/TimeLeaveInputTable.tsx @@ -102,7 +102,7 @@ const TimeLeaveInputTable: React.FC = ({ }, {}); }, [assignedProjects]); - const { getValues, setValue, clearErrors } = + const { getValues, setValue, clearErrors, setError } = useFormContext(); const currentEntries = getValues(day); @@ -486,8 +486,13 @@ const TimeLeaveInputTable: React.FC = ({ .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, diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index ac9aede..63ab31d 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -154,6 +154,7 @@ const UserWorkspacePage: React.FC = ({ leaves={defaultLeaveRecords} allProjects={allProjects} leaveTypes={leaveTypes} + companyHolidays={holidays} />