@@ -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", | ||||