Ver código fonte

Move sections around in project creation

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 ano atrás
pai
commit
61ec4efa3f
5 arquivos alterados com 64 adições e 48 exclusões
  1. +5
    -5
      src/components/CreateProject/CreateProject.tsx
  2. +5
    -23
      src/components/CreateProject/Milestone.tsx
  3. +1
    -1
      src/components/CreateProject/MilestoneSection.tsx
  4. +14
    -7
      src/components/CreateProject/ResourceAllocation.tsx
  5. +39
    -12
      src/components/CreateProject/StaffAllocation.tsx

+ 5
- 5
src/components/CreateProject/CreateProject.tsx Ver arquivo

@@ -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}


src/components/CreateProject/ResourceMilestone.tsx → src/components/CreateProject/Milestone.tsx Ver arquivo

@@ -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;

+ 1
- 1
src/components/CreateProject/MilestoneSection.tsx Ver arquivo

@@ -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 }}>


src/components/CreateProject/ResourceSection.tsx → src/components/CreateProject/ResourceAllocation.tsx Ver arquivo

@@ -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;

+ 39
- 12
src/components/CreateProject/StaffAllocation.tsx Ver arquivo

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


Carregando…
Cancelar
Salvar