| @@ -14,13 +14,32 @@ import TaskSetup from "./TaskSetup"; | |||||
| import StaffAllocation from "./StaffAllocation"; | import StaffAllocation from "./StaffAllocation"; | ||||
| import ResourceMilestone from "./ResourceMilestone"; | import ResourceMilestone from "./ResourceMilestone"; | ||||
| import { Task } from "@/app/api/tasks"; | import { Task } from "@/app/api/tasks"; | ||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { CreateProjectInputs } from "@/app/api/projects/actions"; | import { CreateProjectInputs } from "@/app/api/projects/actions"; | ||||
| import { Error } from "@mui/icons-material"; | |||||
| export interface Props { | export interface Props { | ||||
| allTasks: Task[]; | allTasks: Task[]; | ||||
| } | } | ||||
| const hasErrorsInTab = ( | |||||
| tabIndex: number, | |||||
| errors: FieldErrors<CreateProjectInputs>, | |||||
| ) => { | |||||
| switch (tabIndex) { | |||||
| case 0: | |||||
| return errors.projectName; | |||||
| default: | |||||
| false; | |||||
| } | |||||
| }; | |||||
| const CreateProject: React.FC<Props> = ({ allTasks }) => { | const CreateProject: React.FC<Props> = ({ allTasks }) => { | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| @@ -41,6 +60,16 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => { | |||||
| console.log(data); | console.log(data); | ||||
| }, []); | }, []); | ||||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | |||||
| (errors) => { | |||||
| // Set the tab so that the focus will go there | |||||
| if (errors.projectName) { | |||||
| setTabIndex(0); | |||||
| } | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const formProps = useForm<CreateProjectInputs>({ | const formProps = useForm<CreateProjectInputs>({ | ||||
| defaultValues: { | defaultValues: { | ||||
| tasks: {}, | tasks: {}, | ||||
| @@ -49,23 +78,33 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => { | |||||
| }, | }, | ||||
| }); | }); | ||||
| const errors = formProps.formState.errors; | |||||
| return ( | return ( | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <Stack | <Stack | ||||
| spacing={2} | spacing={2} | ||||
| component="form" | component="form" | ||||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | > | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| <Tab label={t("Project and Client Details")} /> | |||||
| <Tab label={t("Project Task Setup")} /> | |||||
| <Tab label={t("Staff Allocation")} /> | |||||
| <Tab label={t("Resource and Milestone")} /> | |||||
| <Tab | |||||
| label={t("Project and Client Details")} | |||||
| icon={ | |||||
| hasErrorsInTab(0, errors) ? ( | |||||
| <Error sx={{ marginInlineEnd: 1 }} color="error" /> | |||||
| ) : undefined | |||||
| } | |||||
| 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" /> | |||||
| </Tabs> | </Tabs> | ||||
| {tabIndex === 0 && <ProjectClientDetails />} | |||||
| {tabIndex === 1 && <TaskSetup allTasks={allTasks} />} | |||||
| {tabIndex === 2 && <StaffAllocation />} | |||||
| {tabIndex === 3 && <ResourceMilestone allTasks={allTasks} />} | |||||
| {<ProjectClientDetails isActive={tabIndex === 0} />} | |||||
| {<TaskSetup allTasks={allTasks} isActive={tabIndex === 1} />} | |||||
| {<StaffAllocation isActive={tabIndex === 2} />} | |||||
| {<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -18,12 +18,17 @@ import Button from "@mui/material/Button"; | |||||
| 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"; | ||||
| const ProjectClientDetails: React.FC = () => { | |||||
| const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({ | |||||
| isActive, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { register } = useFormContext<CreateProjectInputs>(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors }, | |||||
| } = useFormContext<CreateProjectInputs>(); | |||||
| return ( | return ( | ||||
| <Card> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| <Box> | <Box> | ||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
| @@ -48,7 +53,10 @@ const ProjectClientDetails: React.FC = () => { | |||||
| <TextField | <TextField | ||||
| label={t("Project Name")} | label={t("Project Name")} | ||||
| fullWidth | fullWidth | ||||
| {...register("projectName")} | |||||
| {...register("projectName", { | |||||
| required: "Project name required!", | |||||
| })} | |||||
| error={Boolean(errors.projectName)} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| @@ -1,8 +1,6 @@ | |||||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | import { manhourFormatter } from "@/app/utils/formatUtil"; | ||||
| import { | import { | ||||
| Box, | Box, | ||||
| Card, | |||||
| CardContent, | |||||
| Stack, | Stack, | ||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| @@ -94,55 +92,47 @@ const ResourceCapacity: React.FC<Props> = ({ items = mockItems }) => { | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Card> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Stack gap={2}> | |||||
| <Typography variant="overline" display="block"> | |||||
| {t("Resource Capacity")} | |||||
| </Typography> | |||||
| <Box sx={{ marginInline: -3 }}> | |||||
| <TableContainer> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| {columns.map((column, idx) => ( | |||||
| <TableCell key={`${column.name.toString()}${idx}`}> | |||||
| {column.label} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {items.map((item, index) => { | |||||
| return ( | |||||
| <TableRow | |||||
| hover | |||||
| tabIndex={-1} | |||||
| key={`${item.grade}-${index}`} | |||||
| > | |||||
| {columns.map((column, idx) => { | |||||
| const columnName = column.name; | |||||
| const cellData = item[columnName]; | |||||
| <Stack gap={2}> | |||||
| <Typography variant="overline" display="block"> | |||||
| {t("Resource Capacity")} | |||||
| </Typography> | |||||
| <Box sx={{ marginInline: -3 }}> | |||||
| <TableContainer> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| {columns.map((column, idx) => ( | |||||
| <TableCell key={`${column.name.toString()}${idx}`}> | |||||
| {column.label} | |||||
| </TableCell> | |||||
| ))} | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {items.map((item, index) => { | |||||
| return ( | |||||
| <TableRow hover tabIndex={-1} key={`${item.grade}-${index}`}> | |||||
| {columns.map((column, idx) => { | |||||
| const columnName = column.name; | |||||
| const cellData = item[columnName]; | |||||
| return ( | |||||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||||
| {columnName !== "headcount" && | |||||
| typeof cellData === "number" | |||||
| ? manhourFormatter.format(cellData) | |||||
| : cellData} | |||||
| </TableCell> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ); | |||||
| })} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Box> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| return ( | |||||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||||
| {columnName !== "headcount" && | |||||
| typeof cellData === "number" | |||||
| ? manhourFormatter.format(cellData) | |||||
| : cellData} | |||||
| </TableCell> | |||||
| ); | |||||
| })} | |||||
| </TableRow> | |||||
| ); | |||||
| })} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Box> | |||||
| </Stack> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -15,11 +15,9 @@ import { | |||||
| MenuItem, | MenuItem, | ||||
| Select, | Select, | ||||
| SelectChangeEvent, | SelectChangeEvent, | ||||
| Stack, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { Task, TaskGroup } from "@/app/api/tasks"; | import { Task, TaskGroup } from "@/app/api/tasks"; | ||||
| import uniqBy from "lodash/uniqBy"; | import uniqBy from "lodash/uniqBy"; | ||||
| import { moneyFormatter } from "@/app/utils/formatUtil"; | |||||
| 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"; | ||||
| @@ -29,11 +27,13 @@ import ProjectTotalFee from "./ProjectTotalFee"; | |||||
| export interface Props { | export interface Props { | ||||
| allTasks: Task[]; | allTasks: Task[]; | ||||
| defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | ||||
| isActive: boolean; | |||||
| } | } | ||||
| const ResourceMilestone: React.FC<Props> = ({ | const ResourceMilestone: React.FC<Props> = ({ | ||||
| allTasks, | allTasks, | ||||
| defaultManhourBreakdownByGrade, | defaultManhourBreakdownByGrade, | ||||
| isActive, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { getValues } = useFormContext<CreateProjectInputs>(); | const { getValues } = useFormContext<CreateProjectInputs>(); | ||||
| @@ -65,7 +65,7 @@ const ResourceMilestone: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Card> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | ||||
| <FormControl> | <FormControl> | ||||
| <InputLabel>{t("Task Stage")}</InputLabel> | <InputLabel>{t("Task Stage")}</InputLabel> | ||||
| @@ -93,7 +93,7 @@ const ResourceMilestone: React.FC<Props> = ({ | |||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| <Card> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent> | <CardContent> | ||||
| <ProjectTotalFee taskGroups={taskGroups} /> | <ProjectTotalFee taskGroups={taskGroups} /> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -102,10 +102,10 @@ const ResourceMilestone: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| const NoTaskState: React.FC = () => { | |||||
| const NoTaskState: React.FC<Pick<Props, "isActive">> = ({ isActive }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| return ( | return ( | ||||
| <Card> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent> | <CardContent> | ||||
| <Alert severity="warning"> | <Alert severity="warning"> | ||||
| {t('Please add some tasks in "Project Task Setup" first!')} | {t('Please add some tasks in "Project Task Setup" first!')} | ||||
| @@ -119,7 +119,7 @@ const ResourceMilestoneWrapper: 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 />; | |||||
| return <NoTaskState isActive={props.isActive} />; | |||||
| } | } | ||||
| return <ResourceMilestone {...props} />; | return <ResourceMilestone {...props} />; | ||||
| @@ -93,9 +93,13 @@ const mockStaffs: StaffResult[] = [ | |||||
| interface Props { | interface Props { | ||||
| allStaff?: StaffResult[]; | allStaff?: StaffResult[]; | ||||
| isActive: boolean; | |||||
| } | } | ||||
| const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||||
| const StaffAllocation: React.FC<Props> = ({ | |||||
| allStaff = mockStaffs, | |||||
| isActive, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { setValue, getValues } = useFormContext<CreateProjectInputs>(); | const { setValue, getValues } = useFormContext<CreateProjectInputs>(); | ||||
| @@ -235,7 +239,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Card> | |||||
| <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}> | ||||
| <Typography variant="overline" display="block"> | <Typography variant="overline" display="block"> | ||||
| @@ -318,7 +322,11 @@ const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||||
| </CardActions> | </CardActions> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| <ResourceCapacity /> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <ResourceCapacity /> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -20,9 +20,10 @@ import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||||
| interface Props { | interface Props { | ||||
| allTasks: Task[]; | allTasks: Task[]; | ||||
| isActive: boolean; | |||||
| } | } | ||||
| const TaskSetup: React.FC<Props> = ({ allTasks: tasks }) => { | |||||
| const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | ||||
| const currentTasks = getValues("tasks"); | const currentTasks = getValues("tasks"); | ||||
| @@ -38,7 +39,7 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks }) => { | |||||
| }, [currentTasks, tasks]); | }, [currentTasks, tasks]); | ||||
| return ( | return ( | ||||
| <Card> | |||||
| <Card sx={{ display: isActive ? "block" : "none" }}> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
| {t("Task List Setup")} | {t("Task List Setup")} | ||||
| @@ -28,24 +28,28 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||||
| label: t("Client name"), | label: t("Client name"), | ||||
| paramName: "client", | paramName: "client", | ||||
| type: "select", | type: "select", | ||||
| options: ["A", "B"], | |||||
| options: ["Client A", "Client B", "Client C"], | |||||
| }, | }, | ||||
| { | { | ||||
| label: t("Project category"), | label: t("Project category"), | ||||
| paramName: "category", | paramName: "category", | ||||
| type: "select", | type: "select", | ||||
| options: ["A", "B"], | |||||
| options: ["Confirmed Project", "Project to be bidded"], | |||||
| }, | }, | ||||
| { | { | ||||
| label: t("Team"), | label: t("Team"), | ||||
| paramName: "team", | paramName: "team", | ||||
| type: "select", | type: "select", | ||||
| options: ["A", "B"], | |||||
| options: ["TW", "WY"], | |||||
| }, | }, | ||||
| ], | ], | ||||
| [t], | [t], | ||||
| ); | ); | ||||
| const onReset = useCallback(() => { | |||||
| setFilteredProjects(projects); | |||||
| }, [projects]); | |||||
| const onProjectClick = useCallback((project: ProjectResult) => { | const onProjectClick = useCallback((project: ProjectResult) => { | ||||
| console.log(project); | console.log(project); | ||||
| }, []); | }, []); | ||||
| @@ -72,8 +76,18 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| console.log(query); | |||||
| setFilteredProjects( | |||||
| projects.filter( | |||||
| (p) => | |||||
| p.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| p.name.toLowerCase().includes(query.name.toLowerCase()) && | |||||
| (query.client === "All" || p.client === query.client) && | |||||
| (query.category === "All" || p.category === query.category) && | |||||
| (query.team === "All" || p.team === query.team), | |||||
| ), | |||||
| ); | |||||
| }} | }} | ||||
| onReset={onReset} | |||||
| /> | /> | ||||
| <SearchResults<ProjectResult> | <SearchResults<ProjectResult> | ||||
| items={filteredProjects} | items={filteredProjects} | ||||
| @@ -55,8 +55,8 @@ const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | |||||
| setFilteredTemplates( | setFilteredTemplates( | ||||
| taskTemplates.filter( | taskTemplates.filter( | ||||
| (task) => | (task) => | ||||
| task.code.toLowerCase().includes(query.code) && | |||||
| task.name.toLowerCase().includes(query.name), | |||||
| task.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| task.name.toLowerCase().includes(query.name.toLowerCase()), | |||||
| ), | ), | ||||
| ); | ); | ||||
| }} | }} | ||||
| @@ -277,6 +277,7 @@ const components: ThemeOptions["components"] = { | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| lineHeight: 1.71, | lineHeight: 1.71, | ||||
| minWidth: "auto", | minWidth: "auto", | ||||
| minHeight: 48, | |||||
| paddingLeft: 0, | paddingLeft: 0, | ||||
| paddingRight: 0, | paddingRight: 0, | ||||
| textTransform: "none", | textTransform: "none", | ||||