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.
 
 

168 line
5.2 KiB

  1. import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils";
  2. import { HolidaysResult } from "../holidays";
  3. import { LeaveEntry, RecordTimeLeaveInput, TimeEntry } from "./actions";
  4. import { convertDateArrayToString } from "@/app/utils/formatUtil";
  5. import compact from "lodash/compact";
  6. import dayjs from "dayjs";
  7. export type TimeEntryError = {
  8. [field in keyof TimeEntry]?: string;
  9. };
  10. /**
  11. * @param entry - the time entry
  12. * @returns an object where the keys are the error fields and the values the error message, and undefined if there are no errors
  13. */
  14. export const validateTimeEntry = (
  15. entry: Partial<TimeEntry>,
  16. isHoliday: boolean,
  17. ): TimeEntryError | undefined => {
  18. // Test for errors
  19. const error: TimeEntryError = {};
  20. // Either normal or other hours need to be inputted
  21. if (!entry.inputHours && !entry.otHours) {
  22. error[isHoliday ? "otHours" : "inputHours"] = "Required";
  23. } else if (entry.inputHours && isHoliday) {
  24. error.inputHours = "Cannot input normal hours on holidays";
  25. } else if (entry.inputHours && entry.inputHours <= 0) {
  26. error.inputHours =
  27. "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  28. } else if (entry.inputHours && entry.inputHours > DAILY_NORMAL_MAX_HOURS) {
  29. error.inputHours =
  30. "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  31. } else if (entry.otHours && entry.otHours <= 0) {
  32. error.otHours = "Hours should be bigger than 0";
  33. }
  34. // If there is a project id, there should also be taskGroupId, taskId, inputHours
  35. if (entry.projectId) {
  36. if (!entry.taskGroupId) {
  37. error.taskGroupId = "Required";
  38. } else if (!entry.taskId) {
  39. error.taskId = "Required";
  40. }
  41. } else {
  42. if (entry.taskGroupId && !entry.taskId) {
  43. error.taskId = "Required";
  44. } else if (!entry.remark) {
  45. error.remark = "Required for non-billable tasks";
  46. }
  47. }
  48. return Object.keys(error).length > 0 ? error : undefined;
  49. };
  50. export type LeaveEntryError = {
  51. [field in keyof LeaveEntry]?: string;
  52. };
  53. export const validateLeaveEntry = (
  54. entry: Partial<LeaveEntry>,
  55. isHoliday: boolean,
  56. ): LeaveEntryError | undefined => {
  57. // Test for errrors
  58. const error: LeaveEntryError = {};
  59. if (!entry.leaveTypeId) {
  60. error.leaveTypeId = "Required";
  61. } else if (entry.inputHours && isHoliday) {
  62. error.inputHours = "Cannot input normal hours on holidays";
  63. } else if (!entry.inputHours) {
  64. error.inputHours = "Required";
  65. } else if (
  66. entry.inputHours &&
  67. (entry.inputHours <= 0 || entry.inputHours > DAILY_NORMAL_MAX_HOURS)
  68. ) {
  69. error.inputHours =
  70. "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}";
  71. }
  72. return Object.keys(error).length > 0 ? error : undefined;
  73. };
  74. export const validateTimeLeaveRecord = (
  75. records: RecordTimeLeaveInput,
  76. companyHolidays: HolidaysResult[],
  77. isFullTime?: boolean,
  78. ): { [date: string]: string } | undefined => {
  79. const errors: { [date: string]: string } = {};
  80. const holidays = new Set(
  81. compact([
  82. ...getPublicHolidaysForNYears(2).map((h) => h.date),
  83. ...companyHolidays.map((h) => convertDateArrayToString(h.date)),
  84. ]),
  85. );
  86. Object.keys(records).forEach((date) => {
  87. const dayJsObj = dayjs(date);
  88. const isHoliday =
  89. holidays.has(date) || dayJsObj.day() === 0 || dayJsObj.day() === 6;
  90. const entries = records[date];
  91. // Check each entry
  92. for (const entry of entries) {
  93. let entryError;
  94. if (entry.type === "leaveEntry") {
  95. entryError = validateLeaveEntry(entry, isHoliday);
  96. } else {
  97. entryError = validateTimeEntry(entry, isHoliday);
  98. }
  99. if (entryError) {
  100. errors[date] = "There are errors in the entries";
  101. return;
  102. }
  103. }
  104. // Check total hours
  105. const totalHourError = checkTotalHours(
  106. entries.filter((e) => e.type === "timeEntry") as TimeEntry[],
  107. entries.filter((e) => e.type === "leaveEntry") as LeaveEntry[],
  108. isHoliday,
  109. isFullTime,
  110. );
  111. if (totalHourError) {
  112. errors[date] = totalHourError;
  113. }
  114. });
  115. return Object.keys(errors).length > 0 ? errors : undefined;
  116. };
  117. export const checkTotalHours = (
  118. timeEntries: TimeEntry[],
  119. leaves: LeaveEntry[],
  120. isHoliday?: boolean,
  121. isFullTime?: boolean,
  122. ): string | undefined => {
  123. const leaveHours = leaves.reduce((acc, entry) => acc + entry.inputHours, 0);
  124. const totalInputHours = timeEntries.reduce((acc, entry) => {
  125. return acc + (entry.inputHours || 0);
  126. }, 0);
  127. const totalOtHours = timeEntries.reduce((acc, entry) => {
  128. return acc + (entry.otHours || 0);
  129. }, 0);
  130. if (totalInputHours + leaveHours > DAILY_NORMAL_MAX_HOURS) {
  131. return "The daily normal hours (timesheet hours + leave hours) cannot be more than {{DAILY_NORMAL_MAX_HOURS}}. Please use other hours for exceeding hours or decrease the leave hours.";
  132. } else if (
  133. isFullTime &&
  134. !isHoliday &&
  135. totalInputHours + leaveHours !== DAILY_NORMAL_MAX_HOURS
  136. ) {
  137. return "The daily normal hours (timesheet hours + leave hours) for full-time staffs should be {{DAILY_NORMAL_MAX_HOURS}}.";
  138. } else if (
  139. totalInputHours + totalOtHours + leaveHours >
  140. TIMESHEET_DAILY_MAX_HOURS
  141. ) {
  142. return "The daily total hours cannot be more than {{TIMESHEET_DAILY_MAX_HOURS}}";
  143. }
  144. };
  145. export const DAILY_NORMAL_MAX_HOURS = 8;
  146. export const TIMESHEET_DAILY_MAX_HOURS = 20;