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