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