Browse Source

Update timesheet entry

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 year ago
parent
commit
61d7f400ef
14 changed files with 464 additions and 119 deletions
  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 View File

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

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

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


+ 13
- 1
src/app/api/projects/index.ts View File

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

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

export interface AssignedProject extends ProjectWithTasks {
// Manhour info
hoursSpent: 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) => {
return serverFetchJson<CreateProjectInputs>(
`${BASE_API_URL}/projects/projectDetails/${projectId}`,


+ 5
- 3
src/app/api/timesheets/actions.ts View File

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

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

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

export interface RecordLeaveInput {


+ 44
- 0
src/app/api/timesheets/utils.ts View File

@@ -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 View File

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

dayjs.extend(isBetween);

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


+ 9
- 5
src/components/LeaveTable/LeaveTable.tsx View File

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

interface Props {
leaveTypes: LeaveType[];
}

const MAX_HOURS = 8;

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

@@ -94,17 +93,22 @@ const DayRow: React.FC<{
{shortDateFormatter(language).format(dayJsObj.toDate())}
</TableCell>
<TableCell
sx={{ color: totalHours > MAX_HOURS ? "error.main" : undefined }}
sx={{
color:
totalHours > LEAVE_DAILY_MAX_HOURS ? "error.main" : undefined,
}}
>
{manhourFormatter.format(totalHours)}
{totalHours > MAX_HOURS && (
{totalHours > LEAVE_DAILY_MAX_HOURS && (
<Typography
color="error.main"
variant="body2"
component="span"
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>
)}
</TableCell>


+ 7
- 2
src/components/TimesheetModal/TimesheetModal.tsx View File

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

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


+ 102
- 85
src/components/TimesheetTable/EntryInputTable.tsx View File

@@ -5,6 +5,7 @@ import {
GridActionsCellItem,
GridColDef,
GridEventListener,
GridRenderEditCellParams,
GridRowId,
GridRowModel,
GridRowModes,
@@ -18,31 +19,40 @@ 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 } from "@/app/api/projects";
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 { isValidTimeEntry } from "@/app/api/timesheets/utils";

dayjs.extend(isBetween);

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

type TimeEntryRow = Partial<
export type TimeEntryRow = Partial<
TimeEntry & {
_isNew: boolean;
_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 taskGroupsByProject = useMemo(() => {
return assignedProjects.reduce<{
return allProjects.reduce<{
[projectId: AssignedProject["id"]]: {
value: TaskGroup["id"];
label: string;
@@ -59,7 +69,7 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
),
};
}, {});
}, [assignedProjects]);
}, [allProjects]);

// To check for start / end planned dates
const milestonesByProject = useMemo(() => {
@@ -94,20 +104,10 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
"",
) 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
let isPlanned = false;
let isPlanned;
if (
row.projectId &&
row.taskGroupId &&
@@ -211,14 +211,28 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
{
field: "projectId",
headerName: t("Project Code and Name"),
width: 200,
width: 400,
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"),
width: 200,
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"),
width: 200,
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);
},
},
{
field: "remark",
headerName: t("Remark"),
sortable: false,
flex: 1,
editable: true,
},
],
[
t,
@@ -299,31 +327,20 @@ const EntryInputTable: React.FC<Props> = ({ day, assignedProjects }) => {
handleDelete,
handleSave,
handleCancel,
apiRef,
taskGroupsByProject,
assignedProjects,
allProjects,
taskGroupsByProject,
],
);

useEffect(() => {
setValue(day, [
...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]);


+ 89
- 0
src/components/TimesheetTable/ProjectSelect.tsx View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -18,13 +18,15 @@ import React, { useState } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
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 {
allProjects: ProjectWithTasks[];
assignedProjects: AssignedProject[];
}

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

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


+ 4
- 1
src/components/UserWorkspacePage/UserWorkspacePage.tsx View File

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

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

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


+ 13
- 7
src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx View File

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

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 (
<UserWorkspacePage
allProjects={allProjects}
assignedProjects={assignedProjects}
username={username}
defaultTimesheets={timesheets}


Loading…
Cancel
Save