|
@@ -1,7 +1,7 @@ |
|
|
"use client"; |
|
|
"use client"; |
|
|
|
|
|
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import React, { useEffect } from "react"; |
|
|
|
|
|
|
|
|
import React, { useEffect, useMemo } from "react"; |
|
|
import RestartAlt from "@mui/icons-material/RestartAlt"; |
|
|
import RestartAlt from "@mui/icons-material/RestartAlt"; |
|
|
import SearchResults, { Column } from "../SearchResults"; |
|
|
import SearchResults, { Column } from "../SearchResults"; |
|
|
import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; |
|
|
import { Search, Clear, PersonAdd, PersonRemove } from "@mui/icons-material"; |
|
@@ -26,7 +26,8 @@ import { |
|
|
Tabs, |
|
|
Tabs, |
|
|
SelectChangeEvent, |
|
|
SelectChangeEvent, |
|
|
} from "@mui/material"; |
|
|
} from "@mui/material"; |
|
|
import differenceBy from "lodash/differenceBy"; |
|
|
|
|
|
|
|
|
import differenceWith from "lodash/differenceWith"; |
|
|
|
|
|
import intersectionWith from "lodash/intersectionWith"; |
|
|
import uniq from "lodash/uniq"; |
|
|
import uniq from "lodash/uniq"; |
|
|
import ResourceCapacity from "./ResourceCapacity"; |
|
|
import ResourceCapacity from "./ResourceCapacity"; |
|
|
import { useFormContext } from "react-hook-form"; |
|
|
import { useFormContext } from "react-hook-form"; |
|
@@ -34,65 +35,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions"; |
|
|
import ResourceAllocation from "./ResourceAllocation"; |
|
|
import ResourceAllocation from "./ResourceAllocation"; |
|
|
import { Task } from "@/app/api/tasks"; |
|
|
import { Task } from "@/app/api/tasks"; |
|
|
import { Grade } from "@/app/api/grades"; |
|
|
import { Grade } from "@/app/api/grades"; |
|
|
|
|
|
|
|
|
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: "XYZ", |
|
|
|
|
|
title: "Construction Assistant", |
|
|
|
|
|
}, |
|
|
|
|
|
{ name: "Lawrence", grade: "2", id: 12, team: "ABC", title: "Operator" }, |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
import { StaffResult } from "@/app/api/staff"; |
|
|
|
|
|
|
|
|
const staffComparator = (a: StaffResult, b: StaffResult) => { |
|
|
const staffComparator = (a: StaffResult, b: StaffResult) => { |
|
|
return ( |
|
|
return ( |
|
@@ -103,7 +46,7 @@ const staffComparator = (a: StaffResult, b: StaffResult) => { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
export interface Props { |
|
|
export interface Props { |
|
|
allStaff?: StaffResult[]; |
|
|
|
|
|
|
|
|
allStaffs: StaffResult[]; |
|
|
isActive: boolean; |
|
|
isActive: boolean; |
|
|
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; |
|
|
defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; |
|
|
allTasks: Task[]; |
|
|
allTasks: Task[]; |
|
@@ -111,43 +54,50 @@ export interface Props { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const StaffAllocation: React.FC<Props> = ({ |
|
|
const StaffAllocation: React.FC<Props> = ({ |
|
|
allStaff = mockStaffs, |
|
|
|
|
|
|
|
|
allStaffs: dataStaffs, |
|
|
allTasks, |
|
|
allTasks, |
|
|
isActive, |
|
|
isActive, |
|
|
defaultManhourBreakdownByGrade, |
|
|
defaultManhourBreakdownByGrade, |
|
|
grades, |
|
|
grades, |
|
|
}) => { |
|
|
}) => { |
|
|
const { t } = useTranslation(); |
|
|
const { t } = useTranslation(); |
|
|
const { setValue, getValues } = useFormContext<CreateProjectInputs>(); |
|
|
|
|
|
|
|
|
const { setValue, getValues, watch } = useFormContext<CreateProjectInputs>(); |
|
|
|
|
|
|
|
|
|
|
|
// TODO: remove this when grade and positions are done |
|
|
|
|
|
const allStaffs = useMemo<StaffResult[]>(() => { |
|
|
|
|
|
return dataStaffs.map((staff, index) => ({ |
|
|
|
|
|
...staff, |
|
|
|
|
|
grade: grades[index % grades.length].name, |
|
|
|
|
|
currentPosition: `Mock Postion ${index}`, |
|
|
|
|
|
})); |
|
|
|
|
|
}, [dataStaffs, grades]); |
|
|
|
|
|
|
|
|
const [filteredStaff, setFilteredStaff] = React.useState( |
|
|
const [filteredStaff, setFilteredStaff] = React.useState( |
|
|
allStaff.sort(staffComparator), |
|
|
|
|
|
); |
|
|
|
|
|
const [selectedStaff, setSelectedStaff] = React.useState< |
|
|
|
|
|
typeof filteredStaff |
|
|
|
|
|
>( |
|
|
|
|
|
allStaff.filter((staff) => |
|
|
|
|
|
getValues("allocatedStaffIds").includes(staff.id), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
allStaffs.sort(staffComparator), |
|
|
); |
|
|
); |
|
|
|
|
|
const selectedStaffIds = watch("allocatedStaffIds"); |
|
|
|
|
|
|
|
|
// Adding / Removing staff |
|
|
// 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 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(() => { |
|
|
const clearStaff = React.useCallback(() => { |
|
|
setSelectedStaff([]); |
|
|
|
|
|
}, []); |
|
|
|
|
|
// Sync with form |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
setValue( |
|
|
|
|
|
"allocatedStaffIds", |
|
|
|
|
|
selectedStaff.map((staff) => staff.id), |
|
|
|
|
|
); |
|
|
|
|
|
}, [selectedStaff, setValue]); |
|
|
|
|
|
|
|
|
setValue("allocatedStaffIds", []); |
|
|
|
|
|
}, [setValue]); |
|
|
|
|
|
|
|
|
const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( |
|
|
const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( |
|
|
() => [ |
|
|
() => [ |
|
@@ -159,9 +109,9 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
}, |
|
|
}, |
|
|
{ label: t("Team"), name: "team" }, |
|
|
{ label: t("Team"), name: "team" }, |
|
|
{ label: t("Grade"), name: "grade" }, |
|
|
{ label: t("Grade"), name: "grade" }, |
|
|
{ label: t("Staff ID"), name: "id" }, |
|
|
|
|
|
|
|
|
{ label: t("Staff ID"), name: "staffId" }, |
|
|
{ label: t("Staff Name"), name: "name" }, |
|
|
{ label: t("Staff Name"), name: "name" }, |
|
|
{ label: t("Title"), name: "title" }, |
|
|
|
|
|
|
|
|
{ label: t("Title"), name: "currentPosition" }, |
|
|
], |
|
|
], |
|
|
[addStaff, t], |
|
|
[addStaff, t], |
|
|
); |
|
|
); |
|
@@ -178,7 +128,7 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
{ label: t("Grade"), name: "grade" }, |
|
|
{ label: t("Grade"), name: "grade" }, |
|
|
{ label: t("Staff ID"), name: "id" }, |
|
|
{ label: t("Staff ID"), name: "id" }, |
|
|
{ label: t("Staff Name"), name: "name" }, |
|
|
{ label: t("Staff Name"), name: "name" }, |
|
|
{ label: t("Title"), name: "title" }, |
|
|
|
|
|
|
|
|
{ label: t("Title"), name: "currentPosition" }, |
|
|
], |
|
|
], |
|
|
[removeStaff, t], |
|
|
[removeStaff, t], |
|
|
); |
|
|
); |
|
@@ -202,12 +152,12 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
(acc, filter) => { |
|
|
(acc, filter) => { |
|
|
return { |
|
|
return { |
|
|
...acc, |
|
|
...acc, |
|
|
[filter]: uniq(allStaff.map((staff) => staff[filter])), |
|
|
|
|
|
|
|
|
[filter]: uniq(allStaffs.map((staff) => staff[filter])), |
|
|
}; |
|
|
}; |
|
|
}, |
|
|
}, |
|
|
{}, |
|
|
{}, |
|
|
); |
|
|
); |
|
|
}, [columnFilters, allStaff]); |
|
|
|
|
|
|
|
|
}, [columnFilters, allStaffs]); |
|
|
const defaultFilterValues = React.useMemo(() => { |
|
|
const defaultFilterValues = React.useMemo(() => { |
|
|
return columnFilters.reduce<{ [filter in keyof StaffResult]?: string }>( |
|
|
return columnFilters.reduce<{ [filter in keyof StaffResult]?: string }>( |
|
|
(acc, filter) => { |
|
|
(acc, filter) => { |
|
@@ -224,14 +174,14 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
[], |
|
|
[], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
React.useEffect(() => { |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
setFilteredStaff( |
|
|
setFilteredStaff( |
|
|
allStaff.filter((staff) => { |
|
|
|
|
|
|
|
|
allStaffs.filter((staff) => { |
|
|
const q = query.toLowerCase(); |
|
|
const q = query.toLowerCase(); |
|
|
return ( |
|
|
return ( |
|
|
(staff.name.toLowerCase().includes(q) || |
|
|
(staff.name.toLowerCase().includes(q) || |
|
|
staff.id.toString().includes(q) || |
|
|
staff.id.toString().includes(q) || |
|
|
staff.title.toLowerCase().includes(q)) && |
|
|
|
|
|
|
|
|
staff.currentPosition.toLowerCase().includes(q)) && |
|
|
Object.entries(filters).every(([filterKey, filterValue]) => { |
|
|
Object.entries(filters).every(([filterKey, filterValue]) => { |
|
|
const staffColumnValue = staff[filterKey as keyof StaffResult]; |
|
|
const staffColumnValue = staff[filterKey as keyof StaffResult]; |
|
|
return staffColumnValue === filterValue || filterValue === "All"; |
|
|
return staffColumnValue === filterValue || filterValue === "All"; |
|
@@ -239,7 +189,7 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
); |
|
|
); |
|
|
}), |
|
|
}), |
|
|
); |
|
|
); |
|
|
}, [allStaff, filters, query]); |
|
|
|
|
|
|
|
|
}, [allStaffs, filters, query]); |
|
|
|
|
|
|
|
|
// Tab related |
|
|
// Tab related |
|
|
const [tabIndex, setTabIndex] = React.useState(0); |
|
|
const [tabIndex, setTabIndex] = React.useState(0); |
|
@@ -319,21 +269,29 @@ const StaffAllocation: React.FC<Props> = ({ |
|
|
<Tabs value={tabIndex} onChange={handleTabChange}> |
|
|
<Tabs value={tabIndex} onChange={handleTabChange}> |
|
|
<Tab label={t("Staff Pool")} /> |
|
|
<Tab label={t("Staff Pool")} /> |
|
|
<Tab |
|
|
<Tab |
|
|
label={`${t("Allocated Staff")} (${selectedStaff.length})`} |
|
|
|
|
|
|
|
|
label={`${t("Allocated Staff")} (${selectedStaffIds.length})`} |
|
|
/> |
|
|
/> |
|
|
</Tabs> |
|
|
</Tabs> |
|
|
<Box sx={{ marginInline: -3 }}> |
|
|
<Box sx={{ marginInline: -3 }}> |
|
|
{tabIndex === 0 && ( |
|
|
{tabIndex === 0 && ( |
|
|
<SearchResults |
|
|
<SearchResults |
|
|
noWrapper |
|
|
noWrapper |
|
|
items={differenceBy(filteredStaff, selectedStaff, "id")} |
|
|
|
|
|
|
|
|
items={differenceWith( |
|
|
|
|
|
filteredStaff, |
|
|
|
|
|
selectedStaffIds, |
|
|
|
|
|
(staff, staffId) => staff.id === staffId, |
|
|
|
|
|
)} |
|
|
columns={staffPoolColumns} |
|
|
columns={staffPoolColumns} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|
{tabIndex === 1 && ( |
|
|
{tabIndex === 1 && ( |
|
|
<SearchResults |
|
|
<SearchResults |
|
|
noWrapper |
|
|
noWrapper |
|
|
items={selectedStaff} |
|
|
|
|
|
|
|
|
items={intersectionWith( |
|
|
|
|
|
allStaffs, |
|
|
|
|
|
selectedStaffIds, |
|
|
|
|
|
(staff, staffId) => staff.id === staffId, |
|
|
|
|
|
)} |
|
|
columns={allocatedStaffColumns} |
|
|
columns={allocatedStaffColumns} |
|
|
/> |
|
|
/> |
|
|
)} |
|
|
)} |
|
|