Просмотр исходного кода

Update timesheet entry

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 год назад
Родитель
Сommit
61d7f400ef
14 измененных файлов: 464 добавлений и 119 удалений
  1. +5
    -1
      src/app/(main)/home/page.tsx
  2. +13
    -1
      src/app/api/projects/index.ts
  3. +5
    -3
      src/app/api/timesheets/actions.ts
  4. +44
    -0
      src/app/api/timesheets/utils.ts
  5. +10
    -7
      src/components/LeaveTable/LeaveEntryTable.tsx
  6. +9
    -5
      src/components/LeaveTable/LeaveTable.tsx
  7. +7
    -2
      src/components/TimesheetModal/TimesheetModal.tsx
  8. +102
    -85
      src/components/TimesheetTable/EntryInputTable.tsx
  9. +89
    -0
      src/components/TimesheetTable/ProjectSelect.tsx
  10. +69
    -0
      src/components/TimesheetTable/TaskGroupSelect.tsx
  11. +72
    -0
      src/components/TimesheetTable/TaskSelect.tsx
  12. +22
    -7
      src/components/TimesheetTable/TimesheetTable.tsx
  13. +4
    -1
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  14. +13
    -7
      src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx

+ 5
- 1
src/app/(main)/home/page.tsx Просмотреть файл

@@ -8,7 +8,10 @@ import {
} from "@/app/api/timesheets"; } from "@/app/api/timesheets";
import { authOptions } from "@/config/authConfig"; import { authOptions } from "@/config/authConfig";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { fetchAssignedProjects } from "@/app/api/projects";
import {
fetchAssignedProjects,
fetchProjectWithTasks,
} from "@/app/api/projects";


export const metadata: Metadata = { export const metadata: Metadata = {
title: "User Workspace", title: "User Workspace",
@@ -23,6 +26,7 @@ const Home: React.FC = async () => {
fetchAssignedProjects(username); fetchAssignedProjects(username);
fetchLeaves(username); fetchLeaves(username);
fetchLeaveTypes(); fetchLeaveTypes();
fetchProjectWithTasks();


return ( return (
<I18nProvider namespaces={["home"]}> <I18nProvider namespaces={["home"]}>


+ 13
- 1
src/app/api/projects/index.ts Просмотреть файл

@@ -49,7 +49,7 @@ export interface WorkNature {
name: string; name: string;
} }


export interface AssignedProject {
export interface ProjectWithTasks {
id: number; id: number;
code: string; code: string;
name: string; name: string;
@@ -60,6 +60,9 @@ export interface AssignedProject {
endDate?: string; endDate?: string;
}; };
}; };
}

export interface AssignedProject extends ProjectWithTasks {
// Manhour info // Manhour info
hoursSpent: number; hoursSpent: number;
hoursSpentOther: number; hoursSpentOther: number;
@@ -147,6 +150,15 @@ export const fetchAssignedProjects = cache(async (username: string) => {
); );
}); });


export const fetchProjectWithTasks = cache(async () => {
return serverFetchJson<ProjectWithTasks[]>(
`${BASE_API_URL}/projects/allProjectWithTasks`,
{
next: { tags: ["allProjectWithTasks"] },
},
);
});

