Bläddra i källkod

Save staff allocation in project creation

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 år sedan
förälder
incheckning
ec1e1b4e69
7 ändrade filer med 84 tillägg och 119 borttagningar
  1. +2
    -1
      src/app/(main)/projects/create/page.tsx
  2. +1
    -10
      src/app/api/projects/actions.ts
  3. +4
    -7
      src/app/api/staff/index.ts
  4. +3
    -1
      src/components/CreateProject/CreateProject.tsx
  5. +4
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  6. +13
    -0
      src/components/CreateProject/ProjectClientDetails.tsx
  7. +57
    -99
      src/components/CreateProject/StaffAllocation.tsx

+ 2
- 1
src/app/(main)/projects/create/page.tsx Visa fil

@@ -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 (


+ 1
- 10
src/app/api/projects/actions.ts Visa fil

@@ -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


+ 4
- 7
src/app/api/staff/index.ts Visa fil

@@ -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<StaffResult[]>(`${BASE_API_URL}/staffs`, {
next: { tags: ["staffs"] },
});
return serverFetchJson<StaffResult[]>(`${BASE_API_URL}/staffs`, {
next: { tags: ["staffs"] },
});
});




+ 3
- 1
src/components/CreateProject/CreateProject.tsx Visa fil

@@ -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<Props> = ({
serviceTypes,
buildingTypes,
workNatures,
allStaffs,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
@@ -122,7 +124,6 @@ const CreateProject: React.FC<Props> = ({

const formProps = useForm<CreateProjectInputs>({
defaultValues: {
tasks: {},
taskGroups: {},
allocatedStaffIds: [],
milestones: {},
@@ -182,6 +183,7 @@ const CreateProject: React.FC<Props> = ({
isActive={tabIndex === 2}
allTasks={allTasks}
grades={grades}
allStaffs={allStaffs}
/>
}
{<Milestone allTasks={allTasks} isActive={tabIndex === 3} />}


+ 4
- 1
src/components/CreateProject/CreateProjectWrapper.tsx Visa fil

@@ -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" },


+ 13
- 0
src/components/CreateProject/ProjectClientDetails.tsx Visa fil

@@ -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<Props> = ({
formState: { errors },
watch,
control,
setValue,
getValues,
} = useFormContext<CreateProjectInputs>();

const selectedCustomerId = watch("clientId");
@@ -95,6 +98,16 @@ const ProjectClientDetails: React.FC<Props> = ({
}
}, [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 }),
{},


+ 57
- 99
src/components/CreateProject/StaffAllocation.tsx Visa fil

@@ -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<Props> = ({
allStaff = mockStaffs,
allStaffs: dataStaffs,
allTasks,
isActive,
defaultManhourBreakdownByGrade,
grades,
}) => {
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(
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<Column<StaffResult>[]>(
() => [
@@ -159,9 +109,9 @@ const StaffAllocation: React.FC<Props> = ({
},
{ 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<Props> = ({
{ 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<Props> = ({
(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<Props> = ({
[],
);

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<Props> = ({
);
}),
);
}, [allStaff, filters, query]);
}, [allStaffs, filters, query]);

// Tab related
const [tabIndex, setTabIndex] = React.useState(0);
@@ -319,21 +269,29 @@ const StaffAllocation: React.FC<Props> = ({
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
label={`${t("Allocated Staff")} (${selectedStaffIds.length})`}
/>
</Tabs>
<Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && (
<SearchResults
noWrapper
items={differenceBy(filteredStaff, selectedStaff, "id")}
items={differenceWith(
filteredStaff,
selectedStaffIds,
(staff, staffId) => staff.id === staffId,
)}
columns={staffPoolColumns}
/>
)}
{tabIndex === 1 && (
<SearchResults
noWrapper
items={selectedStaff}
items={intersectionWith(
allStaffs,
selectedStaffIds,
(staff, staffId) => staff.id === staffId,
)}
columns={allocatedStaffColumns}
/>
)}


Laddar…
Avbryt
Spara