@@ -14,13 +14,32 @@ import TaskSetup from "./TaskSetup"; | |||
import StaffAllocation from "./StaffAllocation"; | |||
import ResourceMilestone from "./ResourceMilestone"; | |||
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 { Error } from "@mui/icons-material"; | |||
export interface Props { | |||
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 [tabIndex, setTabIndex] = useState(0); | |||
const { t } = useTranslation(); | |||
@@ -41,6 +60,16 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => { | |||
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>({ | |||
defaultValues: { | |||
tasks: {}, | |||
@@ -49,23 +78,33 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => { | |||
}, | |||
}); | |||
const errors = formProps.formState.errors; | |||
return ( | |||
<FormProvider {...formProps}> | |||
<Stack | |||
spacing={2} | |||
component="form" | |||
onSubmit={formProps.handleSubmit(onSubmit)} | |||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
> | |||
<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> | |||
{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}> | |||
<Button | |||
variant="outlined" | |||
@@ -18,12 +18,17 @@ import Button from "@mui/material/Button"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
const ProjectClientDetails: React.FC = () => { | |||
const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({ | |||
isActive, | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { register } = useFormContext<CreateProjectInputs>(); | |||
const { | |||
register, | |||
formState: { errors }, | |||
} = useFormContext<CreateProjectInputs>(); | |||
return ( | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent component={Stack} spacing={4}> | |||
<Box> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
@@ -48,7 +53,10 @@ const ProjectClientDetails: React.FC = () => { | |||
<TextField | |||
label={t("Project Name")} | |||
fullWidth | |||
{...register("projectName")} | |||
{...register("projectName", { | |||
required: "Project name required!", | |||
})} | |||
error={Boolean(errors.projectName)} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
@@ -1,8 +1,6 @@ | |||
import { manhourFormatter } from "@/app/utils/formatUtil"; | |||
import { | |||
Box, | |||
Card, | |||
CardContent, | |||
Stack, | |||
Table, | |||
TableBody, | |||
@@ -94,55 +92,47 @@ const ResourceCapacity: React.FC<Props> = ({ items = mockItems }) => { | |||
); | |||
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, | |||
Select, | |||
SelectChangeEvent, | |||
Stack, | |||
} from "@mui/material"; | |||
import { Task, TaskGroup } from "@/app/api/tasks"; | |||
import uniqBy from "lodash/uniqBy"; | |||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import MilestoneSection from "./MilestoneSection"; | |||
@@ -29,11 +27,13 @@ import ProjectTotalFee from "./ProjectTotalFee"; | |||
export interface Props { | |||
allTasks: Task[]; | |||
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | |||
isActive: boolean; | |||
} | |||
const ResourceMilestone: React.FC<Props> = ({ | |||
allTasks, | |||
defaultManhourBreakdownByGrade, | |||
isActive, | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { getValues } = useFormContext<CreateProjectInputs>(); | |||
@@ -65,7 +65,7 @@ const ResourceMilestone: React.FC<Props> = ({ | |||
return ( | |||
<> | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | |||
<FormControl> | |||
<InputLabel>{t("Task Stage")}</InputLabel> | |||
@@ -93,7 +93,7 @@ const ResourceMilestone: React.FC<Props> = ({ | |||
</CardActions> | |||
</CardContent> | |||
</Card> | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent> | |||
<ProjectTotalFee taskGroups={taskGroups} /> | |||
</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(); | |||
return ( | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent> | |||
<Alert severity="warning"> | |||
{t('Please add some tasks in "Project Task Setup" first!')} | |||
@@ -119,7 +119,7 @@ const ResourceMilestoneWrapper: React.FC<Props> = (props) => { | |||
const { getValues } = useFormContext<CreateProjectInputs>(); | |||
if (Object.keys(getValues("tasks")).length === 0) { | |||
return <NoTaskState />; | |||
return <NoTaskState isActive={props.isActive} />; | |||
} | |||
return <ResourceMilestone {...props} />; | |||
@@ -93,9 +93,13 @@ const mockStaffs: StaffResult[] = [ | |||
interface Props { | |||
allStaff?: StaffResult[]; | |||
isActive: boolean; | |||
} | |||
const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||
const StaffAllocation: React.FC<Props> = ({ | |||
allStaff = mockStaffs, | |||
isActive, | |||
}) => { | |||
const { t } = useTranslation(); | |||
const { setValue, getValues } = useFormContext<CreateProjectInputs>(); | |||
@@ -235,7 +239,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||
return ( | |||
<> | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Stack gap={2}> | |||
<Typography variant="overline" display="block"> | |||
@@ -318,7 +322,11 @@ const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||
</CardActions> | |||
</CardContent> | |||
</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 { | |||
allTasks: Task[]; | |||
isActive: boolean; | |||
} | |||
const TaskSetup: React.FC<Props> = ({ allTasks: tasks }) => { | |||
const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => { | |||
const { t } = useTranslation(); | |||
const { getValues, setValue } = useFormContext<CreateProjectInputs>(); | |||
const currentTasks = getValues("tasks"); | |||
@@ -38,7 +39,7 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks }) => { | |||
}, [currentTasks, tasks]); | |||
return ( | |||
<Card> | |||
<Card sx={{ display: isActive ? "block" : "none" }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||
{t("Task List Setup")} | |||
@@ -28,24 +28,28 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||
label: t("Client name"), | |||
paramName: "client", | |||
type: "select", | |||
options: ["A", "B"], | |||
options: ["Client A", "Client B", "Client C"], | |||
}, | |||
{ | |||
label: t("Project category"), | |||
paramName: "category", | |||
type: "select", | |||
options: ["A", "B"], | |||
options: ["Confirmed Project", "Project to be bidded"], | |||
}, | |||
{ | |||
label: t("Team"), | |||
paramName: "team", | |||
type: "select", | |||
options: ["A", "B"], | |||
options: ["TW", "WY"], | |||
}, | |||
], | |||
[t], | |||
); | |||
const onReset = useCallback(() => { | |||
setFilteredProjects(projects); | |||
}, [projects]); | |||
const onProjectClick = useCallback((project: ProjectResult) => { | |||
console.log(project); | |||
}, []); | |||
@@ -72,8 +76,18 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => { | |||
<SearchBox | |||
criteria={searchCriteria} | |||
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> | |||
items={filteredProjects} | |||
@@ -55,8 +55,8 @@ const TaskTemplateSearch: React.FC<Props> = ({ taskTemplates }) => { | |||
setFilteredTemplates( | |||
taskTemplates.filter( | |||
(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, | |||
lineHeight: 1.71, | |||
minWidth: "auto", | |||
minHeight: 48, | |||
paddingLeft: 0, | |||
paddingRight: 0, | |||
textTransform: "none", | |||