Browse Source

Add form provider for create project

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 year ago
parent
commit
070c1d3377
8 changed files with 339 additions and 103 deletions
  1. +50
    -0
      src/app/api/projects/actions.ts
  2. +45
    -21
      src/components/CreateProject/CreateProject.tsx
  3. +1
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  4. +43
    -8
      src/components/CreateProject/ProjectClientDetails.tsx
  5. +118
    -32
      src/components/CreateProject/ResourceMilestone.tsx
  6. +35
    -22
      src/components/CreateProject/StaffAllocation.tsx
  7. +37
    -11
      src/components/CreateProject/TaskSetup.tsx
  8. +10
    -8
      src/components/TransferList/TransferList.tsx

+ 50
- 0
src/app/api/projects/actions.ts View File

@@ -0,0 +1,50 @@
"use server";

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { Task, TaskGroup } from "../tasks";

export interface CreateProjectInputs {
// Project details
projectCode: string;
projectSubcode: string;
projectName: string;
projectCategory: string;
projectDescription: string;

// Client details
clientCode: string;
clientName: string;
clientPhone: string;
clientEmail: string;
clientSubsidiary: string;

// Tasks
tasks: {
[taskId: Task["id"]]: {
manhourAllocation: {
[grade: string]: number;
};
};
};

// Staff
allocatedStaffIds: number[];

// Milestones
milestones: {
[taskGroupId: TaskGroup["id"]]: {
startDate: string;
endDate: string;
payments: [];
};
};
}

