Kaynağa Gözat

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

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 yıl önce
ebeveyn
işleme
a16bee1ef0
6 değiştirilmiş dosya ile 143 ekleme ve 49 silme
  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 Dosyayı Görüntüle

@@ -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 = () => {


+ 14
- 21
src/components/CreateProject/MilestoneSection.tsx Dosyayı Görüntüle

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

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

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

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

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

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

+ 7
- 0
src/components/StyledDataGrid/StyledDataGrid.tsx Dosyayı Görüntüle

@@ -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": {


+ 101
- 20
src/components/TimesheetTable/EntryInputTable.tsx Dosyayı Görüntüle

@@ -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<RecordTimesheetInput>();
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 = (
<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 (
<StyledDataGrid
apiRef={apiRef}
@@ -373,6 +453,10 @@ const EntryInputTable: React.FC<{ day: string }> = ({ 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<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;

+ 12
- 0
src/components/TimesheetTable/TimesheetTable.tsx Dosyayı Görüntüle

@@ -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[] }> = ({
</TableCell>
<TableCell sx={{ color: totalHours > 20 ? "error.main" : undefined }}>
{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>
</TableRow>
<TableRow>


+ 2
- 7
src/components/UserWorkspacePage/UserWorkspacePage.tsx Dosyayı Görüntüle

@@ -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<Props> = ({ allProjects }) => {
</ButtonGroup>
</Stack>
</Stack>
<EnterTimesheetModal
<TimesheetModal
timesheetType="time"
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}
/>
{/* <EnterLeaveModal
isOpen={isLeaveModalVisible}
onClose={handleCloseLeaveModal}
/> */}
<TimesheetModal
timesheetType="leave"
isOpen={isLeaveModalVisible}


Yükleniyor…
İptal
Kaydet