選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

211 行
6.6 KiB

  1. import {
  2. RecordLeaveInput,
  3. RecordTimesheetInput,
  4. } from "@/app/api/timesheets/actions";
  5. import {
  6. Box,
  7. Card,
  8. CardActionArea,
  9. CardContent,
  10. Stack,
  11. Typography,
  12. } from "@mui/material";
  13. import union from "lodash/union";
  14. import { useCallback, useEffect, useMemo } from "react";
  15. import dayjs, { Dayjs } from "dayjs";
  16. import { getHolidayForDate, getPublicHolidaysForNYears } from "@/app/utils/holidayUtils";
  17. import { HolidaysResult } from "@/app/api/holidays";
  18. import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil";
  19. import { useTranslation } from "react-i18next";
  20. import pickBy from "lodash/pickBy";
  21. interface Props {
  22. currentMonth: Dayjs;
  23. timesheet: RecordTimesheetInput;
  24. leaves: RecordLeaveInput;
  25. companyHolidays: HolidaysResult[];
  26. onDateSelect: (date: string) => void;
  27. }
  28. const MonthlySummary: React.FC<Props> = ({
  29. timesheet,
  30. leaves,
  31. currentMonth,
  32. companyHolidays,
  33. onDateSelect,
  34. }) => {
  35. const {
  36. t,
  37. i18n: { language },
  38. } = useTranslation("home");
  39. // calendar related
  40. const holidays = useMemo(() => {
  41. const holidays = getPublicHolidaysForNYears(1, currentMonth.year()).map(holiday => holiday.date)
  42. return holidays.filter(date => {
  43. return currentMonth.isSame(dayjs(date), "month")
  44. })
  45. }, [currentMonth]);
  46. const timesheetForCurrentMonth = useMemo(() => {
  47. return pickBy(timesheet, (_, date) => {
  48. return currentMonth.isSame(dayjs(date), "month");
  49. });
  50. }, [currentMonth, timesheet]);
  51. const leavesForCurrentMonth = useMemo(() => {
  52. return pickBy(leaves, (_, date) => {
  53. return currentMonth.isSame(dayjs(date), "month");
  54. });
  55. }, [currentMonth, leaves]);
  56. const days = useMemo(() => {
  57. return union(
  58. Object.keys(timesheetForCurrentMonth),
  59. Object.keys(leavesForCurrentMonth),
  60. holidays
  61. );
  62. }, [timesheetForCurrentMonth, leavesForCurrentMonth, holidays]).sort();
  63. const makeSelectDate = useCallback(
  64. (date: string) => () => {
  65. onDateSelect(date);
  66. },
  67. [onDateSelect],
  68. );
  69. useEffect(()=> {
  70. console.log(holidays)
  71. console.log(timesheetForCurrentMonth)
  72. },[currentMonth, timesheetForCurrentMonth])
  73. return (
  74. <Stack
  75. gap={2}
  76. marginBlockEnd={2}
  77. minWidth={{ sm: 375 }}
  78. maxHeight={{ sm: 500 }}
  79. >
  80. <Typography variant="overline">{t("Monthly Summary")}</Typography>
  81. <Box sx={{ overflowY: "scroll" }} flex={1}>
  82. {days.map((day, index) => {
  83. const dayJsObj = dayjs(day);
  84. const holiday = getHolidayForDate(day, companyHolidays);
  85. const isHoliday =
  86. holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
  87. const ls = leavesForCurrentMonth[day];
  88. const leaveHours =
  89. ls?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
  90. const ts = timesheetForCurrentMonth[day];
  91. const timesheetNormalHours =
  92. ts?.reduce((acc, entry) => acc + (entry.inputHours || 0), 0) || 0;
  93. const timesheetOtHours =
  94. ts?.reduce((acc, entry) => acc + (entry.otHours || 0), 0) || 0;
  95. const timesheetHours = timesheetNormalHours + timesheetOtHours;
  96. return (
  97. <Card
  98. key={`${day}-${index}`}
  99. sx={{ marginBlockEnd: 2, marginInline: 2 }}
  100. >
  101. <CardActionArea onClick={makeSelectDate(day)}>
  102. <CardContent sx={{ padding: 3 }}>
  103. <Typography
  104. variant="overline"
  105. component="div"
  106. sx={{
  107. color: isHoliday ? "error.main" : undefined,
  108. }}
  109. >
  110. {shortDateFormatter(language).format(dayJsObj.toDate())}
  111. {holiday && (
  112. <Typography
  113. marginInlineStart={1}
  114. variant="caption"
  115. >{`(${holiday.title})`}</Typography>
  116. )}
  117. </Typography>
  118. <Stack spacing={1}>
  119. <Box
  120. sx={{
  121. display: "flex",
  122. justifyContent: "space-between",
  123. flexWrap: "wrap",
  124. alignItems: "baseline",
  125. }}
  126. >
  127. <Typography variant="body2">
  128. {t("Timesheet Hours")}
  129. </Typography>
  130. <Typography>
  131. {manhourFormatter.format(timesheetHours)}
  132. </Typography>
  133. </Box>
  134. <Box
  135. sx={{
  136. display: "flex",
  137. justifyContent: "space-between",
  138. flexWrap: "wrap",
  139. alignItems: "baseline",
  140. }}
  141. >
  142. <Typography variant="body2">
  143. {t("Leave Hours")}
  144. </Typography>
  145. <Typography>
  146. {manhourFormatter.format(leaveHours)}
  147. </Typography>
  148. </Box>
  149. <Box
  150. sx={{
  151. display: "flex",
  152. justifyContent: "space-between",
  153. flexWrap: "wrap",
  154. alignItems: "baseline",
  155. }}
  156. >
  157. <Typography variant="body2">
  158. {t("Daily Total Hours")}
  159. </Typography>
  160. <Typography>
  161. {manhourFormatter.format(timesheetHours + leaveHours)}
  162. </Typography>
  163. </Box>
  164. </Stack>
  165. </CardContent>
  166. </CardActionArea>
  167. </Card>
  168. );
  169. })}
  170. </Box>
  171. <Typography variant="overline">
  172. {`${t("Total Monthly Work Hours")}: ${manhourFormatter.format(
  173. Object.values(timesheetForCurrentMonth)
  174. .flatMap((entries) => entries)
  175. .map((entry) => (entry.inputHours ?? 0) + (entry.otHours ?? 0))
  176. .reduce((acc, cur) => {
  177. return acc + cur;
  178. }, 0),
  179. )}`}
  180. </Typography>
  181. <Typography variant="overline">
  182. {`${t("Total Monthly Leave Hours")}: ${manhourFormatter.format(
  183. Object.values(leavesForCurrentMonth)
  184. .flatMap((entries) => entries)
  185. .map((entry) => entry.inputHours)
  186. .reduce((acc, cur) => {
  187. return acc + cur;
  188. }, 0),
  189. )}`}
  190. </Typography>
  191. </Stack>
  192. );
  193. };
  194. export default MonthlySummary;