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