diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index e156c3c..0c7209c 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -2,7 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import "server-only"; -import { Task } from "../tasks"; +import { Task, TaskGroup } from "../tasks"; export interface ProjectResult { id: number; @@ -53,6 +53,12 @@ export interface AssignedProject { code: string; name: string; tasks: Task[]; + milestones: { + [taskGroupId: TaskGroup["id"]]: { + startDate: string; + endDate: string; + }; + }; } export const preloadProjects = () => { diff --git a/src/components/CreateProject/MilestoneSection.tsx b/src/components/CreateProject/MilestoneSection.tsx index e0089a7..9d18717 100644 --- a/src/components/CreateProject/MilestoneSection.tsx +++ b/src/components/CreateProject/MilestoneSection.tsx @@ -36,12 +36,6 @@ interface Props { taskGroupId: TaskGroup["id"]; } -declare module "@mui/x-data-grid" { - interface FooterPropsOverrides { - onAdd: () => void; - } -} - type PaymentRow = Partial; const MilestoneSection: React.FC = ({ taskGroupId }) => { @@ -218,6 +212,17 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { }); }, [getValues, payments, setValue, taskGroupId]); + const footer = ( + + ); + return ( @@ -301,7 +306,7 @@ const MilestoneSection: React.FC = ({ taskGroupId }) => { noRowsOverlay: NoRowsOverlay, }} slotProps={{ - footer: { onAdd: addRow }, + footer: { child: footer }, }} /> @@ -325,20 +330,8 @@ const NoRowsOverlay: React.FC = () => { ); }; -const FooterToolbar: React.FC = ({ onAdd }) => { - const { t } = useTranslation(); - return ( - - - - ); +const FooterToolbar: React.FC = ({ child }) => { + return {child}; }; export default MilestoneSection; diff --git a/src/components/StyledDataGrid/StyledDataGrid.tsx b/src/components/StyledDataGrid/StyledDataGrid.tsx index d8901c3..743d288 100644 --- a/src/components/StyledDataGrid/StyledDataGrid.tsx +++ b/src/components/StyledDataGrid/StyledDataGrid.tsx @@ -1,6 +1,13 @@ import { styled } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; +declare module "@mui/x-data-grid" { + interface FooterPropsOverrides { + onAdd?: () => void; + child?: React.ReactNode; + } +} + const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ "--unstable_DataGrid-radius": 0, "& .MuiDataGrid-columnHeaders": { diff --git a/src/components/TimesheetTable/EntryInputTable.tsx b/src/components/TimesheetTable/EntryInputTable.tsx index 5aa89bd..57a18b3 100644 --- a/src/components/TimesheetTable/EntryInputTable.tsx +++ b/src/components/TimesheetTable/EntryInputTable.tsx @@ -21,6 +21,7 @@ import { manhourFormatter } from "@/app/utils/formatUtil"; import { AssignedProject } from "@/app/api/projects"; import uniqBy from "lodash/uniqBy"; import { TaskGroup } from "@/app/api/tasks"; +import dayjs from "dayjs"; const mockProjects: AssignedProject[] = [ { @@ -47,6 +48,16 @@ const mockProjects: AssignedProject[] = [ }, }, ], + milestones: { + 1: { + startDate: "2000-01-01", + endDate: "2100-01-01", + }, + 2: { + startDate: "2100-01-01", + endDate: "2100-01-02", + }, + }, }, { id: 2, @@ -72,6 +83,16 @@ const mockProjects: AssignedProject[] = [ }, }, ], + milestones: { + 1: { + startDate: "2000-01-01", + endDate: "2100-01-01", + }, + 3: { + startDate: "2100-01-01", + endDate: "2100-01-02", + }, + }, }, { id: 3, @@ -97,6 +118,16 @@ const mockProjects: AssignedProject[] = [ }, }, ], + milestones: { + 1: { + startDate: "2000-01-01", + endDate: "2100-01-01", + }, + 4: { + startDate: "2100-01-01", + endDate: "2100-01-02", + }, + }, }, ]; @@ -104,6 +135,7 @@ type TimeEntryRow = Partial< TimeEntry & { _isNew: boolean; _error: string; + isPlanned: boolean; id: string; taskGroupId: number; } @@ -131,6 +163,15 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { }, {}); }, []); + // To check for start / end planned dates + const milestonesByProject = useMemo(() => { + return mockProjects.reduce<{ + [projectId: AssignedProject["id"]]: AssignedProject["milestones"]; + }>((acc, project) => { + return { ...acc, [project.id]: { ...project.milestones } }; + }, {}); + }, []); + const { getValues, setValue } = useFormContext(); const currentEntries = getValues(day); @@ -156,7 +197,9 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { id, "", ) as TimeEntryRow; - let error: keyof TimeEntry | "taskGroupId" | "" = ""; + + // Test for errrors + let error: keyof TimeEntry | "" = ""; if (!row.projectId) { error = "projectId"; } else if (!row.taskGroupId) { @@ -167,10 +210,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { error = "inputHours"; } - apiRef.current.updateRows([{ id, _error: error }]); + // Test for warnings + let isPlanned = false; + 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], + [apiRef, day, milestonesByProject], ); const handleCancel = useCallback( @@ -363,6 +420,29 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { ]); }, [getValues, entries, setValue, day]); + const hasOutOfPlannedStages = entries.some( + (entry) => entry.isPlanned !== undefined && !entry.isPlanned, + ); + + const footer = ( + + + {hasOutOfPlannedStages && ( + + {t("There are entries for stages out of planned dates!")} + + )} + + ); + return ( = ({ day }) => { border: "1px solid", borderColor: "error.main", }, + ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { + border: "1px solid", + borderColor: "warning.main", + }, }} disableColumnMenu editMode="row" @@ -383,14 +467,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => { processRowUpdate={processRowUpdate} columns={columns} getCellClassName={(params) => { - return params.row._error === params.field ? "hasError" : ""; + let classname = ""; + if (params.row._error === params.field) { + 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: { onAdd: addRow }, + footer: { child: footer }, }} /> ); @@ -410,21 +504,8 @@ const NoRowsOverlay: React.FC = () => { ); }; -const FooterToolbar: React.FC = ({ onAdd }) => { - const { t } = useTranslation(); - return ( - - - - ); +const FooterToolbar: React.FC = ({ child }) => { + return {child}; }; export default EntryInputTable; diff --git a/src/components/TimesheetTable/TimesheetTable.tsx b/src/components/TimesheetTable/TimesheetTable.tsx index f133cc2..b4b1557 100644 --- a/src/components/TimesheetTable/TimesheetTable.tsx +++ b/src/components/TimesheetTable/TimesheetTable.tsx @@ -11,6 +11,7 @@ import { TableContainer, TableHead, TableRow, + Typography, } from "@mui/material"; import dayjs from "dayjs"; import React, { useState } from "react"; @@ -53,6 +54,7 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({ entries, }) => { const { + t, i18n: { language }, } = useTranslation("home"); const dayJsObj = dayjs(day); @@ -80,6 +82,16 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({ 20 ? "error.main" : undefined }}> {manhourFormatter.format(totalHours)} + {totalHours > 20 && ( + + {t("(the daily total hours cannot be more than 20.)")} + + )} diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index ebadfd6..a26cbde 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -6,8 +6,6 @@ import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import { Add } from "@mui/icons-material"; import { Typography } from "@mui/material"; -import EnterTimesheetModal from "../EnterTimesheet/EnterTimesheetModal"; -import EnterLeaveModal from "../EnterLeave/EnterLeaveModal"; import ButtonGroup from "@mui/material/ButtonGroup"; import AssignedProjects from "./AssignedProjects"; import { ProjectHours } from "./UserWorkspaceWrapper"; @@ -65,14 +63,11 @@ const UserWorkspacePage: React.FC = ({ allProjects }) => { - - {/* */}