瀏覽代碼

Add warnings for time entries out of planned start/date time

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 年之前
父節點
當前提交
a16bee1ef0
共有 6 個檔案被更改,包括 143 行新增49 行删除
  1. +7
    -1
      src/app/api/projects/index.ts
  2. +14
    -21
      src/components/CreateProject/MilestoneSection.tsx
  3. +7
    -0
      src/components/StyledDataGrid/StyledDataGrid.tsx
  4. +101
    -20
      src/components/TimesheetTable/EntryInputTable.tsx
  5. +12
    -0
      src/components/TimesheetTable/TimesheetTable.tsx
  6. +2
    -7
      src/components/UserWorkspacePage/UserWorkspacePage.tsx

+ 7
- 1
src/app/api/projects/index.ts 查看文件

@@ -2,7 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api"; import { BASE_API_URL } from "@/config/api";
import { cache } from "react"; import { cache } from "react";
import "server-only"; import "server-only";
import { Task } from "../tasks";
import { Task, TaskGroup } from "../tasks";


export interface ProjectResult { export interface ProjectResult {
id: number; id: number;
@@ -53,6 +53,12 @@ export interface AssignedProject {
code: string; code: string;
name: string; name: string;
tasks: Task[]; tasks: Task[];
milestones: {
[taskGroupId: TaskGroup["id"]]: {
startDate: string;
endDate: string;
};
};
} }


export const preloadProjects = () => { export const preloadProjects = () => {


+ 14
- 21
src/components/CreateProject/MilestoneSection.tsx 查看文件

@@ -36,12 +36,6 @@ interface Props {
taskGroupId: TaskGroup["id"]; taskGroupId: TaskGroup["id"];
} }


declare module "@mui/x-data-grid" {
interface FooterPropsOverrides {
onAdd: () => void;
}
}

type PaymentRow = Partial<PaymentInputs & { _isNew: boolean; _error: string }>; type PaymentRow = Partial<PaymentInputs & { _isNew: boolean; _error: string }>;


const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
@@ -218,6 +212,17 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
}); });
}, [getValues, payments, setValue, taskGroupId]); }, [getValues, payments, setValue, taskGroupId]);


const footer = (
<Button
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Add Payment Milestone")}
</Button>
);

return ( return (
<Stack gap={1}> <Stack gap={1}>
<Typography variant="overline" display="block" marginBlockEnd={1}> <Typography variant="overline" display="block" marginBlockEnd={1}>
@@ -301,7 +306,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
noRowsOverlay: NoRowsOverlay, noRowsOverlay: NoRowsOverlay,
}} }}
slotProps={{ slotProps={{
footer: { onAdd: addRow },
footer: { child: footer },
}} }}
/> />
</Box> </Box>
@@ -325,20 +330,8 @@ const NoRowsOverlay: React.FC = () => {
); );
}; };


const FooterToolbar: React.FC<FooterPropsOverrides> = ({ onAdd }) => {
const { t } = useTranslation();
return (
<GridToolbarContainer sx={{ p: 2 }}>
<Button
variant="outlined"
startIcon={<Add />}
onClick={onAdd}
size="small"
>
{t("Add Payment Milestone")}
</Button>
</GridToolbarContainer>
);
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
}; };


export default MilestoneSection; export default MilestoneSection;

+ 7
- 0
src/components/StyledDataGrid/StyledDataGrid.tsx 查看文件

@@ -1,6 +1,13 @@
import { styled } from "@mui/material"; import { styled } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid"; 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 }) => ({ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
"--unstable_DataGrid-radius": 0, "--unstable_DataGrid-radius": 0,
"& .MuiDataGrid-columnHeaders": { "& .MuiDataGrid-columnHeaders": {


+ 101
- 20
src/components/TimesheetTable/EntryInputTable.tsx 查看文件

@@ -21,6 +21,7 @@ import { manhourFormatter } from "@/app/utils/formatUtil";
import { AssignedProject } from "@/app/api/projects"; import { AssignedProject } from "@/app/api/projects";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import { TaskGroup } from "@/app/api/tasks"; import { TaskGroup } from "@/app/api/tasks";
import dayjs from "dayjs";


const mockProjects: AssignedProject[] = [ 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, 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, 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 & { TimeEntry & {
_isNew: boolean; _isNew: boolean;
_error: string; _error: string;
isPlanned: boolean;
id: string; id: string;
taskGroupId: number; 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<RecordTimesheetInput>(); const { getValues, setValue } = useFormContext<RecordTimesheetInput>();
const currentEntries = getValues(day); const currentEntries = getValues(day);


@@ -156,7 +197,9 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
id, id,
"", "",
) as TimeEntryRow; ) as TimeEntryRow;
let error: keyof TimeEntry | "taskGroupId" | "" = "";

// Test for errrors
let error: keyof TimeEntry | "" = "";
if (!row.projectId) { if (!row.projectId) {
error = "projectId"; error = "projectId";
} else if (!row.taskGroupId) { } else if (!row.taskGroupId) {
@@ -167,10 +210,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
error = "inputHours"; 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; return !error;
}, },
[apiRef],
[apiRef, day, milestonesByProject],
); );


const handleCancel = useCallback( const handleCancel = useCallback(
@@ -363,6 +420,29 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
]); ]);
}, [getValues, entries, setValue, day]); }, [getValues, entries, setValue, day]);


