@@ -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" }, | |||
}); | |||
}; |
@@ -14,12 +14,14 @@ import TaskSetup from "./TaskSetup"; | |||
import StaffAllocation from "./StaffAllocation"; | |||
import ResourceMilestone from "./ResourceMilestone"; | |||
import { Task } from "@/app/api/tasks"; | |||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
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 { t } = useTranslation(); | |||
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 ( | |||
<> | |||
<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> | |||
</> | |||
</FormProvider> | |||
); | |||
}; | |||
@@ -4,7 +4,7 @@ import CreateProject from "./CreateProject"; | |||
const CreateProjectWrapper: React.FC = async () => { | |||
const tasks = await fetchAllTasks(); | |||
return <CreateProject mockTasks={tasks} />; | |||
return <CreateProject allTasks={tasks} />; | |||
}; | |||
export default CreateProjectWrapper; |
@@ -15,9 +15,12 @@ import { useTranslation } from "react-i18next"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import Button from "@mui/material/Button"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
const ProjectClientDetails: React.FC = () => { | |||
const { t } = useTranslation(); | |||
const { register } = useFormContext<CreateProjectInputs>(); | |||
return ( | |||
<Card> | |||
@@ -28,13 +31,25 @@ const ProjectClientDetails: React.FC = () => { | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Code")} fullWidth /> | |||
<TextField | |||
label={t("Project Code")} | |||
fullWidth | |||
{...register("projectCode")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Subcode")} fullWidth /> | |||
<TextField | |||
label={t("Project Subcode")} | |||
fullWidth | |||
{...register("projectSubcode")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Name")} fullWidth /> | |||
<TextField | |||
label={t("Project Name")} | |||
fullWidth | |||
{...register("projectName")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
@@ -60,7 +75,11 @@ const ProjectClientDetails: React.FC = () => { | |||
</FormControl> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Project Description")} fullWidth /> | |||
<TextField | |||
label={t("Project Description")} | |||
fullWidth | |||
{...register("projectDescription")} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Box> | |||
@@ -71,16 +90,32 @@ const ProjectClientDetails: React.FC = () => { | |||
</Typography> | |||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Code and Name")} fullWidth /> | |||
<TextField | |||
label={t("Client Code and Name")} | |||
fullWidth | |||
{...register("clientCode")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Name")} fullWidth /> | |||
<TextField | |||
label={t("Client Lead Name")} | |||
fullWidth | |||
{...register("clientName")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Phone Number")} fullWidth /> | |||
<TextField | |||
label={t("Client Lead Phone Number")} | |||
fullWidth | |||
{...register("clientPhone")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField label={t("Client Lead Email")} fullWidth /> | |||
<TextField | |||
label={t("Client Lead Email")} | |||
fullWidth | |||
{...register("clientEmail")} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<FormControl fullWidth> | |||
@@ -5,18 +5,24 @@ import CardContent from "@mui/material/CardContent"; | |||
import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
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 RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import { | |||
Alert, | |||
Box, | |||
FormControl, | |||
Grid, | |||
InputLabel, | |||
List, | |||
ListItemButton, | |||
ListItemText, | |||
MenuItem, | |||
Paper, | |||
Select, | |||
SelectChangeEvent, | |||
Stack, | |||
TextField, | |||
} from "@mui/material"; | |||
import { Task, TaskGroup } from "@/app/api/tasks"; | |||
import uniqBy from "lodash/uniqBy"; | |||
@@ -24,13 +30,26 @@ import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
import dayjs from "dayjs"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
interface Props { | |||
allTasks: Task[]; | |||
} | |||
interface ResourceSectionProps { | |||
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 { getValues } = useFormContext<CreateProjectInputs>(); | |||
const tasks = useMemo(() => { | |||
return allTasks.filter((task) => getValues("tasks")[task.id]); | |||
}, [allTasks, getValues]); | |||
const taskGroups = useMemo(() => { | |||
return uniqBy( | |||
@@ -41,18 +60,23 @@ const ResourceMilestone: React.FC<Props> = ({ tasks }) => { | |||
const [currentTaskGroupId, setCurrentTaskGroupId] = useState( | |||
taskGroups[0].id, | |||
); | |||
const [currentTasks, setCurrentTasks] = useState<typeof tasks>( | |||
tasks.filter((t) => t.taskGroup.id === currentTaskGroupId), | |||
); | |||
const onSelectTaskGroup = useCallback( | |||
(event: SelectChangeEvent<TaskGroup["id"]>) => { | |||
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 ( | |||
<> | |||
<Card> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}> | |||
<FormControl> | |||
<InputLabel>{t("Task Stage")}</InputLabel> | |||
<Select | |||
@@ -67,32 +91,13 @@ const ResourceMilestone: React.FC<Props> = ({ tasks }) => { | |||
))} | |||
</Select> | |||
</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" }}> | |||
<Button variant="text" startIcon={<RestartAlt />}> | |||
{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 { t } = useTranslation(); | |||
return ( | |||
@@ -126,7 +210,9 @@ const NoTaskState: React.FC = () => { | |||
}; | |||
const ResourceMilestoneWrapper: React.FC<Props> = (props) => { | |||
if (props.tasks.length === 0) { | |||
const { getValues } = useFormContext<CreateProjectInputs>(); | |||
if (Object.keys(getValues("tasks")).length === 0) { | |||
return <NoTaskState />; | |||
} | |||
@@ -1,7 +1,7 @@ | |||
"use client"; | |||
import { useTranslation } from "react-i18next"; | |||
import React from "react"; | |||
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"; | |||
@@ -29,9 +29,11 @@ import { | |||
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: string; | |||
id: number; | |||
name: string; | |||
team: string; | |||
grade: string; | |||
@@ -42,67 +44,69 @@ const mockStaffs: StaffResult[] = [ | |||
{ | |||
name: "Albert", | |||
grade: "1", | |||
id: "1", | |||
id: 1, | |||
team: "ABC", | |||
title: "Associate Quantity Surveyor", | |||
}, | |||
{ | |||
name: "Bernard", | |||
grade: "2", | |||
id: "2", | |||
id: 2, | |||
team: "ABC", | |||
title: "Quantity Surveyor", | |||
}, | |||
{ | |||
name: "Carl", | |||
grade: "3", | |||
id: "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: "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", | |||
id: 8, | |||
team: "XYZ", | |||
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", | |||
grade: "5", | |||
id: "10", | |||
id: 10, | |||
team: "XYZ", | |||
title: "Senior Director", | |||
}, | |||
{ | |||
name: "Kurt", | |||
grade: "1", | |||
id: "11", | |||
id: 11, | |||
team: "ABC", | |||
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 { | |||
allStaff?: StaffResult[]; | |||
initiallySelectedStaff: StaffResult[]; | |||
} | |||
const StaffAllocation: React.FC<Props> = ({ | |||
allStaff = mockStaffs, | |||
initiallySelectedStaff, | |||
}) => { | |||
const StaffAllocation: React.FC<Props> = ({ allStaff = mockStaffs }) => { | |||
const { t } = useTranslation(); | |||
const { setValue, getValues } = useFormContext<CreateProjectInputs>(); | |||
const [filteredStaff, setFilteredStaff] = React.useState(allStaff); | |||
const [selectedStaff, setSelectedStaff] = React.useState< | |||
typeof filteredStaff | |||
>(initiallySelectedStaff); | |||
>( | |||
allStaff.filter((staff) => | |||
getValues("allocatedStaffIds").includes(staff.id), | |||
), | |||
); | |||
// Adding / Removing staff | |||
const addStaff = React.useCallback((staff: StaffResult) => { | |||
@@ -114,6 +118,13 @@ const StaffAllocation: React.FC<Props> = ({ | |||
const clearStaff = React.useCallback(() => { | |||
setSelectedStaff([]); | |||
}, []); | |||
// Sync with form | |||
useEffect(() => { | |||
setValue( | |||
"allocatedStaffIds", | |||
selectedStaff.map((staff) => staff.id), | |||
); | |||
}, [selectedStaff, setValue]); | |||
const staffPoolColumns = React.useMemo<Column<StaffResult>[]>( | |||
() => [ | |||
@@ -196,7 +207,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||
const q = query.toLowerCase(); | |||
return ( | |||
(staff.name.toLowerCase().includes(q) || | |||
staff.id.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]; | |||
@@ -279,7 +290,9 @@ const StaffAllocation: React.FC<Props> = ({ | |||
</Grid> | |||
<Tabs value={tabIndex} onChange={handleTabChange}> | |||
<Tab label={t("Staff Pool")} /> | |||
<Tab label={`${t("Allocated Staff")} (${selectedStaff.length})`} /> | |||
<Tab | |||
label={`${t("Allocated Staff")} (${selectedStaff.length})`} | |||
/> | |||
</Tabs> | |||
<Box sx={{ marginInline: -3 }}> | |||
{tabIndex === 0 && ( | |||
@@ -7,16 +7,35 @@ import Typography from "@mui/material/Typography"; | |||
import { useTranslation } from "react-i18next"; | |||
import TransferList from "../TransferList"; | |||
import Button from "@mui/material/Button"; | |||
import React from "react"; | |||
import React, { useMemo } from "react"; | |||
import CardActions from "@mui/material/CardActions"; | |||
import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
import FormControl from "@mui/material/FormControl"; | |||
import Select from "@mui/material/Select"; | |||
import MenuItem from "@mui/material/MenuItem"; | |||
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 { 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 ( | |||
<Card> | |||
@@ -45,15 +64,22 @@ const TaskSetup = () => { | |||
</Grid> | |||
</Grid> | |||
<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")} | |||
selectedItemsLabel={t("Project Task List")} | |||
/> | |||
@@ -11,7 +11,7 @@ import Divider from "@mui/material/Divider"; | |||
import ChevronLeft from "@mui/icons-material/ChevronLeft"; | |||
import ChevronRight from "@mui/icons-material/ChevronRight"; | |||
import intersection from "lodash/intersection"; | |||
import difference from "lodash/difference"; | |||
import differenceBy from "lodash/differenceBy"; | |||
import Stack from "@mui/material/Stack"; | |||
import Paper from "@mui/material/Paper"; | |||
import Typography from "@mui/material/Typography"; | |||
@@ -163,7 +163,7 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]); | |||
const [leftList, setLeftList] = React.useState<LabelWithId[]>( | |||
difference(allItems, initiallySelectedItems), | |||
differenceBy(allItems, initiallySelectedItems, "id"), | |||
); | |||
const [rightList, setRightList] = React.useState<LabelWithId[]>( | |||
initiallySelectedItems, | |||
@@ -176,7 +176,7 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
(value: LabelWithId) => () => { | |||
const isChecked = checkedList.includes(value); | |||
const newCheckedList = isChecked | |||
? difference(checkedList, [value]) | |||
? differenceBy(checkedList, [value], "id") | |||
: [...checkedList, value]; | |||
setCheckedList(newCheckedList); | |||
@@ -187,7 +187,7 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
const handleToggleAll = React.useCallback( | |||
(items: LabelWithId[], checkedItems: LabelWithId[]) => () => { | |||
if (checkedItems.length === items.length) { | |||
setCheckedList(difference(checkedList, checkedItems)); | |||
setCheckedList(differenceBy(checkedList, checkedItems, "id")); | |||
} else { | |||
setCheckedList([...checkedList, ...items]); | |||
} | |||
@@ -197,14 +197,16 @@ const TransferList: React.FC<TransferListProps> = ({ | |||
const handleCheckedRight = () => { | |||
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 = () => { | |||
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(() => { | |||