| @@ -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: { | ||||