@@ -12,7 +12,7 @@ export interface Task { | |||||
id: number; | id: number; | ||||
name: string; | name: string; | ||||
description: string | null; | description: string | null; | ||||
taskGroup: TaskGroup | null; | |||||
taskGroup: TaskGroup; | |||||
} | } | ||||
export interface TaskTemplate { | 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 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"; | |||||
const CreateProject: React.FC = () => { | |||||
export interface Props { | |||||
mockTasks: Task[]; | |||||
} | |||||
const CreateProject: React.FC<Props> = ({ mockTasks }) => { | |||||
const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -41,7 +46,7 @@ const CreateProject: React.FC = () => { | |||||
{tabIndex === 0 && <ProjectClientDetails />} | {tabIndex === 0 && <ProjectClientDetails />} | ||||
{tabIndex === 1 && <TaskSetup />} | {tabIndex === 1 && <TaskSetup />} | ||||
{tabIndex === 2 && <StaffAllocation initiallySelectedStaff={[]} />} | {tabIndex === 2 && <StaffAllocation initiallySelectedStaff={[]} />} | ||||
{tabIndex === 3 && <ResourceMilestone />} | |||||
{tabIndex === 3 && <ResourceMilestone tasks={mockTasks} />} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | <Button variant="outlined" startIcon={<Close />} onClick={handleCancel}> | ||||
{t("Cancel")} | {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 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 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 { | |||||
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 { 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 ( | return ( | ||||
<Card> | <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> | </CardContent> | ||||
</Card> | </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"; | } from "@mui/material"; | ||||
import differenceBy from "lodash/differenceBy"; | import differenceBy from "lodash/differenceBy"; | ||||
import uniq from "lodash/uniq"; | import uniq from "lodash/uniq"; | ||||
import ResourceCapacity from "./ResourceCapacity"; | |||||
interface StaffResult { | interface StaffResult { | ||||
id: string; | id: string; | ||||
@@ -222,87 +223,90 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
}, [clearQueryInput, clearStaff, defaultFilterValues]); | }, [clearQueryInput, clearStaff, defaultFilterValues]); | ||||
return ( | 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> | </Grid> | ||||
<TransferList | <TransferList | ||||
allItems={[ | 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={[]} | initiallySelectedItems={[]} | ||||
onChange={() => {}} | 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 Button from "@mui/material/Button"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import Search from "@mui/icons-material/Search"; | 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> { | interface BaseCriterion<T extends string> { | ||||
label: string; | label: string; | ||||
@@ -40,7 +42,10 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> { | |||||
type: "dateRange"; | 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> { | interface Props<T extends string> { | ||||
criteria: Criterion<T>[]; | criteria: Criterion<T>[]; | ||||
@@ -54,7 +59,6 @@ function SearchBox<T extends string>({ | |||||
onReset, | onReset, | ||||
}: Props<T>) { | }: Props<T>) { | ||||
const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
const [dayRangeFromDate, setDayRangeFromDate] :any = useState(""); | |||||
const defaultInputs = useMemo( | const defaultInputs = useMemo( | ||||
() => | () => | ||||
criteria.reduce<Record<T, string>>( | 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 = () => { | const handleReset = () => { | ||||
setInputs(defaultInputs); | setInputs(defaultInputs); | ||||
@@ -143,33 +144,34 @@ function SearchBox<T extends string>({ | |||||
</FormControl> | </FormControl> | ||||
)} | )} | ||||
{c.type === "dateRange" && ( | {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> | </Grid> | ||||
); | ); | ||||
@@ -47,6 +47,13 @@ const components: ThemeOptions["components"] = { | |||||
}, | }, | ||||
}, | }, | ||||
}, | }, | ||||
MuiAlert: { | |||||
styleOverrides: { | |||||
root: { | |||||
borderRadius: 8, | |||||
}, | |||||
}, | |||||
}, | |||||
MuiPaper: { | MuiPaper: { | ||||
styleOverrides: { | styleOverrides: { | ||||
rounded: { | rounded: { | ||||