|
- "use client";
-
- import { useTranslation } from "react-i18next";
- import React, { useEffect } 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 differenceBy from "lodash/differenceBy";
- import uniq from "lodash/uniq";
- import ResourceCapacity from "./ResourceCapacity";
- import { useFormContext } from "react-hook-form";
- import { CreateProjectInputs } from "@/app/api/projects/actions";
-
- interface StaffResult {
- id: number;
- name: string;
- team: string;
- grade: string;
- title: string;
- }
-
- const mockStaffs: StaffResult[] = [
- {
- name: "Albert",
- grade: "1",
- id: 1,
- team: "ABC",
- title: "Associate Quantity Surveyor",
- },
- {
- name: "Bernard",
- grade: "2",
- id: 2,
- team: "ABC",
- title: "Quantity Surveyor",
- },
- {
- name: "Carl",
- grade: "3",
- id: 3,
- team: "XYZ",
- title: "Senior Quantity Surveyor",
- },
- { name: "Denis", grade: "4", id: 4, team: "ABC", title: "Manager" },
- { name: "Edward", grade: "5", id: 5, team: "ABC", title: "Director" },
- { name: "Fred", grade: "1", id: 6, team: "XYZ", title: "General Laborer" },
- { name: "Gordon", grade: "2", id: 7, team: "ABC", title: "Inspector" },
- {
- name: "Heather",
- grade: "3",
- id: 8,
- team: "XYZ",
- title: "Field Engineer",
- },
- { name: "Ivan", grade: "4", id: 9, team: "ABC", title: "Senior Manager" },
- {
- name: "Jackson",
- grade: "5",
- id: 10,
- team: "XYZ",
- title: "Senior Director",
- },
- {
- name: "Kurt",
- grade: "1",
- id: 11,
- team: "ABC",
- title: "Construction Assistant",
- },
- { name: "Lawrence", grade: "2", id: 12, team: "ABC", title: "Operator" },
- ];
-
- interface Props {
- allStaff?: StaffResult[];
- isActive: boolean;
- }
-
- const StaffAllocation: React.FC<Props> = ({
- allStaff = mockStaffs,
- isActive,
- }) => {
- const { t } = useTranslation();
- const { setValue, getValues } = useFormContext<CreateProjectInputs>();
-
- const [filteredStaff, setFilteredStaff] = React.useState(allStaff);
- const [selectedStaff, setSelectedStaff] = React.useState<
- typeof filteredStaff
- >(
- allStaff.filter((staff) =>
- getValues("allocatedStaffIds").includes(staff.id),
- ),
- );
-
- // 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([]);
- }, []);
- // Sync with form
- useEffect(() => {
- setValue(
- "allocatedStaffIds",
- selectedStaff.map((staff) => staff.id),
- );
- }, [selectedStaff, setValue]);
-
- const staffPoolColumns = React.useMemo<Column<StaffResult>[]>(
- () => [
- {
- label: t("Add"),
- name: "id",
- onClick: addStaff,
- buttonIcon: <PersonAdd />,
- },
- { label: t("Staff ID"), name: "id" },
- { label: t("Staff Name"), name: "name" },
- { label: t("Team"), name: "team" },
- { label: t("Grade"), name: "grade" },
- { label: t("Title"), name: "title" },
- ],
- [addStaff, t],
- );
-
- const allocatedStaffColumns = React.useMemo<Column<StaffResult>[]>(
- () => [
- {
- label: t("Remove"),
- name: "id",
- onClick: removeStaff,
- buttonIcon: <PersonRemove />,
- },
- { label: t("Staff ID"), name: "id" },
- { label: t("Staff Name"), name: "name" },
- { label: t("Team"), name: "team" },
- { label: t("Grade"), name: "grade" },
- { label: t("Title"), name: "title" },
- ],
- [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(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) => {
- const q = query.toLowerCase();
- return (
- (staff.name.toLowerCase().includes(q) ||
- staff.id.toString().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, 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 }}>
- <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>
- <Card sx={{ display: isActive ? "block" : "none" }}>
- <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
- <ResourceCapacity />
- </CardContent>
- </Card>
- </>
- );
- };
-
- export default StaffAllocation;
|