export const fetchProjectDetails = cache(async (projectId: string) => { export const fetchProjectDetails = cache(async (projectId: string) => {
return serverFetchJson<CreateProjectInputs>( return serverFetchJson<CreateProjectInputs>(
`${BASE_API_URL}/projects/projectDetails/${projectId}`, `${BASE_API_URL}/projects/projectDetails/${projectId}`,


+ 5
- 3
src/app/api/timesheets/actions.ts Просмотреть файл

@@ -8,10 +8,11 @@ import { revalidateTag } from "next/cache";


export interface TimeEntry { export interface TimeEntry {
id: number; id: number;
projectId: ProjectResult["id"];
taskGroupId: TaskGroup["id"];
taskId: Task["id"];
projectId?: ProjectResult["id"];
taskGroupId?: TaskGroup["id"];
taskId?: Task["id"];
inputHours: number; inputHours: number;
remark?: string;
} }


export interface RecordTimesheetInput { export interface RecordTimesheetInput {
@@ -22,6 +23,7 @@ export interface LeaveEntry {
id: number; id: number;
inputHours: number; inputHours: number;
leaveTypeId: number; leaveTypeId: number;
remark?: string;
} }


export interface RecordLeaveInput { export interface RecordLeaveInput {


+ 44
- 0
src/app/api/timesheets/utils.ts Просмотреть файл

@@ -0,0 +1,44 @@
import { LeaveEntry, TimeEntry } from "./actions";

/**
* @param entry - the time entry
* @returns the field where there is an error, or an empty string if there is none
*/
export const isValidTimeEntry = (entry: Partial<TimeEntry>): string => {
// Test for errors
let error: keyof TimeEntry | "" = "";

// If there is a project id, there should also be taskGroupId, taskId, inputHours
if (entry.projectId) {
if (!entry.taskGroupId) {
error = "taskGroupId";
} else if (!entry.taskId) {
error = "taskId";
} else if (!entry.inputHours || !(entry.inputHours >= 0)) {
error = "inputHours";
}
} else {
if (!entry.inputHours || !(entry.inputHours >= 0)) {
error = "inputHours";
} else if (!entry.remark) {
error = "remark";
}
}

return error;
};

export const isValidLeaveEntry = (entry: Partial<LeaveEntry>): string => {
// Test for errrors
let error: keyof LeaveEntry | "" = "";
if (!entry.leaveTypeId) {
error = "leaveTypeId";
} else if (!entry.inputHours || !(entry.inputHours >= 0)) {
error = "inputHours";
}

return error;
};

export const LEAVE_DAILY_MAX_HOURS = 8;
export const TIMESHEET_DAILY_MAX_HOURS = 20;

+ 10
- 7
src/components/LeaveTable/LeaveEntryTable.tsx Просмотреть файл

@@ -21,6 +21,7 @@ import { manhourFormatter } from "@/app/utils/formatUtil";
import dayjs from "dayjs"; import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import { LeaveType } from "@/app/api/timesheets"; import { LeaveType } from "@/app/api/timesheets";
import { isValidLeaveEntry } from "@/app/api/timesheets/utils";


dayjs.extend(isBetween); dayjs.extend(isBetween);


@@ -63,13 +64,7 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => {
"", "",
) as LeaveEntryRow; ) as LeaveEntryRow;


// Test for errrors
let error: keyof LeaveEntry | "" = "";
if (!row.leaveTypeId) {
error = "leaveTypeId";
} else if (!row.inputHours || !(row.inputHours >= 0)) {
error = "inputHours";
}
const error = isValidLeaveEntry(row);


apiRef.current.updateRows([{ id, _error: error }]); apiRef.current.updateRows([{ id, _error: error }]);
return !error; return !error;
@@ -182,6 +177,13 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => {
return manhourFormatter.format(params.value); return manhourFormatter.format(params.value);
}, },
}, },
{
field: "remark",
headerName: t("Remark"),
sortable: false,
flex: 1,
editable: true,
},
], ],
[t, rowModesModel, handleDelete, handleSave, handleCancel, leaveTypes], [t, rowModesModel, handleDelete, handleSave, handleCancel, leaveTypes],
); );
@@ -197,6 +199,7 @@ const EntryInputTable: React.FC<Props> = ({ day, leaveTypes }) => {
id: e.id!, id: e.id!,
inputHours: e.inputHours!, inputHours: e.inputHours!,
leaveTypeId: e.leaveTypeId!, leaveTypeId: e.leaveTypeId!,
remark: e.remark,
})), })),
]); ]);
}, [getValues, entries, setValue, day]); }, [getValues, entries, setValue, day]);


+ 9
- 5
src/components/LeaveTable/LeaveTable.tsx Просмотреть файл

