@@ -34,3 +34,5 @@ yarn-error.log* | |||
# typescript | |||
*.tsbuildinfo | |||
next-env.d.ts | |||
.vscode |
@@ -23,7 +23,7 @@ export interface CreateProjectInputs { | |||
tasks: { | |||
[taskId: Task["id"]]: { | |||
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 { useTranslation } from "react-i18next"; | |||
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 RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import { | |||
Alert, | |||
Box, | |||
FormControl, | |||
Grid, | |||
InputLabel, | |||
List, | |||
ListItemButton, | |||
ListItemText, | |||
MenuItem, | |||
Paper, | |||
Select, | |||
SelectChangeEvent, | |||
Stack, | |||
TextField, | |||
} from "@mui/material"; | |||
import { Task, TaskGroup } from "@/app/api/tasks"; | |||
import uniqBy from "lodash/uniqBy"; | |||
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 { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
import MilestoneSection from "./MilestoneSection"; | |||
import ResourceSection from "./ResourceSection"; | |||
interface Props { | |||
export interface Props { | |||
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 }) => { | |||
@@ -97,7 +83,7 @@ const ResourceMilestone: React.FC<Props> = ({ allTasks }) => { | |||
onSetManhours={() => {}} | |||
onAllocateManhours={() => {}} | |||
/> | |||
<MilestoneSection /> | |||
<MilestoneSection taskGroupId={currentTaskGroupId} /> | |||
<CardActions sx={{ justifyContent: "flex-end" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{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 { t } = useTranslation(); | |||
return ( | |||
<Card> | |||
<CardContent> | |||
<Alert severity="error"> | |||
<Alert severity="warning"> | |||
{t('Please add some tasks in "Project Task Setup" first!')} | |||
</Alert> | |||
</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: { | |||
paddingBlock: "1rem", | |||
paddingInline: "1rem", | |||
"&.MuiDataGrid-menuList": { | |||
paddingBlock: "0.25rem", | |||
paddingInline: "0.25rem", | |||
}, | |||
}, | |||
}, | |||
}, | |||
@@ -28,6 +28,7 @@ const palette = { | |||
}, | |||
warning, | |||
neutral, | |||
grey: neutral, | |||
}; | |||
export const paletteOptions: PaletteOptions = { ...palette, mode: "light" }; | |||