| @@ -12,7 +12,7 @@ export interface Task { | |||
| id: number; | |||
| name: string; | |||
| description: string | null; | |||
| taskGroup: TaskGroup | null; | |||
| taskGroup: TaskGroup; | |||
| } | |||
| export interface TaskTemplate { | |||
| @@ -0,0 +1,9 @@ | |||
| export const manhourFormatter = new Intl.NumberFormat("en-HK", { | |||
| minimumFractionDigits: 2, | |||
| maximumFractionDigits: 2, | |||
| }); | |||
| export const moneyFormatter = new Intl.NumberFormat("en-HK", { | |||
| style: "currency", | |||
| currency: "HKD", | |||
| }); | |||
| @@ -13,8 +13,13 @@ import ProjectClientDetails from "./ProjectClientDetails"; | |||
| import TaskSetup from "./TaskSetup"; | |||
| import StaffAllocation from "./StaffAllocation"; | |||
| import ResourceMilestone from "./ResourceMilestone"; | |||
| import { Task } from "@/app/api/tasks"; | |||
| const CreateProject: React.FC = () => { | |||
| export interface Props { | |||
| mockTasks: Task[]; | |||
| } | |||
| const CreateProject: React.FC<Props> = ({ mockTasks }) => { | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { t } = useTranslation(); | |||
| const router = useRouter(); | |||
| @@ -41,7 +46,7 @@ const CreateProject: React.FC = () => { | |||
| {tabIndex === 0 && <ProjectClientDetails />} | |||
| {tabIndex === 1 && <TaskSetup />} | |||
| {tabIndex === 2 && <StaffAllocation initiallySelectedStaff={[]} />} | |||
| {tabIndex === 3 && <ResourceMilestone />} | |||
| {tabIndex === 3 && <ResourceMilestone tasks={mockTasks} />} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | |||
| {t("Cancel")} | |||
| @@ -0,0 +1,10 @@ | |||
| import { fetchAllTasks } from "@/app/api/tasks"; | |||
| import CreateProject from "./CreateProject"; | |||
| const CreateProjectWrapper: React.FC = async () => { | |||
| const tasks = await fetchAllTasks(); | |||
| return <CreateProject mockTasks={tasks} />; | |||
| }; | |||
| export default CreateProjectWrapper; | |||
| @@ -0,0 +1,149 @@ | |||
| import { manhourFormatter } from "@/app/utils/formatUtil"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Stack, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| const mockItems: ResourceItem[] = [ | |||
| { | |||
| grade: "Grade 1", | |||
| title: "A. QS / QS Trainee", | |||
| headcount: 20, | |||
| totalAvailableManhours: 39520, | |||
| loadedManhours: 3760, | |||
| remainingAvailableManhours: 35760, | |||
| }, | |||
| { | |||
| grade: "Grade 2", | |||
| title: "QS", | |||
| headcount: 20, | |||
| totalAvailableManhours: 39520, | |||
| loadedManhours: 3760, | |||
| remainingAvailableManhours: 35760, | |||
| }, | |||
| { | |||
| grade: "Grade 3", | |||
| title: "Senior QS", | |||
| headcount: 10, | |||
| totalAvailableManhours: 19760, | |||
| loadedManhours: 1530, | |||
| remainingAvailableManhours: 18230, | |||
| }, | |||
| { | |||
| grade: "Grade 4", | |||
| title: "A. Manager / Deputy Manager / Manager / S. Manager", | |||
| headcount: 5, | |||
| totalAvailableManhours: 9880, | |||
| loadedManhours: 2760, | |||
| remainingAvailableManhours: 7120, | |||
| }, | |||
| { | |||
| grade: "Grade 5", | |||
| title: "A. Director / Deputy Director / Director", | |||
| headcount: 20, | |||
| totalAvailableManhours: 1976, | |||
| loadedManhours: 374, | |||
| remainingAvailableManhours: 1602, | |||
| }, | |||
| ]; | |||
| interface ResourceColumn { | |||
| label: string; | |||
| name: keyof ResourceItem; | |||
| } | |||
| interface ResourceItem { | |||
| grade: string; | |||
| title: string; | |||
| headcount: number; | |||
| totalAvailableManhours: number; | |||
| loadedManhours: number; | |||
| remainingAvailableManhours: number; | |||
| } | |||
| interface Props { | |||
| items?: ResourceItem[]; | |||
| } | |||
| const ResourceCapacity: React.FC<Props> = ({ items = mockItems }) => { | |||
| const { t } = useTranslation(); | |||
| const columns = useMemo<ResourceColumn[]>( | |||
| () => [ | |||
| { label: t("Grade"), name: "grade" }, | |||
| { label: t("Title"), name: "title" }, | |||
| { label: t("Headcount"), name: "headcount" }, | |||
| { label: t("Total Available Manhours"), name: "totalAvailableManhours" }, | |||
| { label: t("Loaded Manhours"), name: "loadedManhours" }, | |||
| { | |||
| label: t("Remaining Available Manhours"), | |||
| name: "remainingAvailableManhours", | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| 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]; | |||
| return ( | |||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||
| {columnName !== "headcount" && | |||
| typeof cellData === "number" | |||
| ? manhourFormatter.format(cellData) | |||
| : cellData} | |||
| </TableCell> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| ); | |||
| })} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| </Box> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ResourceCapacity; | |||
| @@ -5,27 +5,132 @@ 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 from "react"; | |||
| import React, { useCallback, useMemo, useState } from "react"; | |||
| import CardActions from "@mui/material/CardActions"; | |||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| import { | |||
| Alert, | |||
| FormControl, | |||
| Grid, | |||
| InputLabel, | |||
| 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 { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import dayjs from "dayjs"; | |||
| const ResourceMilestone = () => { | |||
| interface Props { | |||
| tasks: Task[]; | |||
| } | |||
| const ResourceMilestone: React.FC<Props> = ({ tasks }) => { | |||
| const { t } = useTranslation(); | |||
| const taskGroups = useMemo(() => { | |||
| return uniqBy( | |||
| tasks.map((task) => task.taskGroup), | |||
| "id", | |||
| ); | |||
| }, [tasks]); | |||
| const [currentTaskGroupId, setCurrentTaskGroupId] = useState( | |||
| taskGroups[0].id, | |||
| ); | |||
| const onSelectTaskGroup = useCallback( | |||
| (event: SelectChangeEvent<TaskGroup["id"]>) => { | |||
| const id = event.target.value; | |||
| setCurrentTaskGroupId(typeof id === "string" ? parseInt(id) : id); | |||
| }, | |||
| [], | |||
| ); | |||
| return ( | |||
| <> | |||
| <Card> | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <FormControl> | |||
| <InputLabel>{t("Task Stage")}</InputLabel> | |||
| <Select | |||
| label={t("Task Stage")} | |||
| onChange={onSelectTaskGroup} | |||
| value={currentTaskGroupId} | |||
| > | |||
| {taskGroups.map((taskGroup) => ( | |||
| <MenuItem key={taskGroup.id} value={taskGroup.id}> | |||
| {taskGroup.name} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Resource")} | |||
| </Typography> | |||
| <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> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack direction="row" justifyContent="space-between"> | |||
| <Typography variant="h6">{t("Project Total Fee")}</Typography> | |||
| <Typography>{moneyFormatter.format(80000)}</Typography> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| const NoTaskState: React.FC = () => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Card> | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Resource and Milestone")} | |||
| </Typography> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> | |||
| <CardContent> | |||
| <Alert severity="error"> | |||
| {t('Please add some tasks in "Project Task Setup" first!')} | |||
| </Alert> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default ResourceMilestone; | |||
| const ResourceMilestoneWrapper: React.FC<Props> = (props) => { | |||
| if (props.tasks.length === 0) { | |||
| return <NoTaskState />; | |||
| } | |||
| return <ResourceMilestone {...props} />; | |||
| }; | |||
| export default ResourceMilestoneWrapper; | |||
| @@ -28,6 +28,7 @@ import { | |||
| } from "@mui/material"; | |||
| import differenceBy from "lodash/differenceBy"; | |||
| import uniq from "lodash/uniq"; | |||
| import ResourceCapacity from "./ResourceCapacity"; | |||
| interface StaffResult { | |||
| id: string; | |||
| @@ -222,87 +223,90 @@ const StaffAllocation: React.FC<Props> = ({ | |||
| }, [clearQueryInput, clearStaff, defaultFilterValues]); | |||
| return ( | |||
| <Card> | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <Stack gap={2}> | |||
| <Typography variant="overline" display="block"> | |||
| {t("Staff Allocation")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6} display="flex" alignItems="center"> | |||
| <Search sx={{ marginInlineEnd: 1 }} /> | |||
| <TextField | |||
| variant="standard" | |||
| fullWidth | |||
| onChange={onQueryInputChange} | |||
| value={query} | |||
| placeholder={t("Search by staff ID, name or title")} | |||
| InputProps={{ | |||
| endAdornment: query && ( | |||
| <InputAdornment position="end"> | |||
| <IconButton onClick={clearQueryInput}> | |||
| <Clear /> | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| </Grid> | |||
| {columnFilters.map((filter, idx) => { | |||
| const label = staffPoolColumns.find( | |||
| (c) => c.name === filter, | |||
| )!.label; | |||
| <> | |||
| <Card> | |||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
| <Stack gap={2}> | |||
| <Typography variant="overline" display="block"> | |||
| {t("Staff Allocation")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6} display="flex" alignItems="center"> | |||
| <Search sx={{ marginInlineEnd: 1 }} /> | |||
| <TextField | |||
| variant="standard" | |||
| fullWidth | |||
| onChange={onQueryInputChange} | |||
| value={query} | |||
| placeholder={t("Search by staff ID, name or title")} | |||
| InputProps={{ | |||
| endAdornment: query && ( | |||
| <InputAdornment position="end"> | |||
| <IconButton onClick={clearQueryInput}> | |||
| <Clear /> | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| </Grid> | |||
| {columnFilters.map((filter, idx) => { | |||
| const label = staffPoolColumns.find( | |||
| (c) => c.name === filter, | |||
| )!.label; | |||
| return ( | |||
| <Grid key={`${filter.toString()}-${idx}`} item xs={3}> | |||
| <FormControl fullWidth> | |||
| <InputLabel size="small">{label}</InputLabel> | |||
| <Select | |||
| label={label} | |||
| size="small" | |||
| value={filters[filter]} | |||
| onChange={makeFilterSelect(filter)} | |||
| > | |||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||
| {filterValues[filter]?.map((option, index) => ( | |||
| <MenuItem key={`${option}-${index}`} value={option}> | |||
| {option} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| ); | |||
| })} | |||
| </Grid> | |||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||
| <Tab label={t("Staff Pool")} /> | |||
| <Tab label={`${t("Allocated Staff")} (${selectedStaff.length})`} /> | |||
| </Tabs> | |||
| <Box sx={{ marginInline: -3 }}> | |||
| {tabIndex === 0 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={differenceBy(filteredStaff, selectedStaff, "id")} | |||
| columns={staffPoolColumns} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={selectedStaff} | |||
| columns={allocatedStaffColumns} | |||
| /> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />} onClick={reset}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| return ( | |||
| <Grid key={`${filter.toString()}-${idx}`} item xs={3}> | |||
| <FormControl fullWidth> | |||
| <InputLabel size="small">{label}</InputLabel> | |||
| <Select | |||
| label={label} | |||
| size="small" | |||
| value={filters[filter]} | |||
| onChange={makeFilterSelect(filter)} | |||
| > | |||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | |||
| {filterValues[filter]?.map((option, index) => ( | |||
| <MenuItem key={`${option}-${index}`} value={option}> | |||
| {option} | |||
| </MenuItem> | |||
| ))} | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| ); | |||
| })} | |||
| </Grid> | |||
| <Tabs value={tabIndex} onChange={handleTabChange}> | |||
| <Tab label={t("Staff Pool")} /> | |||
| <Tab label={`${t("Allocated Staff")} (${selectedStaff.length})`} /> | |||
| </Tabs> | |||
| <Box sx={{ marginInline: -3 }}> | |||
| {tabIndex === 0 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={differenceBy(filteredStaff, selectedStaff, "id")} | |||
| columns={staffPoolColumns} | |||
| /> | |||
| )} | |||
| {tabIndex === 1 && ( | |||
| <SearchResults | |||
| noWrapper | |||
| items={selectedStaff} | |||
| columns={allocatedStaffColumns} | |||
| /> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />} onClick={reset}> | |||
| {t("Reset")} | |||
| </Button> | |||
| </CardActions> | |||
| </CardContent> | |||
| </Card> | |||
| <ResourceCapacity /> | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -46,11 +46,11 @@ const TaskSetup = () => { | |||
| </Grid> | |||
| <TransferList | |||
| allItems={[ | |||
| { id: "1", label: "Task 1" }, | |||
| { id: "2", label: "Task 2" }, | |||
| { id: "3", label: "Task 3" }, | |||
| { id: "4", label: "Task 4" }, | |||
| { id: "5", label: "Task 5" }, | |||
| { id: 1, label: "Task 1" }, | |||
| { id: 2, label: "Task 2" }, | |||
| { id: 3, label: "Task 3" }, | |||
| { id: 4, label: "Task 4" }, | |||
| { id: 5, label: "Task 5" }, | |||
| ]} | |||
| initiallySelectedItems={[]} | |||
| onChange={() => {}} | |||
| @@ -1 +1 @@ | |||
| export { default } from "./CreateProject"; | |||
| export { default } from "./CreateProjectWrapper"; | |||
| @@ -15,10 +15,12 @@ import CardActions from "@mui/material/CardActions"; | |||
| import Button from "@mui/material/Button"; | |||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| import Search from "@mui/icons-material/Search"; | |||
| import dayjs from 'dayjs'; | |||
| import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | |||
| import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; | |||
| import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; | |||
| import dayjs from "dayjs"; | |||
| import "dayjs/locale/zh-hk"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { Box } from "@mui/material"; | |||
| interface BaseCriterion<T extends string> { | |||
| label: string; | |||
| @@ -40,7 +42,10 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> { | |||
| type: "dateRange"; | |||
| } | |||
| export type Criterion<T extends string> = TextCriterion<T> | SelectCriterion<T> | DateRangeCriterion<T>; | |||
| export type Criterion<T extends string> = | |||
| | TextCriterion<T> | |||
| | SelectCriterion<T> | |||
| | DateRangeCriterion<T>; | |||
| interface Props<T extends string> { | |||
| criteria: Criterion<T>[]; | |||
| @@ -54,7 +59,6 @@ function SearchBox<T extends string>({ | |||
| onReset, | |||
| }: Props<T>) { | |||
| const { t } = useTranslation("common"); | |||
| const [dayRangeFromDate, setDayRangeFromDate] :any = useState(""); | |||
| const defaultInputs = useMemo( | |||
| () => | |||
| criteria.reduce<Record<T, string>>( | |||
| @@ -82,23 +86,20 @@ function SearchBox<T extends string>({ | |||
| }; | |||
| }, []); | |||
| const makeDateChangeHandler = useCallback( | |||
| (paramName: T) => { | |||
| return (e:any) => { | |||
| setInputs((i) => ({ ...i, [paramName]: dayjs(e).format('YYYY-MM-DD') })); | |||
| const makeDateChangeHandler = useCallback((paramName: T) => { | |||
| return (e: any) => { | |||
| setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); | |||
| }; | |||
| }, | |||
| [], | |||
| ); | |||
| }, []); | |||
| const makeDateToChangeHandler = useCallback( | |||
| (paramName: T) => { | |||
| return (e:any) => { | |||
| setInputs((i) => ({ ...i, [paramName + "To"]: dayjs(e).format('YYYY-MM-DD') })); | |||
| const makeDateToChangeHandler = useCallback((paramName: T) => { | |||
| return (e: any) => { | |||
| setInputs((i) => ({ | |||
| ...i, | |||
| [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), | |||
| })); | |||
| }; | |||
| }, | |||
| [], | |||
| ); | |||
| }, []); | |||
| const handleReset = () => { | |||
| setInputs(defaultInputs); | |||
| @@ -143,33 +144,34 @@ function SearchBox<T extends string>({ | |||
| </FormControl> | |||
| )} | |||
| {c.type === "dateRange" && ( | |||
| <Grid container> | |||
| <Grid item xs={5.5} sm={5.5}> | |||
| <FormControl fullWidth> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <DatePicker | |||
| label={c.label} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| /> | |||
| </LocalizationProvider> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={1} sm={1} md={1} lg={1} sx={{ display: 'flex', justifyContent: "center", alignItems: 'center' }}> | |||
| - | |||
| </Grid> | |||
| <Grid item xs={5.5} sm={5.5}> | |||
| <FormControl fullWidth> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <DatePicker | |||
| label={c.label2} | |||
| onChange={makeDateToChangeHandler(c.paramName)} | |||
| /> | |||
| </LocalizationProvider> | |||
| </FormControl> | |||
| </Grid> | |||
| </Grid> | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD | |||
| adapterLocale="zh-hk" | |||
| > | |||
| <Box display="flex"> | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| label={c.label} | |||
| onChange={makeDateChangeHandler(c.paramName)} | |||
| /> | |||
| </FormControl> | |||
| <Box | |||
| display="flex" | |||
| alignItems="center" | |||
| justifyContent="center" | |||
| marginInline={2} | |||
| > | |||
| {"-"} | |||
| </Box> | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| label={c.label2} | |||
| onChange={makeDateToChangeHandler(c.paramName)} | |||
| /> | |||
| </FormControl> | |||
| </Box> | |||
| </LocalizationProvider> | |||
| )} | |||
| </Grid> | |||
| ); | |||
| @@ -47,6 +47,13 @@ const components: ThemeOptions["components"] = { | |||
| }, | |||
| }, | |||
| }, | |||
| MuiAlert: { | |||
| styleOverrides: { | |||
| root: { | |||
| borderRadius: 8, | |||
| }, | |||
| }, | |||
| }, | |||
| MuiPaper: { | |||
| styleOverrides: { | |||
| rounded: { | |||