| @@ -24,8 +24,10 @@ import { | |||||
| TabsProps, | TabsProps, | ||||
| Tab, | Tab, | ||||
| Tabs, | Tabs, | ||||
| SelectChangeEvent, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import differenceBy from "lodash/differenceBy"; | import differenceBy from "lodash/differenceBy"; | ||||
| import uniq from "lodash/uniq"; | |||||
| interface StaffResult { | interface StaffResult { | ||||
| id: string; | id: string; | ||||
| @@ -100,16 +102,17 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| const [selectedStaff, setSelectedStaff] = React.useState< | const [selectedStaff, setSelectedStaff] = React.useState< | ||||
| typeof filteredStaff | typeof filteredStaff | ||||
| >(initiallySelectedStaff); | >(initiallySelectedStaff); | ||||
| const filters = React.useMemo<(keyof StaffResult)[]>( | |||||
| () => ["team", "grade"], | |||||
| [], | |||||
| ); | |||||
| // Adding / Removing staff | |||||
| const addStaff = React.useCallback((staff: StaffResult) => { | const addStaff = React.useCallback((staff: StaffResult) => { | ||||
| setSelectedStaff((staffs) => [...staffs, staff]); | setSelectedStaff((staffs) => [...staffs, staff]); | ||||
| }, []); | }, []); | ||||
| const removeStaff = React.useCallback((staff: StaffResult) => { | const removeStaff = React.useCallback((staff: StaffResult) => { | ||||
| setSelectedStaff((staffs) => staffs.filter((s) => s.id !== staff.id)); | setSelectedStaff((staffs) => staffs.filter((s) => s.id !== staff.id)); | ||||
| }, []); | }, []); | ||||
| const clearStaff = React.useCallback(() => { | |||||
| setSelectedStaff([]); | |||||
| }, []); | |||||
| const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( | const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -145,6 +148,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| [removeStaff, t], | [removeStaff, t], | ||||
| ); | ); | ||||
| // Query related | |||||
| const [query, setQuery] = React.useState(""); | const [query, setQuery] = React.useState(""); | ||||
| const onQueryInputChange = React.useCallback< | const onQueryInputChange = React.useCallback< | ||||
| React.ChangeEventHandler<HTMLInputElement> | React.ChangeEventHandler<HTMLInputElement> | ||||
| @@ -154,18 +158,55 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| const clearQueryInput = React.useCallback(() => { | const clearQueryInput = React.useCallback(() => { | ||||
| setQuery(""); | setQuery(""); | ||||
| }, []); | }, []); | ||||
| const columnFilters = React.useMemo<(keyof StaffResult)[]>( | |||||
| () => ["team", "grade"], | |||||
| [], | |||||
| ); | |||||
| const filterValues = React.useMemo(() => { | |||||
| return columnFilters.reduce<{ [filter in keyof StaffResult]?: string[] }>( | |||||
| (acc, filter) => { | |||||
| return { | |||||
| ...acc, | |||||
| [filter]: uniq(allStaff.map((staff) => staff[filter])), | |||||
| }; | |||||
| }, | |||||
| {}, | |||||
| ); | |||||
| }, [columnFilters, allStaff]); | |||||
| const defaultFilterValues = React.useMemo(() => { | |||||
| return columnFilters.reduce<{ [filter in keyof StaffResult]?: string }>( | |||||
| (acc, filter) => { | |||||
| return { ...acc, [filter]: "All" }; | |||||
| }, | |||||
| {}, | |||||
| ); | |||||
| }, [columnFilters]); | |||||
| const [filters, setFilters] = React.useState(defaultFilterValues); | |||||
| const makeFilterSelect = React.useCallback( | |||||
| (filter: keyof StaffResult) => (event: SelectChangeEvent<string>) => { | |||||
| setFilters((f) => ({ ...f, [filter]: event.target.value })); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| setFilteredStaff( | setFilteredStaff( | ||||
| allStaff.filter( | |||||
| (staff) => | |||||
| staff.name.toLowerCase().includes(query) || | |||||
| staff.id.toLowerCase().includes(query) || | |||||
| staff.title.toLowerCase().includes(query), | |||||
| ), | |||||
| allStaff.filter((staff) => { | |||||
| const q = query.toLowerCase(); | |||||
| return ( | |||||
| (staff.name.toLowerCase().includes(q) || | |||||
| staff.id.toLowerCase().includes(q) || | |||||
| staff.title.toLowerCase().includes(q)) && | |||||
| Object.entries(filters).every(([filterKey, filterValue]) => { | |||||
| const staffColumnValue = staff[filterKey as keyof StaffResult]; | |||||
| return staffColumnValue === filterValue || filterValue === "All"; | |||||
| }) | |||||
| ); | |||||
| }), | |||||
| ); | ); | ||||
| }, [allStaff, query]); | |||||
| }, [allStaff, filters, query]); | |||||
| // Tab related | |||||
| const [tabIndex, setTabIndex] = React.useState(0); | const [tabIndex, setTabIndex] = React.useState(0); | ||||
| const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
| (_e, newValue) => { | (_e, newValue) => { | ||||
| @@ -174,6 +215,12 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| [], | [], | ||||
| ); | ); | ||||
| const reset = React.useCallback(() => { | |||||
| clearQueryInput(); | |||||
| clearStaff(); | |||||
| setFilters(defaultFilterValues); | |||||
| }, [clearQueryInput, clearStaff, defaultFilterValues]); | |||||
| return ( | return ( | ||||
| <Card> | <Card> | ||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
| @@ -201,7 +248,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| }} | }} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| {filters.map((filter, idx) => { | |||||
| {columnFilters.map((filter, idx) => { | |||||
| const label = staffPoolColumns.find( | const label = staffPoolColumns.find( | ||||
| (c) => c.name === filter, | (c) => c.name === filter, | ||||
| )!.label; | )!.label; | ||||
| @@ -210,8 +257,18 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| <Grid key={`${filter.toString()}-${idx}`} item xs={3}> | <Grid key={`${filter.toString()}-${idx}`} item xs={3}> | ||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <InputLabel size="small">{label}</InputLabel> | <InputLabel size="small">{label}</InputLabel> | ||||
| <Select label={label} size="small"> | |||||
| <Select | |||||
| label={label} | |||||
| size="small" | |||||
| value={filters[filter]} | |||||
| onChange={makeFilterSelect(filter)} | |||||
| > | |||||
| <MenuItem value={"All"}>{t("All")}</MenuItem> | <MenuItem value={"All"}>{t("All")}</MenuItem> | ||||
| {filterValues[filter]?.map((option, index) => ( | |||||
| <MenuItem key={`${option}-${index}`} value={option}> | |||||
| {option} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | </Select> | ||||
| </FormControl> | </FormControl> | ||||
| </Grid> | </Grid> | ||||
| @@ -220,7 +277,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange}> | <Tabs value={tabIndex} onChange={handleTabChange}> | ||||
| <Tab label={t("Staff Pool")} /> | <Tab label={t("Staff Pool")} /> | ||||
| <Tab label={t("Allocated Staff")} /> | |||||
| <Tab label={`${t("Allocated Staff")} (${selectedStaff.length})`} /> | |||||
| </Tabs> | </Tabs> | ||||
| <Box sx={{ marginInline: -3 }}> | <Box sx={{ marginInline: -3 }}> | ||||
| {tabIndex === 0 && ( | {tabIndex === 0 && ( | ||||
| @@ -240,7 +297,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
| </Box> | </Box> | ||||
| </Stack> | </Stack> | ||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| <Button variant="text" startIcon={<RestartAlt />} onClick={reset}> | |||||
| {t("Reset")} | {t("Reset")} | ||||
| </Button> | </Button> | ||||
| </CardActions> | </CardActions> | ||||