diff --git a/src/components/TimesheetAmendment/TimesheetAmendment.tsx b/src/components/TimesheetAmendment/TimesheetAmendment.tsx index 484fe87..c5c6dc7 100644 --- a/src/components/TimesheetAmendment/TimesheetAmendment.tsx +++ b/src/components/TimesheetAmendment/TimesheetAmendment.tsx @@ -42,6 +42,7 @@ import dayjs from "dayjs"; import { checkTotalHours } from "@/app/api/timesheets/utils"; import unionBy from "lodash/unionBy"; import { Luggage, MoreTime } from "@mui/icons-material"; +import { Task } from "@/app/api/tasks"; export interface Props { leaveTypes: LeaveType[]; @@ -49,6 +50,7 @@ export interface Props { teamTimesheets: TeamTimeSheets; companyHolidays: HolidaysResult[]; allProjects: ProjectWithTasks[]; + miscTasks: Task[]; } type MemberOption = TeamTimeSheets[0] & TeamLeaves[0] & { id: string }; @@ -76,6 +78,7 @@ const TimesheetAmendment: React.FC = ({ companyHolidays, allProjects, leaveTypes, + miscTasks, }) => { const { t } = useTranslation(["home", "common"]); @@ -459,6 +462,7 @@ const TimesheetAmendment: React.FC = ({ open={editModalOpen} onClose={closeEditModal} onSave={handleSave} + miscTasks={miscTasks} {...editModalProps} /> = ({ teamTimesheets, companyHolidays, allProjects, + miscTasks, }) => { const { t } = useTranslation("home"); const isMobile = useIsMobile(); @@ -49,6 +50,7 @@ export const TimesheetAmendmentModal: React.FC = ({ companyHolidays={companyHolidays} teamTimesheets={teamTimesheets} allProjects={allProjects} + miscTasks={miscTasks} /> ); diff --git a/src/components/TimesheetTable/EntryInputTable.tsx b/src/components/TimesheetTable/EntryInputTable.tsx deleted file mode 100644 index a2de809..0000000 --- a/src/components/TimesheetTable/EntryInputTable.tsx +++ /dev/null @@ -1,535 +0,0 @@ -import { Add, Check, Close, Delete } from "@mui/icons-material"; -import { Box, Button, Tooltip, Typography } from "@mui/material"; -import { - FooterPropsOverrides, - GridActionsCellItem, - GridCellParams, - GridColDef, - GridEditInputCell, - GridEventListener, - GridRenderEditCellParams, - GridRowId, - GridRowModel, - GridRowModes, - GridRowModesModel, - GridToolbarContainer, - useGridApiRef, -} from "@mui/x-data-grid"; -import { useTranslation } from "react-i18next"; -import StyledDataGrid from "../StyledDataGrid"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useFormContext } from "react-hook-form"; -import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions"; -import { manhourFormatter } from "@/app/utils/formatUtil"; -import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; -import uniqBy from "lodash/uniqBy"; -import { TaskGroup } from "@/app/api/tasks"; -import dayjs from "dayjs"; -import isBetween from "dayjs/plugin/isBetween"; -import ProjectSelect from "./ProjectSelect"; -import TaskGroupSelect from "./TaskGroupSelect"; -import TaskSelect from "./TaskSelect"; -import { - DAILY_NORMAL_MAX_HOURS, - TimeEntryError, - validateTimeEntry, -} from "@/app/api/timesheets/utils"; -import { roundToNearestQuarter } from "@/app/utils/manhourUtils"; -import FastTimeEntryModal from "./FastTimeEntryModal"; - -dayjs.extend(isBetween); - -interface Props { - day: string; - isHoliday: boolean; - allProjects: ProjectWithTasks[]; - assignedProjects: AssignedProject[]; - fastEntryEnabled?: boolean; -} - -export type TimeEntryRow = Partial< - TimeEntry & { - _isNew: boolean; - _error: TimeEntryError; - isPlanned?: boolean; - } ->; - -const EntryInputTable: React.FC = ({ - day, - allProjects, - assignedProjects, - isHoliday, - fastEntryEnabled, -}) => { - const { t } = 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]); - - // To check for start / end planned dates - const milestonesByProject = useMemo(() => { - return assignedProjects.reduce<{ - [projectId: AssignedProject["id"]]: AssignedProject["milestones"]; - }>((acc, project) => { - return { ...acc, [project.id]: { ...project.milestones } }; - }, {}); - }, [assignedProjects]); - - const { getValues, setValue, clearErrors } = - useFormContext(); - const currentEntries = getValues(day); - - const [entries, setEntries] = useState(currentEntries || []); - - const [rowModesModel, setRowModesModel] = useState({}); - - const apiRef = useGridApiRef(); - const addRow = useCallback(() => { - const id = Date.now(); - setEntries((e) => [...e, { id, _isNew: true }]); - setRowModesModel((model) => ({ - ...model, - [id]: { mode: GridRowModes.Edit, fieldToFocus: "projectId" }, - })); - }, []); - - const validateRow = useCallback( - (id: GridRowId) => { - const row = apiRef.current.getRowWithUpdatedValues( - id, - "", - ) as TimeEntryRow; - - const error = validateTimeEntry(row, isHoliday); - - // Test for warnings - let isPlanned; - if ( - row.projectId && - row.taskGroupId && - milestonesByProject[row.projectId] - ) { - const milestone = - milestonesByProject[row.projectId][row.taskGroupId] || {}; - const { startDate, endDate } = milestone; - // Check if the current day is between the start and end date inclusively - isPlanned = dayjs(day).isBetween(startDate, endDate, "day", "[]"); - } - - apiRef.current.updateRows([{ id, _error: error, isPlanned }]); - return !error; - }, - [apiRef, day, isHoliday, milestonesByProject], - ); - - const handleCancel = useCallback( - (id: GridRowId) => () => { - setRowModesModel((model) => ({ - ...model, - [id]: { mode: GridRowModes.View, ignoreModifications: true }, - })); - const editedRow = entries.find((entry) => entry.id === id); - if (editedRow?._isNew) { - setEntries((es) => es.filter((e) => e.id !== id)); - } - }, - [entries], - ); - - const handleDelete = useCallback( - (id: GridRowId) => () => { - setEntries((es) => es.filter((e) => e.id !== id)); - }, - [], - ); - - const handleSave = useCallback( - (id: GridRowId) => () => { - if (validateRow(id)) { - setRowModesModel((model) => ({ - ...model, - [id]: { mode: GridRowModes.View }, - })); - } - }, - [validateRow], - ); - - const handleEditStop = useCallback>( - (params, event) => { - if (!validateRow(params.id)) { - event.defaultMuiPrevented = true; - } - }, - [validateRow], - ); - - const processRowUpdate = useCallback((newRow: GridRowModel) => { - const updatedRow = { ...newRow, _isNew: false }; - setEntries((es) => es.map((e) => (e.id === newRow.id ? updatedRow : e))); - return updatedRow; - }, []); - - const columns = useMemo( - () => [ - { - type: "actions", - field: "actions", - headerName: t("Actions"), - getActions: ({ id }) => { - if (rowModesModel[id]?.mode === GridRowModes.Edit) { - return [ - } - label={t("Save")} - onClick={handleSave(id)} - />, - } - label={t("Cancel")} - onClick={handleCancel(id)} - />, - ]; - } - - return [ - } - label={t("Remove")} - onClick={handleDelete(id)} - />, - ]; - }, - }, - { - field: "projectId", - headerName: t("Project Code and Name"), - width: 300, - editable: true, - valueFormatter(params) { - const project = allProjects.find((p) => p.id === params.value); - return project ? `${project.code} - ${project.name}` : t("None"); - }, - renderEditCell(params: GridRenderEditCellParams) { - return ( - { - params.api.setEditCellValue({ - id: params.id, - field: params.field, - value: projectId, - }); - params.api.setCellFocus(params.id, "taskGroupId"); - }} - /> - ); - }, - }, - { - field: "taskGroupId", - headerName: t("Stage"), - width: 200, - editable: true, - renderEditCell(params: GridRenderEditCellParams) { - return ( - { - params.api.setEditCellValue({ - id: params.id, - field: params.field, - value: taskGroupId, - }); - params.api.setCellFocus(params.id, "taskId"); - }} - /> - ); - }, - valueFormatter(params) { - const taskGroups = params.id - ? taskGroupsByProject[params.api.getRow(params.id).projectId] || [] - : []; - const taskGroup = taskGroups.find((tg) => tg.value === params.value); - return taskGroup ? taskGroup.label : t("None"); - }, - }, - { - field: "taskId", - headerName: t("Task"), - width: 200, - editable: true, - renderEditCell(params: GridRenderEditCellParams) { - return ( - { - params.api.setEditCellValue({ - id: params.id, - field: params.field, - value: taskId, - }); - params.api.setCellFocus(params.id, "inputHours"); - }} - /> - ); - }, - valueFormatter(params) { - const projectId = params.id - ? params.api.getRow(params.id).projectId - : undefined; - - const task = projectId - ? allProjects - .find((p) => p.id === projectId) - ?.tasks.find((t) => t.id === params.value) - : undefined; - - return task ? task.name : t("None"); - }, - }, - { - field: "inputHours", - headerName: t("Hours"), - width: 100, - editable: true, - type: "number", - renderEditCell(params: GridRenderEditCellParams) { - const errorMessage = - params.row._error?.[params.field as keyof TimeEntry]; - const content = ; - return errorMessage ? ( - - {content} - - ) : ( - content - ); - }, - valueParser(value) { - return value ? roundToNearestQuarter(value) : value; - }, - valueFormatter(params) { - return manhourFormatter.format(params.value || 0); - }, - }, - { - field: "otHours", - headerName: t("Other Hours"), - width: 150, - editable: true, - type: "number", - renderEditCell(params: GridRenderEditCellParams) { - const errorMessage = - params.row._error?.[params.field as keyof TimeEntry]; - const content = ; - return errorMessage ? ( - - {content} - - ) : ( - content - ); - }, - valueParser(value) { - return value ? roundToNearestQuarter(value) : value; - }, - valueFormatter(params) { - return manhourFormatter.format(params.value || 0); - }, - }, - { - field: "remark", - headerName: t("Remark"), - sortable: false, - flex: 1, - editable: true, - renderEditCell(params: GridRenderEditCellParams) { - const errorMessage = - params.row._error?.[params.field as keyof TimeEntry]; - const content = ; - return errorMessage ? ( - - {content} - - ) : ( - content - ); - }, - }, - ], - [ - t, - rowModesModel, - handleDelete, - handleSave, - handleCancel, - assignedProjects, - allProjects, - taskGroupsByProject, - ], - ); - - useEffect(() => { - setValue(day, [ - ...entries - .filter((e) => !e._isNew && !e._error && e.id) - .map(({ isPlanned, _error, _isNew, ...entry }) => ({ - id: entry.id!, - ...entry, - })), - ]); - clearErrors(day); - }, [getValues, entries, setValue, day, clearErrors]); - - const hasOutOfPlannedStages = entries.some( - (entry) => entry.isPlanned !== undefined && !entry.isPlanned, - ); - - // Fast entry modal - const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false); - const closeFastEntryModal = useCallback(() => { - setFastEntryModalOpen(false); - }, []); - const openFastEntryModal = useCallback(() => { - setFastEntryModalOpen(true); - }, []); - const onSaveFastEntry = useCallback(async (entries: TimeEntry[]) => { - setEntries((e) => [...e, ...entries]); - setFastEntryModalOpen(false); - }, []); - - const footer = ( - - - {fastEntryEnabled && } - {hasOutOfPlannedStages && ( - - {t("There are entries for stages out of planned dates!")} - - )} - - ); - - return ( - <> - ) => { - let classname = ""; - if (params.row._error?.[params.field as keyof TimeEntry]) { - classname = "hasError"; - } else if ( - params.field === "taskGroupId" && - params.row.isPlanned !== undefined && - !params.row.isPlanned - ) { - classname = "hasWarning"; - } - return classname; - }} - slots={{ - footer: FooterToolbar, - noRowsOverlay: NoRowsOverlay, - }} - slotProps={{ - footer: { child: footer }, - }} - /> - {fastEntryEnabled && ( - - )} - - ); -}; - -const NoRowsOverlay: React.FC = () => { - const { t } = useTranslation("home"); - return ( - - {t("Add some time entries!")} - - ); -}; - -const FooterToolbar: React.FC = ({ child }) => { - return {child}; -}; - -export default EntryInputTable; diff --git a/src/components/TimesheetTable/MobileTimesheetEntry.tsx b/src/components/TimesheetTable/MobileTimesheetEntry.tsx deleted file mode 100644 index f080f29..0000000 --- a/src/components/TimesheetTable/MobileTimesheetEntry.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { TimeEntry, RecordTimesheetInput } from "@/app/api/timesheets/actions"; -import { shortDateFormatter } from "@/app/utils/formatUtil"; -import { Add } from "@mui/icons-material"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import dayjs from "dayjs"; -import React, { useCallback, useMemo, useState } from "react"; -import { useFormContext } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; -import TimesheetEditModal, { - Props as TimesheetEditModalProps, -} from "./TimesheetEditModal"; -import TimeEntryCard from "./TimeEntryCard"; -import { HolidaysResult } from "@/app/api/holidays"; -import { getHolidayForDate } from "@/app/utils/holidayUtils"; -import FastTimeEntryModal from "./FastTimeEntryModal"; - -interface Props { - date: string; - allProjects: ProjectWithTasks[]; - assignedProjects: AssignedProject[]; - companyHolidays: HolidaysResult[]; - fastEntryEnabled?: boolean; -} - -const MobileTimesheetEntry: React.FC = ({ - date, - allProjects, - assignedProjects, - companyHolidays, - fastEntryEnabled, -}) => { - const { - t, - i18n: { language }, - } = useTranslation("home"); - - const projectMap = useMemo(() => { - return allProjects.reduce<{ - [id: ProjectWithTasks["id"]]: ProjectWithTasks; - }>((acc, project) => { - return { ...acc, [project.id]: project }; - }, {}); - }, [allProjects]); - - const dayJsObj = dayjs(date); - const holiday = getHolidayForDate(date, companyHolidays); - const isHoliday = holiday || dayJsObj.day() === 0 || dayJsObj.day() === 6; - - const { watch, setValue, clearErrors } = - useFormContext(); - const currentEntries = watch(date); - - // Edit modal - const [editModalProps, setEditModalProps] = useState< - Partial - >({}); - const [editModalOpen, setEditModalOpen] = useState(false); - - const openEditModal = useCallback( - (defaultValues?: TimeEntry) => () => { - setEditModalProps({ - defaultValues: defaultValues ? { ...defaultValues } : undefined, - onDelete: defaultValues - ? async () => { - setValue( - date, - currentEntries.filter((entry) => entry.id !== defaultValues.id), - ); - clearErrors(date); - setEditModalOpen(false); - } - : undefined, - }); - setEditModalOpen(true); - }, - [clearErrors, currentEntries, date, setValue], - ); - - const closeEditModal = useCallback(() => { - setEditModalOpen(false); - }, []); - - const onSaveEntry = useCallback( - async (entry: TimeEntry) => { - const existingEntry = currentEntries.find((e) => e.id === entry.id); - if (existingEntry) { - setValue( - date, - currentEntries.map((e) => ({ - ...(e.id === existingEntry.id ? entry : e), - })), - ); - clearErrors(date); - } else { - setValue(date, [...currentEntries, entry]); - } - setEditModalOpen(false); - }, - [clearErrors, currentEntries, date, setValue], - ); - - // Fast entry modal - const [fastEntryModalOpen, setFastEntryModalOpen] = useState(false); - const closeFastEntryModal = useCallback(() => { - setFastEntryModalOpen(false); - }, []); - const openFastEntryModal = useCallback(() => { - setFastEntryModalOpen(true); - }, []); - const onSaveFastEntry = useCallback( - async (entries: TimeEntry[]) => { - setValue(date, [...currentEntries, ...entries]); - setFastEntryModalOpen(false); - }, - [currentEntries, date, setValue], - ); - - return ( - <> - - {shortDateFormatter(language).format(dayJsObj.toDate())} - {holiday && ( - {`(${holiday.title})`} - )} - - - {currentEntries.length ? ( - currentEntries.map((entry, index) => { - const project = entry.projectId - ? projectMap[entry.projectId] - : undefined; - - const task = project?.tasks.find((t) => t.id === entry.taskId); - - return ( - - ); - }) - ) : ( - - {t("Add some time entries!")} - - )} - - - {fastEntryEnabled && ( - - )} - - - {fastEntryEnabled && ( - - )} - - - ); -}; - -export default MobileTimesheetEntry; diff --git a/src/components/TimesheetTable/MobileTimesheetTable.tsx b/src/components/TimesheetTable/MobileTimesheetTable.tsx deleted file mode 100644 index bab4351..0000000 --- a/src/components/TimesheetTable/MobileTimesheetTable.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { - RecordLeaveInput, - RecordTimesheetInput, -} from "@/app/api/timesheets/actions"; -import React from "react"; -import { useFormContext } from "react-hook-form"; -import DateHoursList from "../DateHoursTable/DateHoursList"; -import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; -import MobileTimesheetEntry from "./MobileTimesheetEntry"; -import { HolidaysResult } from "@/app/api/holidays"; - -interface Props { - allProjects: ProjectWithTasks[]; - assignedProjects: AssignedProject[]; - leaveRecords: RecordLeaveInput; - companyHolidays: HolidaysResult[]; - errorComponent?: React.ReactNode; - fastEntryEnabled?: boolean; -} - -const MobileTimesheetTable: React.FC = ({ - allProjects, - assignedProjects, - leaveRecords, - companyHolidays, - errorComponent, - fastEntryEnabled, -}) => { - const { watch } = useFormContext(); - const currentInput = watch(); - const days = Object.keys(currentInput); - - return ( - - ); -}; - -export default MobileTimesheetTable; diff --git a/src/components/TimesheetTable/TimesheetTable.tsx b/src/components/TimesheetTable/TimesheetTable.tsx deleted file mode 100644 index 02f91df..0000000 --- a/src/components/TimesheetTable/TimesheetTable.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - RecordLeaveInput, - RecordTimesheetInput, -} from "@/app/api/timesheets/actions"; -import React from "react"; -import { useFormContext } from "react-hook-form"; -import EntryInputTable from "./EntryInputTable"; -import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; -import DateHoursTable from "../DateHoursTable"; -import { HolidaysResult } from "@/app/api/holidays"; - -interface Props { - allProjects: ProjectWithTasks[]; - assignedProjects: AssignedProject[]; - leaveRecords: RecordLeaveInput; - companyHolidays: HolidaysResult[]; - fastEntryEnabled?: boolean; -} - -const TimesheetTable: React.FC = ({ - allProjects, - assignedProjects, - leaveRecords, - companyHolidays, - fastEntryEnabled, -}) => { - const { watch } = useFormContext(); - const currentInput = watch(); - const days = Object.keys(currentInput); - - return ( - - ); -}; - -export default TimesheetTable; diff --git a/src/components/TimesheetTable/index.ts b/src/components/TimesheetTable/index.ts deleted file mode 100644 index 5d7b0ce..0000000 --- a/src/components/TimesheetTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TimesheetTable"; diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 1086405..b5f89d7 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -228,6 +228,7 @@ const UserWorkspacePage: React.FC = ({ teamTimesheets={teamTimesheets} open={isTimesheetAmendmentVisible} onClose={handleAmendmentClose} + miscTasks={miscTasks} /> )}