|
- "use client";
-
- import { useTranslation } from "react-i18next";
- import React, { useEffect, useMemo } from "react";
- import RestartAlt from "@mui/icons-material/RestartAlt";
- import SearchResults, { Column } from "../SearchResults";
- import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material";
- import {
- Stack,
- Typography,
- Grid,
- TextField,
- InputAdornment,
- IconButton,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- Box,
- Button,
- Card,
- CardActions,
- CardContent,
- TabsProps,
- Tab,
- Tabs,
- SelectChangeEvent,
- } from "@mui/material";
- import differenceWith from "lodash/differenceWith";
- import intersectionWith from "lodash/intersectionWith";
- import uniq from "lodash/uniq";
- import ResourceCapacity from "./ResourceCapacity";
- import { useFormContext } from "react-hook-form";
- import { CreateProjectInputs } from "@/app/api/projects/actions";
- import ResourceAllocation from "./ResourceAllocation";
- import { Task } from "@/app/api/tasks";
- import { Grade } from "@/app/api/grades";
- import { StaffResult } from "@/app/api/staff";
-
- const staffComparator = (a: StaffResult, b: StaffResult) => {
- return (
- a.team?.localeCompare(b.team) ||
- a.grade?.localeCompare(b.grade) ||
- a.id - b.id
- );
- };
-
- export interface Props {
- allStaffs: StaffResult[];
- isActive: boolean;
- defaultManhourBreakdownByGrade?: { [gradeId: number]: number };
- allTasks: Task[];
- grades: Grade[];
- }
-
- const StaffAllocation: React.FC<Props> = ({
- allStaffs,
- allTasks,
- isActive,
- defaultManhourBreakdownByGrade,
- grades,
- }) => {
- const { t } = useTranslation();
- const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>();
-
- const [filteredStaff, setFilteredStaff] = React.useState(
- allStaffs.sort(staffComparator),
- );
- const selectedStaffIds = watch("allocatedStaffIds");
-
- // Adding / Removing staff
- const addStaff = React.useCallback(
- (staff: StaffResult) => {
- const currentStaffIds = getValues("allocatedStaffIds");
- setValue("allocatedStaffIds", [...currentStaffIds, staff.id]);
- },
- [getValues, setValue],
- );
- const removeStaff = React.useCallback(
- (staff: StaffResult) => {
- const currentStaffIds = getValues("allocatedStaffIds");
- setValue(
- "allocatedStaffIds",
- currentStaffIds.filter((id) => id !== staff.id),
- );
- },
- [getValues, setValue],
- );
- const clearStaff = React.useCallback(() => {
- setValue("allocatedStaffIds", []);
- }, [setValue]);
-
- const staffPoolColumns = React.useMemo<Column<StaffResult>[]>(
- () => [
- {
- label: t("Add"),
- name: "id",
- onClick: addStaff,
- buttonIcon: <PersonAdd />,
- },
- { label: t("Team"), name: "team" },
- { label: t("Grade"), name: "grade" },
- { label: t("Staff ID"), name: "staffId" },
- { label: t("Staff Name"), name: "name" },
- { label: t("Title"), name: "currentPosition" },
- ],
- [addStaff, t],
- );
-
- const allocatedStaffColumns = React.useMemo<Column<StaffResult>[]>(
- () => [
- {
- label: t("Remove"),
- name: "id",
- onClick: removeStaff,
- buttonIcon: <PersonRemove />,
- },
- { label: t("Team"), name: "team" },
- { label: t("Grade"), name: "grade" },
- { label: t("Staff ID"), name: "id" },
- { label: t("Staff Name"), name: "name" },
- { label: t("Title"), name: "currentPosition" },
- ],
- [removeStaff, t],
- );
-
- // Query related
- const [query, setQuery] = React.useState("");
- const onQueryInputChange = React.useCallback<
- React.ChangeEventHandler<HTMLInputElement>
- >((e) => {
- setQuery(e.target.value);
- }, []);
- 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(allStaffs.map((staff) => staff[filter])),
- };
- },
- {},
- );
- }, [columnFilters, allStaffs]);
- 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 }));
- },
- [],
- );
-
- useEffect(() => {
- setFilteredStaff(
- allStaffs.filter((staff) => {
- const q = query.toLowerCase();
- return (
- (staff.name.toLowerCase().includes(q) ||
- staff.id.toString().includes(q) ||
- staff.currentPosition.toLowerCase().includes(q)) &&
- Object.entries(filters).every(([filterKey, filterValue]) => {
- const staffColumnValue = staff[filterKey as keyof StaffResult];
- return staffColumnValue === filterValue || filterValue === "All";
- })
- );
- }),
- );
- }, [allStaffs, filters, query]);
-
- // Tab related
- const [tabIndex, setTabIndex] = React.useState(0);
- const handleTabChange = React.useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- const reset = React.useCallback(() => {
- clearQueryInput();
- clearStaff();
- setFilters(defaultFilterValues);
- }, [clearQueryInput, clearStaff, defaultFilterValues]);
-
- return (
- <>
- <Card sx={{ display: isActive ? "block" : "none" }}>
- <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
- <ResourceCapacity />
- </CardContent>
- </Card>
- <Card sx={{ display: isActive ? "block" : "none" }}>
- <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")} (${selectedStaffIds.length})`}
- />
- </Tabs>
- <Box sx={{ marginInline: -3 }}>
- {tabIndex === 0 && (
- <SearchResults
- noWrapper
- items={differenceWith(
- filteredStaff,
- selectedStaffIds,
- (staff, staffId) => staff.id === staffId,
- )}
- columns={staffPoolColumns}
- />
- )}
- {tabIndex === 1 && (
- <SearchResults
- noWrapper
- items={intersectionWith(
- allStaffs,
- selectedStaffIds,
- (staff, staffId) => staff.id === staffId,
- )}
- columns={allocatedStaffColumns}
- />
- )}
- </Box>
- </Stack>
- <CardActions sx={{ justifyContent: "flex-end" }}>
- <Button variant="text" startIcon={<RestartAlt />} onClick={reset}>
- {t("Reset")}
- </Button>
- </CardActions>
- </CardContent>
- </Card>
- {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
- {isActive && (
- <Card>
- <CardContent>
- <ResourceAllocation
- grades={grades}
- allTasks={allTasks}
- manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
- />
- </CardContent>
- </Card>
- )}
- </>
- );
- };
-
- export default StaffAllocation;
|