import { TimeEntry } from "@/app/api/timesheets/actions"; import { Check, Delete, Close } from "@mui/icons-material"; import { Box, Button, FormControl, InputLabel, Modal, ModalProps, Paper, SxProps, TextField, Typography, } from "@mui/material"; import React, { useCallback, useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import ProjectSelect from "./ProjectSelect"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import TaskGroupSelect, { TaskGroupSelectWithoutProject, } from "./TaskGroupSelect"; import TaskSelect from "./TaskSelect"; import { Task, TaskGroup } from "@/app/api/tasks"; import uniqBy from "lodash/uniqBy"; import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; import { shortDateFormatter } from "@/app/utils/formatUtil"; import { DAILY_NORMAL_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, } from "@/app/api/timesheets/utils"; import dayjs from "dayjs"; export interface Props extends Omit { onSave: (timeEntry: TimeEntry, recordDate?: string) => Promise; onDelete?: () => Promise; defaultValues?: Partial; allProjects: ProjectWithTasks[]; assignedProjects: AssignedProject[]; modalSx?: SxProps; recordDate?: string; isHoliday?: boolean; miscTasks: Task[]; } const modalSx: SxProps = { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: "90%", maxHeight: "90%", padding: 3, display: "flex", flexDirection: "column", gap: 2, }; const TimesheetEditModal: React.FC = ({ onSave, onDelete, open, onClose, defaultValues, allProjects, assignedProjects, modalSx: mSx, recordDate, isHoliday, miscTasks, }) => { const { t, i18n: { language }, } = useTranslation("home"); const taskGroupsByProject = useMemo(() => { return allProjects.reduce<{ [projectId: AssignedProject["id"]]: { value: TaskGroup["id"]; label: string; }[]; }>((acc, project) => { return { ...acc, [project.id]: uniqBy( project.tasks.map((t) => ({ value: t.taskGroup.id, label: t.taskGroup.name, })), "value", ), }; }, {}); }, [allProjects]); const taskGroupsWithoutProject = useMemo( () => uniqBy( miscTasks.map((t) => t.taskGroup), "id", ), [miscTasks], ); const { register, control, reset, getValues, setValue, trigger, setError, formState, watch, } = useForm(); useEffect(() => { reset(defaultValues ?? { id: Date.now() }); }, [defaultValues, reset]); const saveHandler = useCallback(async () => { const valid = await trigger(); if (valid) { try { await onSave(getValues(), recordDate); reset({ id: Date.now() }); } catch (e) { setError("root", { message: e instanceof Error ? e.message : "Unknown error", }); } } }, [getValues, onSave, recordDate, reset, setError, trigger]); const closeHandler = useCallback>( (...args) => { onClose?.(...args); reset({ id: Date.now() }); }, [onClose, reset], ); const projectId = watch("projectId"); const taskGroupId = watch("taskGroupId"); const otHours = watch("otHours"); return ( {recordDate && ( {shortDateFormatter(language).format(new Date(recordDate))} )} {t("Project Code and Name")} ( { field.onChange(newId ?? null); const firstTaskGroup = ( typeof newId === "number" ? taskGroupsByProject[newId] : [] )[0]; setValue("taskGroupId", firstTaskGroup?.value); setValue("taskId", undefined); }} isTimesheetAdmendment={true} /> )} rules={{ deps: ["taskGroupId", "taskId"] }} /> {t("Stage")} projectId ? ( { field.onChange(newId ?? null); }} /> ) : ( { field.onChange(newId ?? null); if (!newId) { setValue("taskId", undefined); } }} taskGroups={taskGroupsWithoutProject} /> ) } rules={{ validate: (id) => { if (!projectId) { return true; } const taskGroups = taskGroupsByProject[projectId]; return taskGroups.some((tg) => tg.value === id); }, deps: ["taskId"], }} /> {t("Task")} ( { field.onChange(newId ?? null); }} miscTasks={miscTasks} /> )} rules={{ validate: (id) => { const tasks = projectId ? allProjects.find((p) => p.id === projectId)?.tasks : miscTasks; return taskGroupId ? Boolean( tasks?.some( (task) => task.id === id && task.taskGroup.id === taskGroupId, ), ) : !id; }, }} /> roundToNearestQuarter(parseFloat(value)), validate: (value) => { if (value) { if (isHoliday) { return t("Cannot input normal hours on holidays"); } return ( (0 < value && value <= DAILY_NORMAL_MAX_HOURS) || t( "Input hours should be between 0 and {{DAILY_NORMAL_MAX_HOURS}}", { DAILY_NORMAL_MAX_HOURS }, ) ); } else { return Boolean(value || otHours) || t("Required"); } }, })} error={Boolean(formState.errors.inputHours)} helperText={formState.errors.inputHours?.message} /> roundToNearestQuarter(parseFloat(value)), validate: (value) => (value ? value > 0 : true), })} error={Boolean(formState.errors.otHours)} /> Boolean(projectId || taskGroupId || value) || t("Required for non-billable tasks"), })} helperText={formState.errors.remark?.message} /> {formState.errors.root?.message && ( {t(formState.errors.root.message, { DAILY_NORMAL_MAX_HOURS, TIMESHEET_DAILY_MAX_HOURS, })} )} {onDelete && ( )} ); }; export default TimesheetEditModal;