| @@ -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> | |||||
| )} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||