export const saveProject = async (data: CreateProjectInputs) => {
return serverFetchJson(`${BASE_API_URL}/projects/new`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 45
- 21
src/components/CreateProject/CreateProject.tsx View File

@@ -14,12 +14,14 @@ import TaskSetup from "./TaskSetup";
import StaffAllocation from "./StaffAllocation"; import StaffAllocation from "./StaffAllocation";
import ResourceMilestone from "./ResourceMilestone"; import ResourceMilestone from "./ResourceMilestone";
import { Task } from "@/app/api/tasks"; import { Task } from "@/app/api/tasks";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";


export interface Props { export interface Props {
mockTasks: Task[];
allTasks: Task[];
} }


const CreateProject: React.FC<Props> = ({ mockTasks }) => {
const CreateProject: React.FC<Props> = ({ allTasks }) => {
const [tabIndex, setTabIndex] = useState(0); const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -35,27 +37,49 @@ const CreateProject: React.FC<Props> = ({ mockTasks }) => {
[], [],
); );


const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>((data) => {
console.log(data);
}, []);

const formProps = useForm<CreateProjectInputs>({
defaultValues: {
tasks: {},
allocatedStaffIds: [],
milestones: {},
},
});

return ( return (
<>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Project and Client Details")} />
<Tab label={t("Project Task Setup")} />
<Tab label={t("Staff Allocation")} />
<Tab label={t("Resource and Milestone")} />
</Tabs>
{tabIndex === 0 && <ProjectClientDetails />}
{tabIndex === 1 && <TaskSetup />}
{tabIndex === 2 && <StaffAllocation initiallySelectedStaff={[]} />}
{tabIndex === 3 && <ResourceMilestone tasks={mockTasks} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button variant="outlined" startIcon={<Close />} onClick={handleCancel}>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />}>
{t("Confirm")}
</Button>
<FormProvider {...formProps}>
<Stack
spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Project and Client Details")} />
<Tab label={t("Project Task Setup")} />
<Tab label={t("Staff Allocation")} />
<Tab label={t("Resource and Milestone")} />
</Tabs>
{tabIndex === 0 && <ProjectClientDetails />}
{tabIndex === 1 && <TaskSetup allTasks={allTasks} />}
{tabIndex === 2 && <StaffAllocation />}
{tabIndex === 3 && <ResourceMilestone allTasks={allTasks} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Confirm")}
</Button>
</Stack>
</Stack> </Stack>
</>
</FormProvider>
); );
}; };




+ 1
- 1
src/components/CreateProject/CreateProjectWrapper.tsx View File

@@ -4,7 +4,7 @@ import CreateProject from "./CreateProject";
const CreateProjectWrapper: React.FC = async () => { const CreateProjectWrapper: React.FC = async () => {
const tasks = await fetchAllTasks(); const tasks = await fetchAllTasks();


return <CreateProject mockTasks={tasks} />;
return <CreateProject allTasks={tasks} />;
}; };


export default CreateProjectWrapper; export default CreateProjectWrapper;

+ 43
- 8
src/components/CreateProject/ProjectClientDetails.tsx View File

@@ -15,9 +15,12 @@ import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt"; import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";


const ProjectClientDetails: React.FC = () => { const ProjectClientDetails: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { register } = useFormContext<CreateProjectInputs>();


return ( return (
<Card> <Card>
@@ -28,13 +31,25 @@ const ProjectClientDetails: React.FC = () => {
</Typography> </Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Project Code")} fullWidth />
<TextField
label={t("Project Code")}
fullWidth
{...register("projectCode")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Project Subcode")} fullWidth />
<TextField
label={t("Project Subcode")}
fullWidth
{...register("projectSubcode")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Project Name")} fullWidth />
<TextField
label={t("Project Name")}
fullWidth
{...register("projectName")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth> <FormControl fullWidth>
@@ -60,7 +75,11 @@ const ProjectClientDetails: React.FC = () => {
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Project Description")} fullWidth />
<TextField
label={t("Project Description")}
fullWidth
{...register("projectDescription")}
/>
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>
@@ -71,16 +90,32 @@ const ProjectClientDetails: React.FC = () => {
</Typography> </Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Client Code and Name")} fullWidth />
<TextField
label={t("Client Code and Name")}
fullWidth
{...register("clientCode")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Client Lead Name")} fullWidth />
<TextField
label={t("Client Lead Name")}
fullWidth
{...register("clientName")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Client Lead Phone Number")} fullWidth />
<TextField
label={t("Client Lead Phone Number")}
fullWidth
{...register("clientPhone")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<TextField label={t("Client Lead Email")} fullWidth />
<TextField
label={t("Client Lead Email")}
fullWidth
{...register("clientEmail")}
/>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<FormControl fullWidth> <FormControl fullWidth>


+ 118
- 32
src/components/CreateProject/ResourceMilestone.tsx View File

@@ -5,18 +5,24 @@ import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import React, { useCallback, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt"; import RestartAlt from "@mui/icons-material/RestartAlt";
import { import {
Alert, Alert,
Box,
FormControl, FormControl,
Grid, Grid,
InputLabel, InputLabel,
List,
ListItemButton,
ListItemText,
MenuItem, MenuItem,
Paper,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
Stack, Stack,
TextField,
} from "@mui/material"; } from "@mui/material";
import { Task, TaskGroup } from "@/app/api/tasks"; import { Task, TaskGroup } from "@/app/api/tasks";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
@@ -24,13 +30,26 @@ import { moneyFormatter } from "@/app/utils/formatUtil";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";


interface Props { interface Props {
allTasks: Task[];
}

interface ResourceSectionProps {
tasks: Task[]; tasks: Task[];
defaultManhourBreakdownByGrade: { [grade: string]: number };
onSetManhours: (hours: number, taskId: Task["id"]) => void;
onAllocateManhours: () => void;
} }


const ResourceMilestone: React.FC<Props> = ({ tasks }) => {
const ResourceMilestone: React.FC<Props> = ({ allTasks }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getValues } = useFormContext<CreateProjectInputs>();
const tasks = useMemo(() => {
return allTasks.filter((task) => getValues("tasks")[task.id]);
}, [allTasks, getValues]);


const taskGroups = useMemo(() => { const taskGroups = useMemo(() => {
return uniqBy( return uniqBy(
@@ -41,18 +60,23 @@ const ResourceMilestone: React.FC<Props> = ({ tasks }) => {
const [currentTaskGroupId, setCurrentTaskGroupId] = useState( const [currentTaskGroupId, setCurrentTaskGroupId] = useState(
taskGroups[0].id, taskGroups[0].id,
); );
const [currentTasks, setCurrentTasks] = useState<typeof tasks>(
tasks.filter((t) => t.taskGroup.id === currentTaskGroupId),
);
const onSelectTaskGroup = useCallback( const onSelectTaskGroup = useCallback(
(event: SelectChangeEvent<TaskGroup["id"]>) => { (event: SelectChangeEvent<TaskGroup["id"]>) => {
const id = event.target.value; const id = event.target.value;
setCurrentTaskGroupId(typeof id === "string" ? parseInt(id) : id);
const newTaksGroupId = typeof id === "string" ? parseInt(id) : id;
setCurrentTaskGroupId(newTaksGroupId);
setCurrentTasks(tasks.filter((t) => t.taskGroup.id === newTaksGroupId));
}, },
[],
[tasks],
); );


return ( return (
<> <>
<Card> <Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<FormControl> <FormControl>
<InputLabel>{t("Task Stage")}</InputLabel> <InputLabel>{t("Task Stage")}</InputLabel>
<Select <Select
@@ -67,32 +91,13 @@ const ResourceMilestone: React.FC<Props> = ({ tasks }) => {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Resource")}
</Typography>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Milestone")}
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs>
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
defaultValue={dayjs()}
/>
</FormControl>
</Grid>
<Grid item xs>
<FormControl fullWidth>
<DatePicker
label={t("Stage End Date")}
defaultValue={dayjs()}
/>
</FormControl>
</Grid>
</Grid>
</LocalizationProvider>
<ResourceSection
tasks={currentTasks}
defaultManhourBreakdownByGrade={{}}
onSetManhours={() => {}}
onAllocateManhours={() => {}}
/>
<MilestoneSection />
<CardActions sx={{ justifyContent: "flex-end" }}> <CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}> <Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")} {t("Reset")}
@@ -112,6 +117,85 @@ const ResourceMilestone: React.FC<Props> = ({ tasks }) => {
); );
}; };


const ResourceSection: React.FC<ResourceSectionProps> = ({
tasks,
onAllocateManhours,
onSetManhours,
defaultManhourBreakdownByGrade,
}) => {
const { t } = useTranslation();
const [selectedTaskId, setSelectedTaskId] = useState(tasks[0].id);
const makeOnTaskSelect = useCallback(
(taskId: Task["id"]): React.MouseEventHandler =>
() => {
return setSelectedTaskId(taskId);
},
[],
);

useEffect(() => {
setSelectedTaskId(tasks[0].id);
}, [tasks]);

return (
<Box marginBlock={4}>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Resource")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<Paper elevation={2}>
<List dense sx={{ maxHeight: 300, overflow: "auto" }}>
{tasks.map((task, index) => {
return (
<ListItemButton
selected={selectedTaskId === task.id}
key={`${task.id}-${index}`}
onClick={makeOnTaskSelect(task.id)}
>
<ListItemText primary={task.name} />
</ListItemButton>
);
})}
</List>
</Paper>
</Grid>
<Grid item xs={6}>
<TextField label={t("Mahours Allocated to Task")} fullWidth />
</Grid>
</Grid>
</Box>
);
};

const MilestoneSection: React.FC = () => {
const { t } = useTranslation();
return (
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Milestone")}
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs>
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
defaultValue={dayjs()}
/>
</FormControl>
</Grid>
<Grid item xs>
<FormControl fullWidth>
<DatePicker label={t("Stage End Date")} defaultValue={dayjs()} />
</FormControl>
</Grid>
</Grid>
</LocalizationProvider>
</Box>
);
};

const NoTaskState: React.FC = () => { const NoTaskState: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -126,7 +210,9 @@ const NoTaskState: React.FC = () => {
}; };


const ResourceMilestoneWrapper: React.FC<Props> = (props) => { const ResourceMilestoneWrapper: React.FC<Props> = (props) => {
if (props.tasks.length === 0) {
const { getValues } = useFormContext<CreateProjectInputs>();

if (Object.keys(getValues("tasks")).length === 0) {
return <NoTaskState />; return <NoTaskState />;
} }




+ 35
- 22
src/components/CreateProject/StaffAllocation.tsx View File

@@ -1,7 +1,7 @@
"use client"; "use client";


import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import React from "react";
import React, { useEffect } 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";
@@ -29,9 +29,11 @@ import {
import differenceBy from "lodash/differenceBy"; import differenceBy from "lodash/differenceBy";
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 { CreateProjectInputs } from "@/app/api/projects/actions";


interface StaffResult { interface StaffResult {
id: string;
id: number;
name: string; name: string;
team: string; team: string;
grade: string; grade: string;
@@ -42,67 +44,69 @@ const mockStaffs: StaffResult[] = [
{ {
name: "Albert", name: "Albert",
grade: "1", grade: "1",
id: "1",
id: 1,
team: "ABC", team: "ABC",
title: "Associate Quantity Surveyor", title: "Associate Quantity Surveyor",
}, },
{ {
name: "Bernard", name: "Bernard",
grade: "2", grade: "2",
id: "2",
id: 2,
team: "ABC", team: "ABC",
title: "Quantity Surveyor", title: "Quantity Surveyor",
}, },
{ {
name: "Carl", name: "Carl",
grade: "3", grade: "3",
id: "3",
id: 3,
team: "XYZ", team: "XYZ",
title: "Senior Quantity Surveyor", 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: "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", name: "Heather",
grade: "3", grade: "3",
id: "8",
id: 8,
team: "XYZ", team: "XYZ",
title: "Field Engineer", title: "Field Engineer",
}, },
{ name: "Ivan", grade: "4", id: "9", team: "ABC", title: "Senior Manager" },
{ name: "Ivan", grade: "4", id: 9, team: "ABC", title: "Senior Manager" },
{ {
name: "Jackson", name: "Jackson",
grade: "5", grade: "5",
id: "10",
id: 10,
team: "XYZ", team: "XYZ",
title: "Senior Director", title: "Senior Director",
}, },
{ {
name: "Kurt", name: "Kurt",
grade: "1", grade: "1",
id: "11",
id: 11,
team: "ABC", team: "ABC",
title: "Construction Assistant", title: "Construction Assistant",
}, },
{ name: "Lawrence", grade: "2", id: "12", team: "ABC", title: "Operator" },
{ name: "Lawrence", grade: "2", id: 12, team: "ABC", title: "Operator" },
]; ];


interface Props { interface Props {
allStaff?: StaffResult[]; allStaff?: StaffResult[];
initiallySelectedStaff: StaffResult[];
} }


const StaffAllocation: React.FC<Props> = ({
allStaff = mockStaffs,
initiallySelectedStaff,
}) => {
const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { setValue, getValues } = useFormContext<CreateProjectInputs>();

const [filteredStaff, setFilteredStaff] = React.useState(allStaff); const [filteredStaff, setFilteredStaff] = React.useState(allStaff);
const [selectedStaff, setSelectedStaff] = React.useState< const [selectedStaff, setSelectedStaff] = React.useState<
typeof filteredStaff typeof filteredStaff
>(initiallySelectedStaff);
>(
allStaff.filter((staff) =>
getValues("allocatedStaffIds").includes(staff.id),
),
);


// Adding / Removing staff // Adding / Removing staff
const addStaff = React.useCallback((staff: StaffResult) => { const addStaff = React.useCallback((staff: StaffResult) => {
@@ -114,6 +118,13 @@ const StaffAllocation: React.FC<Props> = ({
const clearStaff = React.useCallback(() => { const clearStaff = React.useCallback(() => {
setSelectedStaff([]); setSelectedStaff([]);
}, []); }, []);
// Sync with form
useEffect(() => {
setValue(
"allocatedStaffIds",
selectedStaff.map((staff) => staff.id),
);
}, [selectedStaff, setValue]);


const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( const staffPoolColumns = React.useMemo<Column<StaffResult>[]>(
() => [ () => [
@@ -196,7 +207,7 @@ const StaffAllocation: React.FC<Props> = ({
const q = query.toLowerCase(); const q = query.toLowerCase();
return ( return (
(staff.name.toLowerCase().includes(q) || (staff.name.toLowerCase().includes(q) ||
staff.id.toLowerCase().includes(q) ||
staff.id.toString().includes(q) ||
staff.title.toLowerCase().includes(q)) && staff.title.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];
@@ -279,7 +290,9 @@ const StaffAllocation: React.FC<Props> = ({
</Grid> </Grid>
<Tabs value={tabIndex} onChange={handleTabChange}> <Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label={t("Staff Pool")} /> <Tab label={t("Staff Pool")} />
<Tab label={`${t("Allocated Staff")} (${selectedStaff.length})`} />
<Tab
label={`${t("Allocated Staff")} (${selectedStaff.length})`}
/>
</Tabs> </Tabs>
<Box sx={{ marginInline: -3 }}> <Box sx={{ marginInline: -3 }}>
{tabIndex === 0 && ( {tabIndex === 0 && (


+ 37
- 11
src/components/CreateProject/TaskSetup.tsx View File

@@ -7,16 +7,35 @@ import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import TransferList from "../TransferList"; import TransferList from "../TransferList";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import React from "react";
import React, { useMemo } from "react";
import CardActions from "@mui/material/CardActions"; import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt"; import RestartAlt from "@mui/icons-material/RestartAlt";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select"; import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import InputLabel from "@mui/material/InputLabel"; import InputLabel from "@mui/material/InputLabel";
import { Task } from "@/app/api/tasks";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";


const TaskSetup = () => {
interface Props {
allTasks: Task[];
}

const TaskSetup: React.FC<Props> = ({ allTasks: tasks }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { getValues, setValue } = useFormContext<CreateProjectInputs>();
const currentTasks = getValues("tasks");

const items = useMemo(
() => tasks.map((t) => ({ id: t.id, label: t.name, group: t.taskGroup })),
[tasks],
);
const selectedItems = useMemo(() => {
return tasks
.filter((t) => currentTasks[t.id])
.map((t) => ({ id: t.id, label: t.name, group: t.taskGroup }));
}, [currentTasks, tasks]);


return ( return (
<Card> <Card>
@@ -45,15 +64,22 @@ const TaskSetup = () => {
</Grid> </Grid>
</Grid> </Grid>
<TransferList <TransferList
allItems={[
{ id: 1, label: "Task 1" },
{ id: 2, label: "Task 2" },
{ id: 3, label: "Task 3" },
{ id: 4, label: "Task 4" },
{ id: 5, label: "Task 5" },
]}
initiallySelectedItems={[]}
onChange={() => {}}
allItems={items}
initiallySelectedItems={selectedItems}
onChange={(selectedTasks) => {
const newTasks = selectedTasks.reduce<CreateProjectInputs["tasks"]>(
(acc, item) => {
// Reuse the task from currentTasks if present
return {
...acc,
[item.id]: currentTasks[item.id] ?? { manhourAllocation: {} },
};
},
{},
);

setValue("tasks", newTasks);
}}
allItemsLabel={t("Task Pool")} allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Project Task List")} selectedItemsLabel={t("Project Task List")}
/> />


+ 10
- 8
src/components/TransferList/TransferList.tsx View File

@@ -11,7 +11,7 @@ import Divider from "@mui/material/Divider";
import ChevronLeft from "@mui/icons-material/ChevronLeft"; import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight"; import ChevronRight from "@mui/icons-material/ChevronRight";
import intersection from "lodash/intersection"; import intersection from "lodash/intersection";
import difference from "lodash/difference";
import differenceBy from "lodash/differenceBy";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@@ -163,7 +163,7 @@ const TransferList: React.FC<TransferListProps> = ({


const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]); const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]);
const [leftList, setLeftList] = React.useState<LabelWithId[]>( const [leftList, setLeftList] = React.useState<LabelWithId[]>(
difference(allItems, initiallySelectedItems),
differenceBy(allItems, initiallySelectedItems, "id"),
); );
const [rightList, setRightList] = React.useState<LabelWithId[]>( const [rightList, setRightList] = React.useState<LabelWithId[]>(
initiallySelectedItems, initiallySelectedItems,
@@ -176,7 +176,7 @@ const TransferList: React.FC<TransferListProps> = ({
(value: LabelWithId) => () => { (value: LabelWithId) => () => {
const isChecked = checkedList.includes(value); const isChecked = checkedList.includes(value);
const newCheckedList = isChecked const newCheckedList = isChecked
? difference(checkedList, [value])
? differenceBy(checkedList, [value], "id")
: [...checkedList, value]; : [...checkedList, value];


setCheckedList(newCheckedList); setCheckedList(newCheckedList);
@@ -187,7 +187,7 @@ const TransferList: React.FC<TransferListProps> = ({
const handleToggleAll = React.useCallback( const handleToggleAll = React.useCallback(
(items: LabelWithId[], checkedItems: LabelWithId[]) => () => { (items: LabelWithId[], checkedItems: LabelWithId[]) => () => {
if (checkedItems.length === items.length) { if (checkedItems.length === items.length) {
setCheckedList(difference(checkedList, checkedItems));
setCheckedList(differenceBy(checkedList, checkedItems, "id"));
} else { } else {
setCheckedList([...checkedList, ...items]); setCheckedList([...checkedList, ...items]);
} }
@@ -197,14 +197,16 @@ const TransferList: React.FC<TransferListProps> = ({


const handleCheckedRight = () => { const handleCheckedRight = () => {
setRightList([...rightList, ...leftListChecked].sort(compareFn)); setRightList([...rightList, ...leftListChecked].sort(compareFn));
setLeftList(difference(leftList, leftListChecked).sort(compareFn));
setCheckedList(difference(checkedList, leftListChecked));
setLeftList(differenceBy(leftList, leftListChecked, "id").sort(compareFn));
setCheckedList(differenceBy(checkedList, leftListChecked, "id"));
}; };


const handleCheckedLeft = () => { const handleCheckedLeft = () => {
setLeftList([...leftList, ...rightListChecked].sort(compareFn)); setLeftList([...leftList, ...rightListChecked].sort(compareFn));
setRightList(difference(rightList, rightListChecked).sort(compareFn));
setCheckedList(difference(checkedList, rightListChecked));
setRightList(
differenceBy(rightList, rightListChecked, "id").sort(compareFn),
);
setCheckedList(differenceBy(checkedList, rightListChecked, "id"));
}; };


React.useEffect(() => { React.useEffect(() => {


Loading…
Cancel
Save