From ec1e1b4e6940605e167c7ccdb9a50cb17de92d3b Mon Sep 17 00:00:00 2001 From: Wayne Date: Sun, 14 Apr 2024 23:08:27 +0900 Subject: [PATCH] Save staff allocation in project creation --- src/app/(main)/projects/create/page.tsx | 3 +- src/app/api/projects/actions.ts | 11 +- src/app/api/staff/index.ts | 11 +- .../CreateProject/CreateProject.tsx | 4 +- .../CreateProject/CreateProjectWrapper.tsx | 5 +- .../CreateProject/ProjectClientDetails.tsx | 13 ++ .../CreateProject/StaffAllocation.tsx | 156 +++++++----------- 7 files changed, 84 insertions(+), 119 deletions(-) diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx index f1672eb..102e272 100644 --- a/src/app/(main)/projects/create/page.tsx +++ b/src/app/(main)/projects/create/page.tsx @@ -7,7 +7,7 @@ import { fetchProjectServiceTypes, fetchProjectWorkNatures, } from "@/app/api/projects"; -import { preloadStaff } from "@/app/api/staff"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks"; import CreateProject from "@/components/CreateProject"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -31,6 +31,7 @@ const Projects: React.FC = async () => { fetchProjectServiceTypes(); fetchProjectBuildingTypes(); fetchProjectWorkNatures(); + preloadTeamLeads(); preloadStaff(); return ( diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index b745707..232f863 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -26,24 +26,15 @@ export interface CreateProjectInputs { clientContactId: number; clientSubsidiaryId?: number; - // Tasks - tasks: { - [taskId: Task["id"]]: { - manhourAllocation: ManhourAllocation; - }; - }; - + // Allocation totalManhour: number; manhourPercentageByGrade: ManhourAllocation; - taskGroups: { [taskGroup: TaskGroup["id"]]: { taskIds: Task["id"][]; percentAllocation: number; }; }; - - // Staff allocatedStaffIds: number[]; // Milestones diff --git a/src/app/api/staff/index.ts b/src/app/api/staff/index.ts index e4ed9cd..afa082f 100644 --- a/src/app/api/staff/index.ts +++ b/src/app/api/staff/index.ts @@ -15,7 +15,7 @@ export interface StaffResult { grade: string; joinPosition: string; currentPosition: string; - data: data + data: data; } export interface searchInput { staffId: string; @@ -40,10 +40,7 @@ export const preloadStaff = () => { }; export const fetchStaff = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/staffs`, { - next: { tags: ["staffs"] }, - }); + return serverFetchJson(`${BASE_API_URL}/staffs`, { + next: { tags: ["staffs"] }, + }); }); - - - diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 22299f2..61409b0 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -49,6 +49,7 @@ export interface Props { locationTypes: LocationType[]; buildingTypes: BuildingType[]; workNatures: WorkNature[]; + allStaffs: StaffResult[]; // Mocked grades: Grade[]; @@ -79,6 +80,7 @@ const CreateProject: React.FC = ({ serviceTypes, buildingTypes, workNatures, + allStaffs, }) => { const [serverError, setServerError] = useState(""); const [tabIndex, setTabIndex] = useState(0); @@ -122,7 +124,6 @@ const CreateProject: React.FC = ({ const formProps = useForm({ defaultValues: { - tasks: {}, taskGroups: {}, allocatedStaffIds: [], milestones: {}, @@ -182,6 +183,7 @@ const CreateProject: React.FC = ({ isActive={tabIndex === 2} allTasks={allTasks} grades={grades} + allStaffs={allStaffs} /> } {} diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx index 0e9ed17..67acc03 100644 --- a/src/components/CreateProject/CreateProjectWrapper.tsx +++ b/src/components/CreateProject/CreateProjectWrapper.tsx @@ -9,7 +9,7 @@ import { fetchProjectServiceTypes, fetchProjectWorkNatures, } from "@/app/api/projects"; -import { fetchTeamLeads } from "@/app/api/staff"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; import { fetchAllCustomers } from "@/app/api/customer"; const CreateProjectWrapper: React.FC = async () => { @@ -25,6 +25,7 @@ const CreateProjectWrapper: React.FC = async () => { serviceTypes, buildingTypes, workNatures, + allStaffs, ] = await Promise.all([ fetchAllTasks(), fetchTaskTemplates(), @@ -37,6 +38,7 @@ const CreateProjectWrapper: React.FC = async () => { fetchProjectServiceTypes(), fetchProjectBuildingTypes(), fetchProjectWorkNatures(), + fetchStaff(), ]); return ( @@ -52,6 +54,7 @@ const CreateProjectWrapper: React.FC = async () => { serviceTypes={serviceTypes} buildingTypes={buildingTypes} workNatures={workNatures} + allStaffs={allStaffs} // Mocks grades={[ { name: "Grade 1", id: 1, code: "1" }, diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx index 32db790..8bdbf4d 100644 --- a/src/components/CreateProject/ProjectClientDetails.tsx +++ b/src/components/CreateProject/ProjectClientDetails.tsx @@ -32,6 +32,7 @@ import Link from "next/link"; import React, { useEffect, useMemo, useState } from "react"; import { fetchCustomer } from "@/app/api/customer/actions"; import { Checkbox, ListItemText } from "@mui/material"; +import uniq from "lodash/uniq"; interface Props { isActive: boolean; @@ -64,6 +65,8 @@ const ProjectClientDetails: React.FC = ({ formState: { errors }, watch, control, + setValue, + getValues, } = useFormContext(); const selectedCustomerId = watch("clientId"); @@ -95,6 +98,16 @@ const ProjectClientDetails: React.FC = ({ } }, [selectedCustomerId]); + // Automatically add the team lead to the allocated staff list + const selectedTeamLeadId = watch("projectLeadId"); + useEffect(() => { + if (selectedTeamLeadId !== undefined) { + const currentStaffIds = getValues("allocatedStaffIds"); + const newList = uniq([...currentStaffIds, selectedTeamLeadId]); + setValue("allocatedStaffIds", newList); + } + }, [getValues, selectedTeamLeadId, setValue]); + const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>( (acc, building) => ({ ...acc, [building.id]: building.name }), {}, diff --git a/src/components/CreateProject/StaffAllocation.tsx b/src/components/CreateProject/StaffAllocation.tsx index bfeba33..b724b0e 100644 --- a/src/components/CreateProject/StaffAllocation.tsx +++ b/src/components/CreateProject/StaffAllocation.tsx @@ -1,7 +1,7 @@ "use client"; import { useTranslation } from "react-i18next"; -import React, { useEffect } from "react"; +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"; @@ -26,7 +26,8 @@ import { Tabs, SelectChangeEvent, } from "@mui/material"; -import differenceBy from "lodash/differenceBy"; +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"; @@ -34,65 +35,7 @@ import { CreateProjectInputs } from "@/app/api/projects/actions"; import ResourceAllocation from "./ResourceAllocation"; import { Task } from "@/app/api/tasks"; 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) => { return ( @@ -103,7 +46,7 @@ const staffComparator = (a: StaffResult, b: StaffResult) => { }; export interface Props { - allStaff?: StaffResult[]; + allStaffs: StaffResult[]; isActive: boolean; defaultManhourBreakdownByGrade?: { [gradeId: number]: number }; allTasks: Task[]; @@ -111,43 +54,50 @@ export interface Props { } const StaffAllocation: React.FC = ({ - allStaff = mockStaffs, + allStaffs: dataStaffs, allTasks, isActive, defaultManhourBreakdownByGrade, grades, }) => { const { t } = useTranslation(); - const { setValue, getValues } = useFormContext(); + const { setValue, getValues, watch } = useFormContext(); + + // TODO: remove this when grade and positions are done + const allStaffs = useMemo(() => { + return dataStaffs.map((staff, index) => ({ + ...staff, + grade: grades[index % grades.length].name, + currentPosition: `Mock Postion ${index}`, + })); + }, [dataStaffs, grades]); 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 - 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(() => { - setSelectedStaff([]); - }, []); - // Sync with form - useEffect(() => { - setValue( - "allocatedStaffIds", - selectedStaff.map((staff) => staff.id), - ); - }, [selectedStaff, setValue]); + setValue("allocatedStaffIds", []); + }, [setValue]); const staffPoolColumns = React.useMemo[]>( () => [ @@ -159,9 +109,9 @@ const StaffAllocation: React.FC = ({ }, { label: t("Team"), name: "team" }, { 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("Title"), name: "title" }, + { label: t("Title"), name: "currentPosition" }, ], [addStaff, t], ); @@ -178,7 +128,7 @@ const StaffAllocation: React.FC = ({ { label: t("Grade"), name: "grade" }, { label: t("Staff ID"), name: "id" }, { label: t("Staff Name"), name: "name" }, - { label: t("Title"), name: "title" }, + { label: t("Title"), name: "currentPosition" }, ], [removeStaff, t], ); @@ -202,12 +152,12 @@ const StaffAllocation: React.FC = ({ (acc, filter) => { return { ...acc, - [filter]: uniq(allStaff.map((staff) => staff[filter])), + [filter]: uniq(allStaffs.map((staff) => staff[filter])), }; }, {}, ); - }, [columnFilters, allStaff]); + }, [columnFilters, allStaffs]); const defaultFilterValues = React.useMemo(() => { return columnFilters.reduce<{ [filter in keyof StaffResult]?: string }>( (acc, filter) => { @@ -224,14 +174,14 @@ const StaffAllocation: React.FC = ({ [], ); - React.useEffect(() => { + useEffect(() => { setFilteredStaff( - allStaff.filter((staff) => { + allStaffs.filter((staff) => { const q = query.toLowerCase(); return ( (staff.name.toLowerCase().includes(q) || staff.id.toString().includes(q) || - staff.title.toLowerCase().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"; @@ -239,7 +189,7 @@ const StaffAllocation: React.FC = ({ ); }), ); - }, [allStaff, filters, query]); + }, [allStaffs, filters, query]); // Tab related const [tabIndex, setTabIndex] = React.useState(0); @@ -319,21 +269,29 @@ const StaffAllocation: React.FC = ({ {tabIndex === 0 && ( staff.id === staffId, + )} columns={staffPoolColumns} /> )} {tabIndex === 1 && ( staff.id === staffId, + )} columns={allocatedStaffColumns} /> )}