@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next"; | |||
import ProjectClientDetails from "./ProjectClientDetails"; | |||
import TaskSetup from "./TaskSetup"; | |||
import StaffAllocation from "./StaffAllocation"; | |||
import ResourceMilestone from "./ResourceMilestone"; | |||
import Milestone from "./Milestone"; | |||
import { Task, TaskTemplate } from "@/app/api/tasks"; | |||
import { | |||
FieldErrors, | |||
@@ -122,8 +122,8 @@ const CreateProject: React.FC<Props> = ({ | |||
iconPosition="end" | |||
/> | |||
<Tab label={t("Project Task Setup")} iconPosition="end" /> | |||
<Tab label={t("Staff Allocation")} iconPosition="end" /> | |||
<Tab label={t("Resource and Milestone")} iconPosition="end" /> | |||
<Tab label={t("Staff Allocation and Resource")} iconPosition="end" /> | |||
<Tab label={t("Milestone")} iconPosition="end" /> | |||
</Tabs> | |||
{ | |||
<ProjectClientDetails | |||
@@ -139,8 +139,8 @@ const CreateProject: React.FC<Props> = ({ | |||
isActive={tabIndex === 1} | |||
/> | |||
} | |||
{<StaffAllocation isActive={tabIndex === 2} />} | |||
{<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||
{<StaffAllocation isActive={tabIndex === 2} allTasks={allTasks} />} | |||
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||
{serverError && ( | |||
<Typography variant="body2" color="error" alignSelf="flex-end"> | |||
{serverError} | |||
@@ -2,7 +2,6 @@ | |||
import Card from "@mui/material/Card"; | |||
import CardContent from "@mui/material/CardContent"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import Button from "@mui/material/Button"; | |||
import React, { useCallback, useMemo, useState } from "react"; | |||
@@ -21,20 +20,14 @@ import uniqBy from "lodash/uniqBy"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import MilestoneSection from "./MilestoneSection"; | |||
import ResourceSection from "./ResourceSection"; | |||
import ProjectTotalFee from "./ProjectTotalFee"; | |||
export interface Props { | |||
allTasks: Task[]; | |||
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | |||
isActive: boolean; | |||
} | |||
const ResourceMilestone: React.FC<Props> = ({ | |||
allTasks, | |||
defaultManhourBreakdownByGrade, | |||
isActive, | |||
}) => { | |||
const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
const { t } = useTranslation(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
@@ -49,17 +42,13 @@ const ResourceMilestone: React.FC<Props> = ({ | |||
const [currentTaskGroupId, setCurrentTaskGroupId] = useState( | |||
taskGroups[0].id, | |||
); | |||
const [currentTasks, setCurrentTasks] = useState<typeof tasks>( | |||
tasks.filter((t) => t.taskGroup.id === currentTaskGroupId), | |||
); | |||
const onSelectTaskGroup = useCallback( | |||
(event: SelectChangeEvent<TaskGroup["id"]>) => { | |||
const id = event.target.value; | |||
const newTaksGroupId = typeof id === "string" ? parseInt(id) : id; | |||
setCurrentTaskGroupId(newTaksGroupId); | |||
setCurrentTasks(tasks.filter((t) => t.taskGroup.id === newTaksGroupId)); | |||
}, | |||
[tasks], | |||
[], | |||
); | |||
return ( | |||
@@ -81,13 +70,6 @@ const ResourceMilestone: React.FC<Props> = ({ | |||
</Select> | |||
</FormControl> | |||
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} | |||
{isActive && ( | |||
<ResourceSection | |||
tasks={currentTasks} | |||
manhourBreakdownByGrade={defaultManhourBreakdownByGrade} | |||
/> | |||
)} | |||
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} | |||
{isActive && <MilestoneSection taskGroupId={currentTaskGroupId} />} | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
@@ -118,14 +100,14 @@ const NoTaskState: React.FC<Pick<Props, "isActive">> = ({ isActive }) => { | |||
); | |||
}; | |||
const ResourceMilestoneWrapper: React.FC<Props> = (props) => { | |||
const MilestoneWrapper: React.FC<Props> = (props) => { | |||
const { getValues } = useFormContext<CreateProjectInputs>(); | |||
if (Object.keys(getValues("tasks")).length === 0) { | |||
return <NoTaskState isActive={props.isActive} />; | |||
} | |||
return <ResourceMilestone {...props} />; | |||
return <Milestone {...props} />; | |||
}; | |||
export default ResourceMilestoneWrapper; | |||
export default MilestoneWrapper; |
@@ -218,7 +218,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
return ( | |||
<Stack gap={1}> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Task Stage Milestones")} | |||
{t("Stage Milestones")} | |||
</Typography> | |||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
@@ -11,7 +11,7 @@ import { | |||
} from "@mui/material"; | |||
import { useState, useCallback, useEffect, useMemo } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Props as ResourceMilestoneProps } from "./ResourceMilestone"; | |||
import { Props as StaffAllocationProps } from "./StaffAllocation"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { useForm, useFormContext } from "react-hook-form"; | |||
import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid"; | |||
@@ -25,8 +25,8 @@ import _reduce from "lodash/reduce"; | |||
const mockGrades = [1, 2, 3, 4, 5]; | |||
interface Props { | |||
tasks: Task[]; | |||
manhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"]; | |||
allTasks: Task[]; | |||
manhourBreakdownByGrade: StaffAllocationProps["defaultManhourBreakdownByGrade"]; | |||
} | |||
type Row = ManhourAllocation & { id: "manhourAllocation" }; | |||
@@ -36,8 +36,8 @@ const parseValidManhours = (value: number | string): number => { | |||
return isNaN(inputNumber) || inputNumber < 0 ? 0 : inputNumber; | |||
}; | |||
const ResourceSection: React.FC<Props> = ({ | |||
tasks, | |||
const ResourceAllocation: React.FC<Props> = ({ | |||
allTasks, | |||
manhourBreakdownByGrade = mockGrades.reduce< | |||
NonNullable<Props["manhourBreakdownByGrade"]> | |||
>((acc, grade) => { | |||
@@ -45,6 +45,13 @@ const ResourceSection: React.FC<Props> = ({ | |||
}, {}), | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { watch } = useFormContext<CreateProjectInputs>(); | |||
const currentTasks = watch("tasks"); | |||
const tasks = useMemo( | |||
() => allTasks.filter((task) => currentTasks[task.id]), | |||
[allTasks, currentTasks], | |||
); | |||
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id); | |||
const makeOnTaskSelect = useCallback( | |||
(taskId: Task["id"]): React.MouseEventHandler => | |||
@@ -165,7 +172,7 @@ const ResourceSection: React.FC<Props> = ({ | |||
); | |||
return ( | |||
<Box marginBlock={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Task Breakdown")} | |||
</Typography> | |||
@@ -223,4 +230,4 @@ const ResourceSection: React.FC<Props> = ({ | |||
); | |||
}; | |||
export default ResourceSection; | |||
export default ResourceAllocation; |
@@ -31,6 +31,8 @@ import uniq from "lodash/uniq"; | |||
import ResourceCapacity from "./ResourceCapacity"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import ResourceAllocation from "./ResourceAllocation"; | |||
import { Task } from "@/app/api/tasks"; | |||
interface StaffResult { | |||
id: number; | |||
@@ -85,25 +87,39 @@ const mockStaffs: StaffResult[] = [ | |||
name: "Kurt", | |||
grade: "1", | |||
id: 11, | |||
team: "ABC", | |||
team: "XYZ", | |||
title: "Construction Assistant", | |||
}, | |||
{ name: "Lawrence", grade: "2", id: 12, team: "ABC", title: "Operator" }, | |||
]; | |||
interface Props { | |||
const staffComparator = (a: StaffResult, b: StaffResult) => { | |||
return ( | |||
a.team.localeCompare(b.team) || | |||
a.grade.localeCompare(b.grade) || | |||
a.id - b.id | |||
); | |||
}; | |||
export interface Props { | |||
allStaff?: StaffResult[]; | |||
isActive: boolean; | |||
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | |||
allTasks: Task[]; | |||
} | |||
const StaffAllocation: React.FC<Props> = ({ | |||
allStaff = mockStaffs, | |||
allTasks, | |||
isActive, | |||
defaultManhourBreakdownByGrade, | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { setValue, getValues } = useFormContext<CreateProjectInputs>(); | |||
const [filteredStaff, setFilteredStaff] = React.useState(allStaff); | |||
const [filteredStaff, setFilteredStaff] = React.useState( | |||
allStaff.sort(staffComparator), | |||
); | |||
const [selectedStaff, setSelectedStaff] = React.useState< | |||
typeof filteredStaff | |||
>( | |||
@@ -138,10 +154,10 @@ const StaffAllocation: React.FC<Props> = ({ | |||
onClick: addStaff, | |||
buttonIcon: <PersonAdd />, | |||
}, | |||
{ label: t("Staff ID"), name: "id" }, | |||
{ label: t("Staff Name"), name: "name" }, | |||
{ label: t("Team"), name: "team" }, | |||
{ label: t("Grade"), name: "grade" }, | |||
{ label: t("Staff ID"), name: "id" }, | |||
{ label: t("Staff Name"), name: "name" }, | |||
{ label: t("Title"), name: "title" }, | |||
], | |||
[addStaff, t], | |||
@@ -155,10 +171,10 @@ const StaffAllocation: React.FC<Props> = ({ | |||
onClick: removeStaff, | |||
buttonIcon: <PersonRemove />, | |||
}, | |||
{ label: t("Staff ID"), name: "id" }, | |||
{ label: t("Staff Name"), name: "name" }, | |||
{ label: t("Team"), name: "team" }, | |||
{ label: t("Grade"), name: "grade" }, | |||
{ label: t("Staff ID"), name: "id" }, | |||
{ label: t("Staff Name"), name: "name" }, | |||
{ label: t("Title"), name: "title" }, | |||
], | |||
[removeStaff, t], | |||
@@ -239,6 +255,11 @@ const StaffAllocation: React.FC<Props> = ({ | |||
return ( | |||
<> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<ResourceCapacity /> | |||
</CardContent> | |||
</Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Stack gap={2}> | |||
@@ -322,11 +343,17 @@ const StaffAllocation: React.FC<Props> = ({ | |||
</CardActions> | |||
</CardContent> | |||
</Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<ResourceCapacity /> | |||
</CardContent> | |||
</Card> | |||
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */} | |||
{isActive && ( | |||
<Card> | |||
<CardContent> | |||
<ResourceAllocation | |||
allTasks={allTasks} | |||
manhourBreakdownByGrade={defaultManhourBreakdownByGrade} | |||
/> | |||
</CardContent> | |||
</Card> | |||
)} | |||
</> | |||
); | |||
}; | |||