@@ -34,3 +34,5 @@ yarn-error.log* | |||||
# typescript | # typescript | ||||
*.tsbuildinfo | *.tsbuildinfo | ||||
next-env.d.ts | next-env.d.ts | ||||
.vscode |
@@ -23,7 +23,7 @@ export interface CreateProjectInputs { | |||||
tasks: { | tasks: { | ||||
[taskId: Task["id"]]: { | [taskId: Task["id"]]: { | ||||
manhourAllocation: { | manhourAllocation: { | ||||
[grade: string]: number; | |||||
[gradeId: number]: number; | |||||
}; | }; | ||||
}; | }; | ||||
}; | }; | ||||
@@ -0,0 +1,154 @@ | |||||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||||
import { TaskGroup } from "@/app/api/tasks"; | |||||
import { Add, Delete } from "@mui/icons-material"; | |||||
import { | |||||
Stack, | |||||
Typography, | |||||
Grid, | |||||
FormControl, | |||||
Box, | |||||
Button, | |||||
} from "@mui/material"; | |||||
import { | |||||
GridColDef, | |||||
GridActionsCellItem, | |||||
GridToolbarContainer, | |||||
} from "@mui/x-data-grid"; | |||||
import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; | |||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | |||||
import "dayjs/locale/zh-hk"; | |||||
import React, { useMemo } from "react"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import StyledDataGrid from "../StyledDataGrid"; | |||||
interface Props { | |||||
taskGroupId: TaskGroup["id"]; | |||||
} | |||||
interface FooterToolbarProps { | |||||
onAdd: () => void; | |||||
} | |||||
const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
const { t } = useTranslation(); | |||||
const {} = useFormContext<CreateProjectInputs>(); | |||||
const columns = useMemo<GridColDef[]>( | |||||
() => [ | |||||
{ | |||||
type: "actions", | |||||
field: "actions", | |||||
headerName: t("Actions"), | |||||
getActions: () => [ | |||||
<GridActionsCellItem | |||||
key="delete-action" | |||||
icon={<Delete />} | |||||
label={t("Remove")} | |||||
onClick={() => {}} | |||||
/>, | |||||
], | |||||
}, | |||||
{ | |||||
field: "description", | |||||
headerName: t("Payment Milestone Description"), | |||||
width: 300, | |||||
editable: true, | |||||
}, | |||||
{ | |||||
field: "date", | |||||
headerName: t("Payment Milestone Date"), | |||||
width: 200, | |||||
type: "date", | |||||
editable: true, | |||||
}, | |||||
{ | |||||
field: "amount", | |||||
headerName: t("Payment Milestone Amount"), | |||||
width: 300, | |||||
editable: true, | |||||
type: "number", | |||||
}, | |||||
], | |||||
[t], | |||||
); | |||||
return ( | |||||
<Stack gap={1}> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Milestone")} | |||||
</Typography> | |||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs> | |||||
<FormControl fullWidth> | |||||
<DatePicker | |||||
label={t("Stage Start Date")} | |||||
defaultValue={dayjs()} | |||||
/> | |||||
</FormControl> | |||||
</Grid> | |||||
<Grid item xs> | |||||
<FormControl fullWidth> | |||||
<DatePicker label={t("Stage End Date")} defaultValue={dayjs()} /> | |||||
</FormControl> | |||||
</Grid> | |||||
</Grid> | |||||
</LocalizationProvider> | |||||
<Box | |||||
sx={(theme) => ({ | |||||
marginBlockStart: 1, | |||||
marginInline: -3, | |||||
borderBottom: `1px solid ${theme.palette.divider}`, | |||||
})} | |||||
> | |||||
<StyledDataGrid | |||||
autoHeight | |||||
sx={{ "--DataGrid-overlayHeight": "100px" }} | |||||
disableColumnMenu | |||||
rows={[]} | |||||
columns={columns} | |||||
slots={{ | |||||
footer: FooterToolbar, | |||||
noRowsOverlay: NoRowsOverlay, | |||||
}} | |||||
slotProps={{ | |||||
footer: undefined, | |||||
}} | |||||
/> | |||||
</Box> | |||||
</Stack> | |||||
); | |||||
}; | |||||
const NoRowsOverlay: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<Box | |||||
display="flex" | |||||
justifyContent="center" | |||||
alignItems="center" | |||||
height="100%" | |||||
> | |||||
<Typography variant="caption">{t("Add some milestones!")}</Typography> | |||||
</Box> | |||||
); | |||||
}; | |||||
const FooterToolbar: React.FC<FooterToolbarProps> = ({ onAdd }) => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<GridToolbarContainer sx={{ p: 2 }}> | |||||
<Button | |||||
variant="outlined" | |||||
startIcon={<Add />} | |||||
onClick={onAdd} | |||||
size="small" | |||||
> | |||||
{t("Add Milestone")} | |||||
</Button> | |||||
</GridToolbarContainer> | |||||
); | |||||
}; | |||||
export default MilestoneSection; |
@@ -5,43 +5,29 @@ import CardContent from "@mui/material/CardContent"; | |||||
import Typography from "@mui/material/Typography"; | 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, useEffect, useMemo, useState } from "react"; | |||||
import React, { useCallback, useMemo, useState } from "react"; | |||||
import CardActions from "@mui/material/CardActions"; | import CardActions from "@mui/material/CardActions"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import { | import { | ||||
Alert, | Alert, | ||||
Box, | |||||
FormControl, | FormControl, | ||||
Grid, | |||||
InputLabel, | InputLabel, | ||||
List, | |||||
ListItemButton, | |||||
ListItemText, | |||||
MenuItem, | MenuItem, | ||||
Paper, | |||||
Select, | Select, | ||||
SelectChangeEvent, | SelectChangeEvent, | ||||
Stack, | Stack, | ||||
TextField, | |||||
} 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 { moneyFormatter } from "@/app/utils/formatUtil"; | ||||
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | |||||
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 ResourceSection from "./ResourceSection"; | |||||
interface Props { | |||||
export interface Props { | |||||
allTasks: Task[]; | allTasks: Task[]; | ||||
} | |||||
interface ResourceSectionProps { | |||||
tasks: Task[]; | |||||
defaultManhourBreakdownByGrade: { [grade: string]: number }; | |||||
onSetManhours: (hours: number, taskId: Task["id"]) => void; | |||||
onAllocateManhours: () => void; | |||||
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; | |||||
} | } | ||||
const ResourceMilestone: React.FC<Props> = ({ allTasks }) => { | const ResourceMilestone: React.FC<Props> = ({ allTasks }) => { | ||||
@@ -97,7 +83,7 @@ const ResourceMilestone: React.FC<Props> = ({ allTasks }) => { | |||||
onSetManhours={() => {}} | onSetManhours={() => {}} | ||||
onAllocateManhours={() => {}} | onAllocateManhours={() => {}} | ||||
/> | /> | ||||
<MilestoneSection /> | |||||
<MilestoneSection taskGroupId={currentTaskGroupId} /> | |||||
<CardActions sx={{ justifyContent: "flex-end" }}> | <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
<Button variant="text" startIcon={<RestartAlt />}> | <Button variant="text" startIcon={<RestartAlt />}> | ||||
{t("Reset")} | {t("Reset")} | ||||
@@ -117,91 +103,12 @@ const ResourceMilestone: React.FC<Props> = ({ allTasks }) => { | |||||
); | ); | ||||
}; | }; | ||||
const ResourceSection: React.FC<ResourceSectionProps> = ({ | |||||
tasks, | |||||
onAllocateManhours, | |||||
onSetManhours, | |||||
defaultManhourBreakdownByGrade, | |||||
}) => { | |||||
const { t } = useTranslation(); | |||||
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id); | |||||
const makeOnTaskSelect = useCallback( | |||||
(taskId: Task["id"]): React.MouseEventHandler => | |||||
() => { | |||||
return setSelectedTaskId(taskId); | |||||
}, | |||||
[], | |||||
); | |||||
useEffect(() => { | |||||
setSelectedTaskId(tasks[0].id); | |||||
}, [tasks]); | |||||
return ( | |||||
<Box marginBlock={4}> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Resource")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<Paper elevation={2}> | |||||
<List dense sx={{ maxHeight: 300, overflow: "auto" }}> | |||||
{tasks.map((task, index) => { | |||||
return ( | |||||
<ListItemButton | |||||
selected={selectedTaskId === task.id} | |||||
key={`${task.id}-${index}`} | |||||
onClick={makeOnTaskSelect(task.id)} | |||||
> | |||||
<ListItemText primary={task.name} /> | |||||
</ListItemButton> | |||||
); | |||||
})} | |||||
</List> | |||||
</Paper> | |||||
</Grid> | |||||
<Grid item xs={6}> | |||||
<TextField label={t("Mahours Allocated to Task")} fullWidth /> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
); | |||||
}; | |||||
const MilestoneSection: React.FC = () => { | |||||
const { t } = useTranslation(); | |||||
return ( | |||||
<Box> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Milestone")} | |||||
</Typography> | |||||
<LocalizationProvider dateAdapter={AdapterDayjs}> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs> | |||||
<FormControl fullWidth> | |||||
<DatePicker | |||||
label={t("Stage Start Date")} | |||||
defaultValue={dayjs()} | |||||
/> | |||||
</FormControl> | |||||
</Grid> | |||||
<Grid item xs> | |||||
<FormControl fullWidth> | |||||
<DatePicker label={t("Stage End Date")} defaultValue={dayjs()} /> | |||||
</FormControl> | |||||
</Grid> | |||||
</Grid> | |||||
</LocalizationProvider> | |||||
</Box> | |||||
); | |||||
}; | |||||
const NoTaskState: React.FC = () => { | const NoTaskState: React.FC = () => { | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
return ( | return ( | ||||
<Card> | <Card> | ||||
<CardContent> | <CardContent> | ||||
<Alert severity="error"> | |||||
<Alert severity="warning"> | |||||
{t('Please add some tasks in "Project Task Setup" first!')} | {t('Please add some tasks in "Project Task Setup" first!')} | ||||
</Alert> | </Alert> | ||||
</CardContent> | </CardContent> | ||||
@@ -0,0 +1,74 @@ | |||||
import { Task } from "@/app/api/tasks"; | |||||
import { | |||||
Box, | |||||
Typography, | |||||
Grid, | |||||
Paper, | |||||
List, | |||||
ListItemButton, | |||||
ListItemText, | |||||
TextField, | |||||
} from "@mui/material"; | |||||
import { useState, useCallback, useEffect } from "react"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import { Props as ResourceMilestoneProps } from "./ResourceMilestone"; | |||||
interface Props { | |||||
tasks: Task[]; | |||||
defaultManhourBreakdownByGrade: ResourceMilestoneProps["defaultManhourBreakdownByGrade"]; | |||||
onSetManhours: (hours: number, taskId: Task["id"]) => void; | |||||
onAllocateManhours: () => void; | |||||
} | |||||
const ResourceSection: React.FC<Props> = ({ | |||||
tasks, | |||||
onAllocateManhours, | |||||
onSetManhours, | |||||
defaultManhourBreakdownByGrade, | |||||
}) => { | |||||
const { t } = useTranslation(); | |||||
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id); | |||||
const makeOnTaskSelect = useCallback( | |||||
(taskId: Task["id"]): React.MouseEventHandler => | |||||
() => { | |||||
return setSelectedTaskId(taskId); | |||||
}, | |||||
[], | |||||
); | |||||
useEffect(() => { | |||||
setSelectedTaskId(tasks[0].id); | |||||
}, [tasks]); | |||||
return ( | |||||
<Box marginBlock={4}> | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
{t("Resource")} | |||||
</Typography> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
<Grid item xs={6}> | |||||
<Paper elevation={2}> | |||||
<List dense sx={{ maxHeight: 300, overflow: "auto" }}> | |||||
{tasks.map((task, index) => { | |||||
return ( | |||||
<ListItemButton | |||||
selected={selectedTaskId === task.id} | |||||
key={`${task.id}-${index}`} | |||||
onClick={makeOnTaskSelect(task.id)} | |||||
> | |||||
<ListItemText primary={task.name} /> | |||||
</ListItemButton> | |||||
); | |||||
})} | |||||
</List> | |||||
</Paper> | |||||
</Grid> | |||||
<Grid item xs={6}> | |||||
<TextField label={t("Mahours Allocated to Task")} fullWidth /> | |||||
</Grid> | |||||
</Grid> | |||||
</Box> | |||||
); | |||||
}; | |||||
export default ResourceSection; |
@@ -0,0 +1,22 @@ | |||||
import { styled } from "@mui/material"; | |||||
import { DataGrid } from "@mui/x-data-grid"; | |||||
const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ | |||||
"--unstable_DataGrid-radius": 0, | |||||
"& .MuiDataGrid-columnHeaders": { | |||||
backgroundColor: theme.palette.grey[50], | |||||
}, | |||||
"& .MuiDataGrid-columnHeaderTitle": { | |||||
color: theme.palette.grey[700], | |||||
fontSize: 12, | |||||
fontWeight: 600, | |||||
lineHeight: 2, | |||||
letterSpacing: 0.5, | |||||
textTransform: "uppercase", | |||||
}, | |||||
"& .MuiDataGrid-columnSeparator": { | |||||
color: theme.palette.primary.main, | |||||
}, | |||||
})); | |||||
export default StyledDataGrid; |
@@ -0,0 +1 @@ | |||||
export { default } from "./StyledDataGrid"; |
@@ -340,6 +340,10 @@ const components: ThemeOptions["components"] = { | |||||
padding: { | padding: { | ||||
paddingBlock: "1rem", | paddingBlock: "1rem", | ||||
paddingInline: "1rem", | paddingInline: "1rem", | ||||
"&.MuiDataGrid-menuList": { | |||||
paddingBlock: "0.25rem", | |||||
paddingInline: "0.25rem", | |||||
}, | |||||
}, | }, | ||||
}, | }, | ||||
}, | }, | ||||
@@ -28,6 +28,7 @@ const palette = { | |||||
}, | }, | ||||
warning, | warning, | ||||
neutral, | neutral, | ||||
grey: neutral, | |||||
}; | }; | ||||
export const paletteOptions: PaletteOptions = { ...palette, mode: "light" }; | export const paletteOptions: PaletteOptions = { ...palette, mode: "light" }; | ||||