const hasOutOfPlannedStages = entries.some(
(entry) => entry.isPlanned !== undefined && !entry.isPlanned,
);

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Record time")}
</Button>
{hasOutOfPlannedStages && (
<Typography color="warning.main" variant="body2">
{t("There are entries for stages out of planned dates!")}
</Typography>
)}
</Box>
);

return ( return (
<StyledDataGrid <StyledDataGrid
apiRef={apiRef} apiRef={apiRef}
@@ -373,6 +453,10 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
border: "1px solid", border: "1px solid",
borderColor: "error.main", borderColor: "error.main",
}, },
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
}} }}
disableColumnMenu disableColumnMenu
editMode="row" editMode="row"
@@ -383,14 +467,24 @@ const EntryInputTable: React.FC<{ day: string }> = ({ day }) => {
processRowUpdate={processRowUpdate} processRowUpdate={processRowUpdate}
columns={columns} columns={columns}
getCellClassName={(params) => { 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={{ slots={{
footer: FooterToolbar, footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay, noRowsOverlay: NoRowsOverlay,
}} }}
slotProps={{ slotProps={{
footer: { onAdd: addRow },
footer: { child: footer },
}} }}
/> />
); );
@@ -410,21 +504,8 @@ const NoRowsOverlay: React.FC = () => {
); );
}; };


const FooterToolbar: React.FC<FooterPropsOverrides> = ({ onAdd }) => {
const { t } = useTranslation();
return (
<GridToolbarContainer sx={{ p: 2 }}>
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={onAdd}
size="small"
>
{t("Record time")}
</Button>
</GridToolbarContainer>
);
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
}; };


export default EntryInputTable; export default EntryInputTable;

+ 12
- 0
src/components/TimesheetTable/TimesheetTable.tsx 查看文件

@@ -11,6 +11,7 @@ import {
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
Typography,
} from "@mui/material"; } from "@mui/material";
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -53,6 +54,7 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({
entries, entries,
}) => { }) => {
const { const {
t,
i18n: { language }, i18n: { language },
} = useTranslation("home"); } = useTranslation("home");
const dayJsObj = dayjs(day); const dayJsObj = dayjs(day);
@@ -80,6 +82,16 @@ const DayRow: React.FC<{ day: string; entries: TimeEntry[] }> = ({
</TableCell> </TableCell>
<TableCell sx={{ color: totalHours > 20 ? "error.main" : undefined }}> <TableCell sx={{ color: totalHours > 20 ? "error.main" : undefined }}>
{manhourFormatter.format(totalHours)} {manhourFormatter.format(totalHours)}
{totalHours > 20 && (
<Typography
color="error.main"
variant="body2"
component="span"
sx={{ marginInlineStart: 1 }}
>
{t("(the daily total hours cannot be more than 20.)")}
</Typography>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>


+ 2
- 7
src/components/UserWorkspacePage/UserWorkspacePage.tsx 查看文件

@@ -6,8 +6,6 @@ import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { Add } from "@mui/icons-material"; import { Add } from "@mui/icons-material";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import EnterTimesheetModal from "../EnterTimesheet/EnterTimesheetModal";
import EnterLeaveModal from "../EnterLeave/EnterLeaveModal";
import ButtonGroup from "@mui/material/ButtonGroup"; import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects"; import AssignedProjects from "./AssignedProjects";
import { ProjectHours } from "./UserWorkspaceWrapper"; import { ProjectHours } from "./UserWorkspaceWrapper";
@@ -65,14 +63,11 @@ const UserWorkspacePage: React.FC<Props> = ({ allProjects }) => {
</ButtonGroup> </ButtonGroup>
</Stack> </Stack>
</Stack> </Stack>
<EnterTimesheetModal
<TimesheetModal
timesheetType="time"
isOpen={isTimeheetModalVisible} isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal} onClose={handleCloseTimesheetModal}
/> />
{/* <EnterLeaveModal
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
/> */}
<TimesheetModal <TimesheetModal
timesheetType="leave" timesheetType="leave"
isOpen={isLeaveModalVisible} isOpen={isLeaveModalVisible}


Loading…
取消
儲存