You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

221 regels
7.1 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. LEAVE_DAILY_MAX_HOURS,
  21. TIMESHEET_DAILY_MAX_HOURS,
  22. } from "@/app/api/timesheets/utils";
  23. import { HolidaysResult } from "@/app/api/holidays";
  24. import { getHolidayForDate } from "@/app/utils/holidayUtils";
  25. interface Props<EntryComponentProps = object> {
  26. days: string[];
  27. companyHolidays: HolidaysResult[];
  28. leaveEntries: RecordLeaveInput;
  29. timesheetEntries: RecordTimesheetInput;
  30. EntryComponent: React.FunctionComponent<
  31. EntryComponentProps & { date: string }
  32. >;
  33. entryComponentProps: EntryComponentProps;
  34. }
  35. function DateHoursList<EntryTableProps>({
  36. days,
  37. leaveEntries,
  38. timesheetEntries,
  39. EntryComponent,
  40. entryComponentProps,
  41. companyHolidays,
  42. }: Props<EntryTableProps>) {
  43. const {
  44. t,
  45. i18n: { language },
  46. } = useTranslation("home");
  47. const [selectedDate, setSelectedDate] = useState("");
  48. const isDateSelected = selectedDate !== "";
  49. const makeSelectDate = useCallback(
  50. (date: string) => () => {
  51. setSelectedDate(date);
  52. },
  53. [],
  54. );
  55. const onDateDone = useCallback<React.MouseEventHandler<HTMLButtonElement>>(
  56. (e) => {
  57. setSelectedDate("");
  58. e.preventDefault();
  59. },
  60. [],
  61. );
  62. return (
  63. <>
  64. {isDateSelected ? (
  65. <EntryComponent date={selectedDate} {...entryComponentProps} />
  66. ) : (
  67. <Box overflow="scroll" flex={1}>
  68. {days.map((day, index) => {
  69. const dayJsObj = dayjs(day);
  70. const holiday = getHolidayForDate(day, companyHolidays);
  71. const isHoliday =
  72. holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6;
  73. const leaves = leaveEntries[day];
  74. const leaveHours =
  75. leaves?.reduce((acc, entry) => acc + entry.inputHours, 0) || 0;
  76. const timesheet = timesheetEntries[day];
  77. const timesheetHours =
  78. timesheet?.reduce(
  79. (acc, entry) =>
  80. acc + (entry.inputHours || 0) + (entry.otHours || 0),
  81. 0,
  82. ) || 0;
  83. const dailyTotal = leaveHours + timesheetHours;
  84. const leaveExceeded = leaveHours > LEAVE_DAILY_MAX_HOURS;
  85. const dailyTotalExceeded = dailyTotal > TIMESHEET_DAILY_MAX_HOURS;
  86. return (
  87. <Card
  88. key={`${day}-${index}`}
  89. sx={{ marginBlockEnd: 2, marginInline: 2 }}
  90. >
  91. <CardActionArea onClick={makeSelectDate(day)}>
  92. <CardContent sx={{ padding: 3 }}>
  93. <Typography
  94. variant="overline"
  95. component="div"
  96. sx={{
  97. color: isHoliday ? "error.main" : undefined,
  98. }}
  99. >
  100. {shortDateFormatter(language).format(dayJsObj.toDate())}
  101. {holiday && (
  102. <Typography
  103. marginInlineStart={1}
  104. variant="caption"
  105. >{`(${holiday.title})`}</Typography>
  106. )}
  107. </Typography>
  108. <Stack spacing={1}>
  109. <Box
  110. sx={{
  111. display: "flex",
  112. justifyContent: "space-between",
  113. alignItems: "baseline",
  114. }}
  115. >
  116. <Typography variant="body2">
  117. {t("Timesheet Hours")}
  118. </Typography>
  119. <Typography>
  120. {manhourFormatter.format(timesheetHours)}
  121. </Typography>
  122. </Box>
  123. <Box
  124. sx={{
  125. display: "flex",
  126. justifyContent: "space-between",
  127. flexWrap: "wrap",
  128. alignItems: "baseline",
  129. color: leaveExceeded ? "error.main" : undefined,
  130. }}
  131. >
  132. <Typography variant="body2">
  133. {t("Leave Hours")}
  134. </Typography>
  135. <Typography>
  136. {manhourFormatter.format(leaveHours)}
  137. </Typography>
  138. {leaveExceeded && (
  139. <Typography
  140. component="div"
  141. width="100%"
  142. variant="caption"
  143. >
  144. {t("Leave hours cannot be more than {{hours}}", {
  145. hours: LEAVE_DAILY_MAX_HOURS,
  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: dailyTotalExceeded ? "error.main" : undefined,
  157. }}
  158. >
  159. <Typography variant="body2">
  160. {t("Daily Total Hours")}
  161. </Typography>
  162. <Typography>
  163. {manhourFormatter.format(timesheetHours + leaveHours)}
  164. </Typography>
  165. {dailyTotalExceeded && (
  166. <Typography
  167. component="div"
  168. width="100%"
  169. variant="caption"
  170. >
  171. {t(
  172. "The daily total hours cannot be more than {{hours}}",
  173. {
  174. hours: TIMESHEET_DAILY_MAX_HOURS,
  175. },
  176. )}
  177. </Typography>
  178. )}
  179. </Box>
  180. </Stack>
  181. </CardContent>
  182. </CardActionArea>
  183. </Card>
  184. );
  185. })}
  186. </Box>
  187. )}
  188. <Box padding={2} display="flex" justifyContent="flex-end">
  189. {isDateSelected ? (
  190. <Button
  191. variant="outlined"
  192. startIcon={<ArrowBack />}
  193. onClick={onDateDone}
  194. >
  195. {t("Done")}
  196. </Button>
  197. ) : (
  198. <Button variant="contained" startIcon={<Check />} type="submit">
  199. {t("Save")}
  200. </Button>
  201. )}
  202. </Box>
  203. </>
  204. );
  205. }
  206. export default DateHoursList;