@@ -19,13 +19,12 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LeaveEntryTable from "./LeaveEntryTable"; import LeaveEntryTable from "./LeaveEntryTable";
import { LeaveType } from "@/app/api/timesheets"; import { LeaveType } from "@/app/api/timesheets";
import { LEAVE_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils";


interface Props { interface Props {
leaveTypes: LeaveType[]; leaveTypes: LeaveType[];
} }


const MAX_HOURS = 8;

const LeaveTable: React.FC<Props> = ({ leaveTypes }) => { const LeaveTable: React.FC<Props> = ({ leaveTypes }) => {
const { t } = useTranslation("home"); const { t } = useTranslation("home");


@@ -94,17 +93,22 @@ const DayRow: React.FC<{
{shortDateFormatter(language).format(dayJsObj.toDate())} {shortDateFormatter(language).format(dayJsObj.toDate())}
</TableCell> </TableCell>
<TableCell <TableCell
sx={{ color: totalHours > MAX_HOURS ? "error.main" : undefined }}
sx={{
color:
totalHours > LEAVE_DAILY_MAX_HOURS ? "error.main" : undefined,
}}
> >
{manhourFormatter.format(totalHours)} {manhourFormatter.format(totalHours)}
{totalHours > MAX_HOURS && (
{totalHours > LEAVE_DAILY_MAX_HOURS && (
<Typography <Typography
color="error.main" color="error.main"
variant="body2" variant="body2"
component="span" component="span"
sx={{ marginInlineStart: 1 }} sx={{ marginInlineStart: 1 }}
> >
{t("(the daily total hours cannot be more than 8.)")}
{t("(the daily total hours cannot be more than {{hours}})", {
hours: LEAVE_DAILY_MAX_HOURS,
})}
</Typography> </Typography>
)} )}
</TableCell> </TableCell>


+ 7
- 2
src/components/TimesheetModal/TimesheetModal.tsx Просмотреть файл

@@ -19,11 +19,12 @@ import {
} from "@/app/api/timesheets/actions"; } from "@/app/api/timesheets/actions";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { AssignedProject } from "@/app/api/projects";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";


interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
username: string; username: string;
defaultTimesheets?: RecordTimesheetInput; defaultTimesheets?: RecordTimesheetInput;
@@ -42,6 +43,7 @@ const modalSx: SxProps = {
const TimesheetModal: React.FC<Props> = ({ const TimesheetModal: React.FC<Props> = ({
isOpen, isOpen,
onClose, onClose,
allProjects,
assignedProjects, assignedProjects,
username, username,
defaultTimesheets, defaultTimesheets,
@@ -106,7 +108,10 @@ const TimesheetModal: React.FC<Props> = ({
marginBlock: 4, marginBlock: 4,
}} }}
> >
<TimesheetTable assignedProjects={assignedProjects} />
<TimesheetTable
assignedProjects={assignedProjects}
allProjects={allProjects}
/>
</Box> </Box>
<CardActions sx={{ justifyContent: "flex-end" }}> <CardActions sx={{ justifyContent: "flex-end" }}>
<Button <Button


+ 102
- 85
src/components/TimesheetTable/EntryInputTable.tsx Просмотреть файл

@@ -5,6 +5,7 @@ import {
GridActionsCellItem, GridActionsCellItem,
GridColDef, GridColDef,
GridEventListener, GridEventListener,
GridRenderEditCellParams,
GridRowId, GridRowId,
GridRowModel, GridRowModel,
GridRowModes, GridRowModes,
@@ -18,31 +19,40 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions"; import { RecordTimesheetInput, TimeEntry } from "@/app/api/timesheets/actions";
import { manhourFormatter } from "@/app/utils/formatUtil"; import { manhourFormatter } from "@/app/utils/formatUtil";
import { AssignedProject } from "@/app/api/projects";
import { AssignedProject, ProjectWithTasks } 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"; import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween"; import isBetween from "dayjs/plugin/isBetween";
import ProjectSelect from "./ProjectSelect";
import TaskGroupSelect from "./TaskGroupSelect";
import TaskSelect from "./TaskSelect";
import { isValidTimeEntry } from "@/app/api/timesheets/utils";


dayjs.extend(isBetween); dayjs.extend(isBetween);


interface Props { interface Props {
day: string; day: string;
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
} }


type TimeEntryRow = Partial<
export type TimeEntryRow = Partial<
TimeEntry & { TimeEntry & {
_isNew: boolean; _isNew: boolean;
_error: string; _error: string;
isPlanned: boolean;
isPlanned?: boolean;
} }
>; >;


const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
const EntryInputTable: React.FC<Props> = ({
day,
allProjects,
assignedProjects,
}) => {
const { t } = useTranslation("home"); const { t } = useTranslation("home");
const taskGroupsByProject = useMemo(() => { const taskGroupsByProject = useMemo(() => {
return assignedProjects.reduce<{
return allProjects.reduce<{
[projectId: AssignedProject["id"]]: { [projectId: AssignedProject["id"]]: {
value: TaskGroup["id"]; value: TaskGroup["id"];
label: string; label: string;
@@ -59,7 +69,7 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
), ),
}; };
}, {}); }, {});
}, [assignedProjects]);
}, [allProjects]);


// To check for start / end planned dates // To check for start / end planned dates
const milestonesByProject = useMemo(() => { const milestonesByProject = useMemo(() => {
@@ -94,20 +104,10 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
"", "",
) as TimeEntryRow; ) as TimeEntryRow;


// Test for errrors
let error: keyof TimeEntry | "" = "";
if (!row.projectId) {
error = "projectId";
} else if (!row.taskGroupId) {
error = "taskGroupId";
} else if (!row.taskId) {
error = "taskId";
} else if (!row.inputHours || !(row.inputHours >= 0)) {
error = "inputHours";
}
const error = isValidTimeEntry(row);


// Test for warnings // Test for warnings
let isPlanned = false;
let isPlanned;
if ( if (
row.projectId && row.projectId &&
row.taskGroupId && row.taskGroupId &&
@@ -211,14 +211,28 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
{ {
field: "projectId", field: "projectId",
headerName: t("Project Code and Name"), headerName: t("Project Code and Name"),
width: 200,
width: 400,
editable: true, editable: true,
type: "singleSelect",
valueOptions() {
return assignedProjects.map((p) => ({ value: p.id, label: p.name }));
valueFormatter(params) {
const project = assignedProjects.find((p) => p.id === params.value);
return project ? `${project.code} - ${project.name}` : t("None");
}, },
valueGetter({ value }) {
return value ?? "";
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) {
return (
<ProjectSelect
allProjects={allProjects}
assignedProjects={assignedProjects}
value={params.value}
onProjectSelect={(projectId) => {
params.api.setEditCellValue({
id: params.id,
field: params.field,
value: projectId,
});
params.api.setCellFocus(params.id, "taskGroupId");
}}
/>
);
}, },
}, },
{ {
@@ -226,27 +240,29 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
headerName: t("Stage"), headerName: t("Stage"),
width: 200, width: 200,
editable: true, editable: true,
type: "singleSelect",
valueGetter({ value }) {
return value ?? "";
},
valueOptions(params) {
const updatedRow = params.id
? apiRef.current.getRowWithUpdatedValues(params.id, "")
: null;
if (!updatedRow) {
return [];
}

const projectInfo = assignedProjects.find(
(p) => p.id === updatedRow.projectId,
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) {
return (
<TaskGroupSelect
projectId={params.row.projectId}
value={params.value}
taskGroupsByProject={taskGroupsByProject}
onTaskGroupSelect={(taskGroupId) => {
params.api.setEditCellValue({
id: params.id,
field: params.field,
value: taskGroupId,
});
params.api.setCellFocus(params.id, "taskId");
}}
/>
); );

if (!projectInfo) {
return [];
}

return taskGroupsByProject[projectInfo.id];
},
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");
}, },
}, },
{ {
@@ -254,32 +270,37 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
headerName: t("Task"), headerName: t("Task"),
width: 200, width: 200,
editable: true, editable: true,
type: "singleSelect",
valueGetter({ value }) {
return value ?? "";
},
valueOptions(params) {
const updatedRow = params.id
? apiRef.current.getRowWithUpdatedValues(params.id, "")
: null;
if (!updatedRow) {
return [];
}

const projectInfo = assignedProjects.find(
(p) => p.id === updatedRow.projectId,
renderEditCell(params: GridRenderEditCellParams<TimeEntryRow, number>) {
return (
<TaskSelect
value={params.value}
projectId={params.row.projectId}
taskGroupId={params.row.taskGroupId}
allProjects={allProjects}
editCellProps={params}
onTaskSelect={(taskId) => {
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;


if (!projectInfo) {
return [];
}
const task = projectId
? assignedProjects
.find((p) => p.id === projectId)
?.tasks.find((t) => t.id === params.value)
: undefined;


return projectInfo.tasks
.filter((t) => t.taskGroup.id === updatedRow.taskGroupId)
.map((t) => ({
value: t.id,
label: t.name,
}));
return task ? task.name : t("None");
}, },
}, },
{ {
@@ -292,6 +313,13 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
return manhourFormatter.format(params.value); return manhourFormatter.format(params.value);
}, },
}, },
{
field: "remark",
headerName: t("Remark"),
sortable: false,
flex: 1,
editable: true,
},
], ],
[ [
t, t,
@@ -299,31 +327,20 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
handleDelete, handleDelete,
handleSave, handleSave,
handleCancel, handleCancel,
apiRef,
taskGroupsByProject,
assignedProjects, assignedProjects,
allProjects,
taskGroupsByProject,
], ],
); );


useEffect(() => { useEffect(() => {
setValue(day, [ setValue(day, [
...entries ...entries
.filter(
(e) =>
!e._isNew &&
!e._error &&
e.inputHours &&
e.projectId &&
e.taskId &&
e.taskGroupId &&
e.id,
)
.map((e) => ({
id: e.id!,
inputHours: e.inputHours!,
projectId: e.projectId!,
taskId: e.taskId!,
taskGroupId: e.taskGroupId!,
.filter((e) => !e._isNew && !e._error && e.id && e.inputHours)
.map(({ isPlanned, _error, _isNew, ...entry }) => ({
id: entry.id!,
inputHours: entry.inputHours!,
...entry,
})), })),
]); ]);
}, [getValues, entries, setValue, day]); }, [getValues, entries, setValue, day]);


+ 89
- 0
src/components/TimesheetTable/ProjectSelect.tsx Просмотреть файл

@@ -0,0 +1,89 @@
import React, { useCallback, useMemo } from "react";
import {
ListSubheader,
MenuItem,
Select,
SelectChangeEvent,
} from "@mui/material";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { useTranslation } from "react-i18next";
import differenceBy from "lodash/differenceBy";

interface Props {
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
value: number | undefined;
onProjectSelect: (projectId: number | string) => void;
}

const ProjectSelect: React.FC<Props> = ({
allProjects,
assignedProjects,
value,
onProjectSelect,
}) => {
const { t } = useTranslation("home");

const nonAssignedProjects = useMemo(() => {
return differenceBy(allProjects, assignedProjects, "id");
}, [allProjects, assignedProjects]);

const onChange = useCallback(
(event: SelectChangeEvent<number>) => {
const newValue = event.target.value;
onProjectSelect(newValue);
},
[onProjectSelect],
);

return (
<Select
displayEmpty
value={value || ""}
onChange={onChange}
sx={{ width: "100%" }}
MenuProps={{
slotProps: {
paper: {
sx: { maxHeight: 400 },
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
>
<ListSubheader>{t("Non-billable")}</ListSubheader>
<MenuItem value={""}>{t("None")}</MenuItem>
{assignedProjects.length > 0 && [
<ListSubheader key="assignedProjectsSubHeader">
{t("Assigned Projects")}
</ListSubheader>,
...assignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
{nonAssignedProjects.length > 0 && [
<ListSubheader key="nonAssignedProjectsSubHeader">
{t("Non-assigned Projects")}
</ListSubheader>,
...nonAssignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
</Select>
);
};

export default ProjectSelect;

+ 69
- 0
src/components/TimesheetTable/TaskGroupSelect.tsx Просмотреть файл

@@ -0,0 +1,69 @@
import React, { useCallback } from "react";
import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
import { useTranslation } from "react-i18next";
import { TaskGroup } from "@/app/api/tasks";

interface Props {
taskGroupsByProject: {
[projectId: number]: {
value: TaskGroup["id"];
label: string;
}[];
};
projectId: number | undefined;
value: number | undefined;
onTaskGroupSelect: (taskGroupId: number | string) => void;
}

const TaskGroupSelect: React.FC<Props> = ({
value,
projectId,
onTaskGroupSelect,
taskGroupsByProject,
}) => {
const { t } = useTranslation("home");

const taskGroups = projectId ? taskGroupsByProject[projectId] : [];

const onChange = useCallback(
(event: SelectChangeEvent<number>) => {
const newValue = event.target.value;
onTaskGroupSelect(newValue);
},
[onTaskGroupSelect],
);

return (
<Select
displayEmpty
disabled={taskGroups.length === 0}
value={value || ""}
onChange={onChange}
sx={{ width: "100%" }}
MenuProps={{
slotProps: {
paper: {
sx: { maxHeight: 400 },
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
>
{taskGroups.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>}
{taskGroups.map((taskGroup) => (
<MenuItem key={taskGroup.value} value={taskGroup.value}>
{taskGroup.label}
</MenuItem>
))}
</Select>
);
};

export default TaskGroupSelect;

+ 72
- 0
src/components/TimesheetTable/TaskSelect.tsx Просмотреть файл

@@ -0,0 +1,72 @@
import React, { useCallback } from "react";
import { MenuItem, Select, SelectChangeEvent } from "@mui/material";
import { GridRenderEditCellParams } from "@mui/x-data-grid";
import { TimeEntryRow } from "./EntryInputTable";
import { useTranslation } from "react-i18next";
import { ProjectWithTasks } from "@/app/api/projects";

interface Props {
allProjects: ProjectWithTasks[];
value: number | undefined;
projectId: number | undefined;
taskGroupId: number | undefined;
editCellProps: GridRenderEditCellParams<TimeEntryRow, number>;
onTaskSelect: (taskId: number | string) => void;
}

const TaskSelect: React.FC<Props> = ({
value,
allProjects,
projectId,
taskGroupId,
onTaskSelect,
}) => {
const { t } = useTranslation("home");

const project = allProjects.find((p) => p.id === projectId);
const tasks = project
? project.tasks.filter((task) => task.taskGroup.id === taskGroupId)
: [];

const onChange = useCallback(
(event: SelectChangeEvent<number>) => {
const newValue = event.target.value;
onTaskSelect(newValue);
},
[onTaskSelect],
);

return (
<Select
displayEmpty
disabled={tasks.length === 0}
value={value || ""}
onChange={onChange}
sx={{ width: "100%" }}
MenuProps={{
slotProps: {
paper: {
sx: { maxHeight: 400 },
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
}}
>
{tasks.length === 0 && <MenuItem value={""}>{t("None")}</MenuItem>}
{tasks.map((task) => (
<MenuItem key={task.id} value={task.id}>
{task.name}
</MenuItem>
))}
</Select>
);
};

export default TaskSelect;

+ 22
- 7
src/components/TimesheetTable/TimesheetTable.tsx Просмотреть файл

@@ -18,13 +18,15 @@ import React, { useState } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EntryInputTable from "./EntryInputTable"; import EntryInputTable from "./EntryInputTable";
import { AssignedProject } from "@/app/api/projects";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { TIMESHEET_DAILY_MAX_HOURS } from "@/app/api/timesheets/utils";


interface Props { interface Props {
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
} }


const TimesheetTable: React.FC<Props> = ({ assignedProjects }) => {
const TimesheetTable: React.FC<Props> = ({ allProjects, assignedProjects }) => {
const { t } = useTranslation("home"); const { t } = useTranslation("home");


const { watch } = useFormContext<RecordTimesheetInput>(); const { watch } = useFormContext<RecordTimesheetInput>();
@@ -49,6 +51,7 @@ const TimesheetTable: React.FC<Props> = ({ assignedProjects }) => {
key={`${day}${index}`} key={`${day}${index}`}
day={day} day={day}
entries={entries} entries={entries}
allProjects={allProjects}
assignedProjects={assignedProjects} assignedProjects={assignedProjects}
/> />
); );
@@ -62,8 +65,9 @@ const TimesheetTable: React.FC<Props> = ({ assignedProjects }) => {
const DayRow: React.FC<{ const DayRow: React.FC<{
day: string; day: string;
entries: TimeEntry[]; entries: TimeEntry[];
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
}> = ({ day, entries, assignedProjects }) => {
}> = ({ day, entries, allProjects, assignedProjects }) => {
const { const {
t, t,
i18n: { language }, i18n: { language },
@@ -91,16 +95,23 @@ const DayRow: React.FC<{
> >
{shortDateFormatter(language).format(dayJsObj.toDate())} {shortDateFormatter(language).format(dayJsObj.toDate())}
</TableCell> </TableCell>
<TableCell sx={{ color: totalHours > 20 ? "error.main" : undefined }}>
<TableCell
sx={{
color:
totalHours > TIMESHEET_DAILY_MAX_HOURS ? "error.main" : undefined,
}}
>
{manhourFormatter.format(totalHours)} {manhourFormatter.format(totalHours)}
{totalHours > 20 && (
{totalHours > TIMESHEET_DAILY_MAX_HOURS && (
<Typography <Typography
color="error.main" color="error.main"
variant="body2" variant="body2"
component="span" component="span"
sx={{ marginInlineStart: 1 }} sx={{ marginInlineStart: 1 }}
> >
{t("(the daily total hours cannot be more than 20.)")}
{t("(the daily total hours cannot be more than {{hours}})", {
hours: TIMESHEET_DAILY_MAX_HOURS,
})}
</Typography> </Typography>
)} )}
</TableCell> </TableCell>
@@ -117,7 +128,11 @@ const DayRow: React.FC<{
> >
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<Box> <Box>
<EntryInputTable day={day} assignedProjects={assignedProjects} />
<EntryInputTable
day={day}
assignedProjects={assignedProjects}
allProjects={allProjects}
/>
</Box> </Box>
</Collapse> </Collapse>
</TableCell> </TableCell>


+ 4
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx Просмотреть файл

@@ -9,7 +9,7 @@ import { Typography } from "@mui/material";
import ButtonGroup from "@mui/material/ButtonGroup"; import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects"; import AssignedProjects from "./AssignedProjects";
import TimesheetModal from "../TimesheetModal"; import TimesheetModal from "../TimesheetModal";
import { AssignedProject } from "@/app/api/projects";
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { import {
RecordLeaveInput, RecordLeaveInput,
RecordTimesheetInput, RecordTimesheetInput,
@@ -19,6 +19,7 @@ import { LeaveType } from "@/app/api/timesheets";


export interface Props { export interface Props {
leaveTypes: LeaveType[]; leaveTypes: LeaveType[];
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[]; assignedProjects: AssignedProject[];
username: string; username: string;
defaultLeaveRecords: RecordLeaveInput; defaultLeaveRecords: RecordLeaveInput;
@@ -27,6 +28,7 @@ export interface Props {


const UserWorkspacePage: React.FC<Props> = ({ const UserWorkspacePage: React.FC<Props> = ({
leaveTypes, leaveTypes,
allProjects,
assignedProjects, assignedProjects,
username, username,
defaultLeaveRecords, defaultLeaveRecords,
@@ -82,6 +84,7 @@ const UserWorkspacePage: React.FC<Props> = ({
<TimesheetModal <TimesheetModal
isOpen={isTimeheetModalVisible} isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal} onClose={handleCloseTimesheetModal}
allProjects={allProjects}
assignedProjects={assignedProjects} assignedProjects={assignedProjects}
username={username} username={username}
defaultTimesheets={defaultTimesheets} defaultTimesheets={defaultTimesheets}


+ 13
- 7
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx Просмотреть файл

@@ -1,4 +1,7 @@
import { fetchAssignedProjects } from "@/app/api/projects";
import {
fetchAssignedProjects,
fetchProjectWithTasks,
} from "@/app/api/projects";
import UserWorkspacePage from "./UserWorkspacePage"; import UserWorkspacePage from "./UserWorkspacePage";
import { import {
fetchLeaveTypes, fetchLeaveTypes,
@@ -11,15 +14,18 @@ interface Props {
} }


const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => { const UserWorkspaceWrapper: React.FC<Props> = async ({ username }) => {
const [assignedProjects, timesheets, leaves, leaveTypes] = await Promise.all([
fetchAssignedProjects(username),
fetchTimesheets(username),
fetchLeaves(username),
fetchLeaveTypes(),
]);
const [assignedProjects, allProjects, timesheets, leaves, leaveTypes] =
await Promise.all([
fetchAssignedProjects(username),
fetchProjectWithTasks(),
fetchTimesheets(username),
fetchLeaves(username),
fetchLeaveTypes(),
]);


return ( return (
<UserWorkspacePage <UserWorkspacePage
allProjects={allProjects}
assignedProjects={assignedProjects} assignedProjects={assignedProjects}
username={username} username={username}
defaultTimesheets={timesheets} defaultTimesheets={timesheets}


Загрузка…
Отмена
Сохранить