您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

249 行
8.4 KiB

  1. import {
  2. RecordLeaveInput,
  3. RecordTimesheetInput,
  4. } from "@/app/api/timesheets/actions";
  5. import { manhourFormatter, shortDateFormatter } from "@/app/utils/formatUtil";
  6. import { ArrowBack, Check } from "@mui/icons-material";
  7. import {
  8. Box,
  9. Button,
  10. Card,
  11. CardActionArea,
  12. CardContent,
  13. Stack,
  14. Typography,
  15. } from "@mui/material";
  16. import dayjs from "dayjs";
  17. import React, { useCallback, useState } from "react";
  18. import { useTranslation } from "react-i18next";
  19. import {
  20. DAILY_NORMAL_MAX_HOURS,
  21. LEAVE_DAILY_MAX_HOURS,
  22. TIMESHEET_DAILY_MAX_HOURS,
  23. } from "@/app/api/timesheets/utils";
  24. import { HolidaysResult } from "@/app/api/holidays";
  25. import { getHolidayForDate } from "@/app/utils/holidayUtils";
  26. interface Props<EntryComponentProps = object> {
  27. days: string[];
  28. companyHolidays: HolidaysResult[];
  29. leaveEntries: RecordLeaveInput;
  30. timesheetEntries: RecordTimesheetInput;
  31. EntryComponent: React.FunctionComponent<
  32. EntryComponentProps & { date: string }
  33. >;
  34. entryComponentProps: EntryComponentProps;
  35. errorComponent?: React.ReactNode;
  36. }
  37. function DateHoursList<EntryTableProps>({
  38. days,
  39. leaveEntries,
  40. timesheetEntries,
  41. EntryComponent,
  42. entryComponentProps,
  43. companyHolidays,
  44. errorComponent,
  45. }: Props<EntryTableProps>) {
  46. const {
  47. t,
  48. i18n: { language },
  49. } = useTranslation("home");
  50. const [selectedDate, setSelectedDate] = useState("");
  51. const isDateSelected = selectedDate !== "";
  52. const makeSelectDate = useCallback(
  53. (date: string) => () => {
  54. setSelectedDate(date);
  55. },
  56. [],
  57. );
  58. const onDateDone = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
  59. (e) => {
  60. setSelectedDate("");
  61. e.preventDefault();
  62. },
  63. [],
  64. );
  65. return (
  66. <>
  67. {isDateSelected ? (
  68. <EntryComponent date={selectedDate} {...entryComponentProps} />
  69. ) : (
  70. <Box overflow="scroll" flex={1}>
  71. {days.map((day, index) => {
  72. const dayJsObj = dayjs(day);
  73. const holiday = getHolidayForDate(day, companyHolidays);
  74. const isHoliday =
  75. holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
  76. const leaves = leaveEntries[day];
  77. const leaveHours =
  78. leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
  79. const timesheet = timesheetEntries[day];
  80. const timesheetNormalHours =
  81. timesheet?.reduce(
  82. (acc, entry) => acc + (entry.inputHours || 0),
  83. 0,
  84. ) || 0;
  85. const timesheetOtHours =
  86. timesheet?.reduce(
  87. (acc, entry) => acc + (entry.otHours || 0),
  88. 0,
  89. ) || 0;
  90. const timesheetHours = timesheetNormalHours + timesheetOtHours;
  91. const dailyTotal = leaveHours + timesheetHours;
  92. const normalHoursExceeded =
  93. timesheetNormalHours > DAILY_NORMAL_MAX_HOURS;
  94. const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS;
  95. const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS;
  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. color: normalHoursExceeded ? "error.main" : undefined,
  126. }}
  127. >
  128. <Typography variant="body2">
  129. {t("Timesheet Hours")}
  130. </Typography>
  131. <Typography>
  132. {manhourFormatter.format(timesheetHours)}
  133. </Typography>
  134. {normalHoursExceeded && (
  135. <Typography
  136. component="div"
  137. width="100%"
  138. variant="caption"
  139. paddingInlineEnd="40%"
  140. >
  141. {t(
  142. "The daily normal hours cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours.",
  143. {
  144. DAILY_NORMAL_MAX_HOURS,
  145. },
  146. )}
  147. </Typography>
  148. )}
  149. </Box>
  150. <Box
  151. sx={{
  152. display: "flex",
  153. justifyContent: "space-between",
  154. flexWrap: "wrap",
  155. alignItems: "baseline",
  156. color: leaveExceeded ? "error.main" : undefined,
  157. }}
  158. >
  159. <Typography variant="body2">
  160. {t("Leave Hours")}
  161. </Typography>
  162. <Typography>
  163. {manhourFormatter.format(leaveHours)}
  164. </Typography>
  165. {leaveExceeded && (
  166. <Typography
  167. component="div"
  168. width="100%"
  169. variant="caption"
  170. >
  171. {t("Leave hours cannot be more than {{hours}}", {
  172. hours: LEAVE_DAILY_MAX_HOURS,
  173. })}
  174. </Typography>
  175. )}
  176. </Box>
  177. <Box
  178. sx={{
  179. display: "flex",
  180. justifyContent: "space-between",
  181. flexWrap: "wrap",
  182. alignItems: "baseline",
  183. color: dailyTotalExceeded ? "error.main" : undefined,
  184. }}
  185. >
  186. <Typography variant="body2">
  187. {t("Daily Total Hours")}
  188. </Typography>
  189. <Typography>
  190. {manhourFormatter.format(timesheetHours + leaveHours)}
  191. </Typography>
  192. {dailyTotalExceeded && (
  193. <Typography
  194. component="div"
  195. width="100%"
  196. variant="caption"
  197. >
  198. {t(
  199. "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}",
  200. {
  201. TIMESHEET_DAILY_MAX_HOURS,
  202. },
  203. )}
  204. </Typography>
  205. )}
  206. </Box>
  207. </Stack>
  208. </CardContent>
  209. </CardActionArea>
  210. </Card>
  211. );
  212. })}
  213. </Box>
  214. )}
  215. {errorComponent}
  216. <Box padding={2} display="flex" justifyContent="flex-end">
  217. {isDateSelected ? (
  218. <Button
  219. variant="outlined"
  220. startIcon={<ArrowBack />}
  221. onClick={onDateDone}
  222. >
  223. {t("Done")}
  224. </Button>
  225. ) : (
  226. <Button variant="contained" startIcon={<Check />} type="submit">
  227. {t("Save")}
  228. </Button>
  229. )}
  230. </Box>
  231. </>
  232. );
  233. }
  234. export default DateHoursList;