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.
 
 

197 lines
5.3 KiB

  1. import { LeaveType } from "@/app/api/timesheets";
  2. import { LeaveEntry } from "@/app/api/timesheets/actions";
  3. import {
  4. DAILY_NORMAL_MAX_HOURS,
  5. TIMESHEET_DAILY_MAX_HOURS,
  6. } from "@/app/api/timesheets/utils";
  7. import { shortDateFormatter } from "@/app/utils/formatUtil";
  8. import { roundToNearestQuarter } from "@/app/utils/manhourUtils";
  9. import { Check, Delete, Close } from "@mui/icons-material";
  10. import {
  11. Box,
  12. Button,
  13. FormControl,
  14. InputLabel,
  15. MenuItem,
  16. Modal,
  17. ModalProps,
  18. Paper,
  19. Select,
  20. SxProps,
  21. TextField,
  22. Typography,
  23. } from "@mui/material";
  24. import React, { useCallback, useEffect } from "react";
  25. import { Controller, useForm } from "react-hook-form";
  26. import { useTranslation } from "react-i18next";
  27. export interface Props extends Omit<ModalProps, "children"> {
  28. onSave: (leaveEntry: LeaveEntry, recordDate?: string) => Promise<void>;
  29. onDelete?: () => Promise<void>;
  30. leaveTypes: LeaveType[];
  31. defaultValues?: Partial<LeaveEntry>;
  32. modalSx?: SxProps;
  33. recordDate?: string;
  34. isHoliday?: boolean;
  35. }
  36. const modalSx: SxProps = {
  37. position: "absolute",
  38. top: "50%",
  39. left: "50%",
  40. transform: "translate(-50%, -50%)",
  41. width: "90%",
  42. maxHeight: "90%",
  43. padding: 3,
  44. display: "flex",
  45. flexDirection: "column",
  46. gap: 2,
  47. };
  48. const LeaveEditModal: React.FC<Props> = ({
  49. onSave,
  50. onDelete,
  51. open,
  52. onClose,
  53. leaveTypes,
  54. defaultValues,
  55. recordDate,
  56. modalSx: mSx,
  57. isHoliday,
  58. }) => {
  59. const {
  60. t,
  61. i18n: { language },
  62. } = useTranslation("home");
  63. const { register, control, reset, getValues, trigger, formState, setError } =
  64. useForm<LeaveEntry>({
  65. defaultValues: {
  66. leaveTypeId: leaveTypes[0].id,
  67. },
  68. });
  69. useEffect(() => {
  70. reset(defaultValues ?? { leaveTypeId: leaveTypes[0].id, id: Date.now() });
  71. }, [defaultValues, leaveTypes, reset]);
  72. const saveHandler = useCallback(async () => {
  73. const valid = await trigger();
  74. if (valid) {
  75. try {
  76. await onSave(getValues(), recordDate);
  77. reset({ id: Date.now() });
  78. } catch (e) {
  79. setError("root", {
  80. message: e instanceof Error ? e.message : "Unknown error",
  81. });
  82. }
  83. }
  84. }, [getValues, onSave, recordDate, reset, setError, trigger]);
  85. const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
  86. (...args) => {
  87. onClose?.(...args);
  88. reset({ id: Date.now() });
  89. },
  90. [onClose, reset],
  91. );
  92. return (
  93. <Modal open={open} onClose={closeHandler}>
  94. <Paper sx={{ ...modalSx, ...mSx }}>
  95. {recordDate && (
  96. <Typography
  97. variant="h6"
  98. marginBlockEnd={2}
  99. color={isHoliday ? "error.main" : undefined}
  100. >
  101. {shortDateFormatter(language).format(new Date(recordDate))}
  102. </Typography>
  103. )}
  104. <FormControl fullWidth>
  105. <InputLabel>{t("Leave Types")}</InputLabel>
  106. <Controller
  107. defaultValue={leaveTypes[0].id}
  108. control={control}
  109. name="leaveTypeId"
  110. render={({ field }) => (
  111. <Select label={t("Leave Types")} {...field}>
  112. {leaveTypes.map((type, index) => (
  113. <MenuItem key={`${type.id}-${index}`} value={type.id}>
  114. {t(type.name)}
  115. </MenuItem>
  116. ))}
  117. </Select>
  118. )}
  119. />
  120. </FormControl>
  121. <TextField
  122. type="number"
  123. label={t("Hours")}
  124. fullWidth
  125. {...register("inputHours", {
  126. setValueAs: (value) => roundToNearestQuarter(parseFloat(value)),
  127. validate: (value) => {
  128. if (isHoliday) {
  129. return t("Cannot input normal hours on holidays");
  130. }
  131. return (
  132. (0 < value && value <= DAILY_NORMAL_MAX_HOURS) ||
  133. t(
  134. "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}",
  135. { DAILY_NORMAL_MAX_HOURS },
  136. )
  137. );
  138. },
  139. })}
  140. error={Boolean(formState.errors.inputHours)}
  141. helperText={formState.errors.inputHours?.message}
  142. />
  143. <TextField
  144. label={t("Remarks")}
  145. fullWidth
  146. multiline
  147. rows={2}
  148. {...register("remark")}
  149. />
  150. {formState.errors.root?.message && (
  151. <Typography variant="caption" color="error">
  152. {t(formState.errors.root.message, {
  153. DAILY_NORMAL_MAX_HOURS,
  154. TIMESHEET_DAILY_MAX_HOURS,
  155. })}
  156. </Typography>
  157. )}
  158. <Box display="flex" justifyContent="flex-end" gap={1}>
  159. {onDelete && (
  160. <Button
  161. variant="outlined"
  162. startIcon={<Delete />}
  163. color="error"
  164. onClick={onDelete}
  165. >
  166. {t("Delete")}
  167. </Button>
  168. )}
  169. <Button
  170. variant="outlined"
  171. startIcon={<Close />}
  172. onClick={(event) => closeHandler(event, "backdropClick")}
  173. >
  174. {t("Close")}
  175. </Button>
  176. <Button
  177. variant="contained"
  178. startIcon={<Check />}
  179. onClick={saveHandler}
  180. >
  181. {t("Save")}
  182. </Button>
  183. </Box>
  184. </Paper>
  185. </Modal>
  186. );
  187. };
  188. export default LeaveEditModal;