Selaa lähdekoodia

Use task templates in project creation

tags/Baseline_30082024_FRONTEND_UAT
Wayne 1 vuosi sitten
vanhempi
commit
700bc98c72
15 muutettua tiedostoa jossa 197 lisäystä ja 86 poistoa
  1. +1
    -0
      next.config.js
  2. +7
    -0
      src/app/(main)/projects/create/page.tsx
  3. +1
    -1
      src/app/api/projects/actions.ts
  4. +15
    -0
      src/app/api/projects/index.ts
  5. +1
    -0
      src/app/api/tasks/index.ts
  6. +22
    -4
      src/components/CreateProject/CreateProject.tsx
  7. +11
    -2
      src/components/CreateProject/CreateProjectWrapper.tsx
  8. +27
    -10
      src/components/CreateProject/ProjectClientDetails.tsx
  9. +9
    -5
      src/components/CreateProject/ResourceMilestone.tsx
  10. +54
    -16
      src/components/CreateProject/TaskSetup.tsx
  11. +3
    -3
      src/components/LoginPage/LoginPage.tsx
  12. +8
    -7
      src/components/ProjectSearch/ProjectSearch.tsx
  13. +3
    -2
      src/components/ProjectSearch/ProjectSearchWrapper.tsx
  14. +24
    -22
      src/components/TransferList/MultiSelectList.tsx
  15. +11
    -14
      src/components/TransferList/TransferList.tsx

+ 1
- 0
next.config.js Näytä tiedosto

@@ -5,6 +5,7 @@ const withPWA = require("next-pwa")({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development'
});

const nextConfig = {


+ 7
- 0
src/app/(main)/projects/create/page.tsx Näytä tiedosto

@@ -1,3 +1,5 @@
import { fetchProjectCategories } from "@/app/api/projects";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import Typography from "@mui/material/Typography";
@@ -10,6 +12,11 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");

// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();
fetchProjectCategories();

return (
<>
<Typography variant="h4">{t("Create Project")}</Typography>


+ 1
- 1
src/app/api/projects/actions.ts Näytä tiedosto

@@ -8,7 +8,7 @@ export interface CreateProjectInputs {
// Project details
projectCode: string;
projectName: string;
projectCategory: string;
projectCategoryId: number;
projectDescription: string;

// Client details


+ 15
- 0
src/app/api/projects/index.ts Näytä tiedosto

@@ -10,7 +10,13 @@ export interface ProjectResult {
client: string;
}

export interface ProjectCategory {
id: number;
label: string;
}

export const preloadProjects = () => {
fetchProjectCategories();
fetchProjects();
};

@@ -18,6 +24,15 @@ export const fetchProjects = cache(async () => {
return mockProjects;
});

export const fetchProjectCategories = cache(async () => {
return mockProjectCategories;
});

const mockProjectCategories: ProjectCategory[] = [
{ id: 1, label: "Confirmed Project" },
{ id: 2, label: "Project to be bidded" },
];

const mockProjects: ProjectResult[] = [
{
id: 1,


+ 1
- 0
src/app/api/tasks/index.ts Näytä tiedosto

@@ -19,6 +19,7 @@ export interface TaskTemplate {
id: number;
code: string;
name: string;
tasks: Task[];
}

export const preloadTaskTemplates = () => {


+ 22
- 4
src/components/CreateProject/CreateProject.tsx Näytä tiedosto

@@ -13,7 +13,7 @@ import ProjectClientDetails from "./ProjectClientDetails";
import TaskSetup from "./TaskSetup";
import StaffAllocation from "./StaffAllocation";
import ResourceMilestone from "./ResourceMilestone";
import { Task } from "@/app/api/tasks";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
FieldErrors,
FormProvider,
@@ -23,9 +23,12 @@ import {
} from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";

export interface Props {
allTasks: Task[];
projectCategories: ProjectCategory[];
taskTemplates: TaskTemplate[];
}

const hasErrorsInTab = (
@@ -40,7 +43,11 @@ const hasErrorsInTab = (
}
};

const CreateProject: React.FC<Props> = ({ allTasks }) => {
const CreateProject: React.FC<Props> = ({
allTasks,
projectCategories,
taskTemplates,
}) => {
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const router = useRouter();
@@ -101,8 +108,19 @@ const CreateProject: React.FC<Props> = ({ allTasks }) => {
<Tab label={t("Staff Allocation")} iconPosition="end" />
<Tab label={t("Resource and Milestone")} iconPosition="end" />
</Tabs>
{<ProjectClientDetails isActive={tabIndex === 0} />}
{<TaskSetup allTasks={allTasks} isActive={tabIndex === 1} />}
{
<ProjectClientDetails
projectCategories={projectCategories}
isActive={tabIndex === 0}
/>
}
{
<TaskSetup
allTasks={allTasks}
taskTemplates={taskTemplates}
isActive={tabIndex === 1}
/>
}
{<StaffAllocation isActive={tabIndex === 2} />}
{<ResourceMilestone allTasks={allTasks} isActive={tabIndex === 3} />}
<Stack direction="row" justifyContent="flex-end" gap={1}>


+ 11
- 2
src/components/CreateProject/CreateProjectWrapper.tsx Näytä tiedosto

@@ -1,10 +1,19 @@
import { fetchAllTasks } from "@/app/api/tasks";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import CreateProject from "./CreateProject";
import { fetchProjectCategories } from "@/app/api/projects";

const CreateProjectWrapper: React.FC = async () => {
const tasks = await fetchAllTasks();
const taskTemplates = await fetchTaskTemplates();
const projectCategories = await fetchProjectCategories();

return <CreateProject allTasks={tasks} />;
return (
<CreateProject
allTasks={tasks}
projectCategories={projectCategories}
taskTemplates={taskTemplates}
/>
);
};

export default CreateProjectWrapper;

+ 27
- 10
src/components/CreateProject/ProjectClientDetails.tsx Näytä tiedosto

@@ -15,16 +15,24 @@ 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 { Controller, useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import { ProjectCategory } from "@/app/api/projects";

const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({
interface Props {
isActive: boolean;
projectCategories: ProjectCategory[];
}

const ProjectClientDetails: React.FC<Props> = ({
isActive,
projectCategories,
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
control,
} = useFormContext<CreateProjectInputs>();

return (
@@ -55,14 +63,23 @@ const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Project Category")}</InputLabel>
<Select
label={t("Project Category")}
value={"Temporary Project"}
>
<MenuItem value={"Temporary Project"}>
{t("Temporary Project")}
</MenuItem>
</Select>
<Controller
defaultValue={projectCategories[0].id}
control={control}
name="projectCategoryId"
render={({ field }) => (
<Select label={t("Project Category")} {...field}>
{projectCategories.map((category, index) => (
<MenuItem
key={`${category.id}-${index}`}
value={category.id}
>
{t(category.label)}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item xs={6}>


+ 9
- 5
src/components/CreateProject/ResourceMilestone.tsx Näytä tiedosto

@@ -81,11 +81,15 @@ const ResourceMilestone: React.FC<Props> = ({
))}
</Select>
</FormControl>
<ResourceSection
tasks={currentTasks}
manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
<MilestoneSection taskGroupId={currentTaskGroupId} />
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && (
<ResourceSection
tasks={currentTasks}
manhourBreakdownByGrade={defaultManhourBreakdownByGrade}
/>
)}
{/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
{isActive && <MilestoneSection taskGroupId={currentTaskGroupId} />}
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
{t("Reset")}


+ 54
- 16
src/components/CreateProject/TaskSetup.tsx Näytä tiedosto

@@ -7,31 +7,65 @@ import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import TransferList from "../TransferList";
import Button from "@mui/material/Button";
import React, { useMemo } from "react";
import React, { useCallback, useMemo, useState } 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 Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import InputLabel from "@mui/material/InputLabel";
import { Task } from "@/app/api/tasks";
import { Task, TaskTemplate } from "@/app/api/tasks";
import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions";
import isNumber from "lodash/isNumber";

interface Props {
allTasks: Task[];
taskTemplates: TaskTemplate[];
isActive: boolean;
}

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

const items = useMemo(
() => tasks.map((t) => ({ id: t.id, label: t.name, group: t.taskGroup })),
[tasks],
const onReset = useCallback(() => {
setValue("tasks", {});
}, [setValue]);

const [selectedTaskTemplateId, setSelectedTaskTemplateId] = useState<
"All" | number
>("All");
const onSelectTaskTemplate = useCallback(
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
setSelectedTaskTemplateId(e.target.value);
onReset();
}
},
[onReset],
);

const items = useMemo(() => {
const taskList =
selectedTaskTemplateId === "All"
? tasks
: taskTemplates.find(
(template) => template.id === selectedTaskTemplateId,
)?.tasks || tasks;

return taskList.map((t) => ({
id: t.id,
label: t.name,
group: t.taskGroup,
}));
}, [tasks, selectedTaskTemplateId, taskTemplates]);

const selectedItems = useMemo(() => {
return tasks
.filter((t) => currentTasks[t.id])
@@ -53,20 +87,24 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => {
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Task List Source")}</InputLabel>
<Select
<Select<"All" | number>
label={t("Task List Source")}
value={"M1009 - Consultancy Project X Temp"}
value={selectedTaskTemplateId}
onChange={onSelectTaskTemplate}
>
<MenuItem value={"M1009 - Consultancy Project X Temp"}>
{"M1009 - Consultancy Project X Temp"}
</MenuItem>
<MenuItem value={"All"}>{t("All tasks")}</MenuItem>
{taskTemplates.map((template, index) => (
<MenuItem key={`${template.id}-${index}`} value={template.id}>
{template.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
<TransferList
allItems={items}
initiallySelectedItems={selectedItems}
selectedItems={selectedItems}
onChange={(selectedTasks) => {
const newTasks = selectedTasks.reduce<CreateProjectInputs["tasks"]>(
(acc, item) => {
@@ -85,7 +123,7 @@ const TaskSetup: React.FC<Props> = ({ allTasks: tasks, isActive }) => {
selectedItemsLabel={t("Project Task List")}
/>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button variant="text" startIcon={<RestartAlt />}>
<Button variant="text" startIcon={<RestartAlt />} onClick={onReset}>
{t("Reset")}
</Button>
</CardActions>


+ 3
- 3
src/components/LoginPage/LoginPage.tsx Näytä tiedosto

@@ -10,10 +10,10 @@ const LoginPage = () => {
<Grid item sm sx={{ backgroundColor: 'neutral.900'}}>
</Grid>
<Grid item xs={12} sm={8} lg={5}>
<Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', svg: { maxHeight: 120 } }}>
<Logo />
</Box>
<Paper square sx={{ height: "100%" }}>
<Box sx={{ width: '100%', padding: 5, paddingBlockStart: 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', svg: { maxHeight: 120 } }}>
<Logo />
</Box>
<LoginForm />
</Paper>
</Grid>


+ 8
- 7
src/components/ProjectSearch/ProjectSearch.tsx Näytä tiedosto

@@ -1,23 +1,24 @@
"use client";

import { ProjectResult } from "@/app/api/projects";
import { ProjectCategory, ProjectResult } from "@/app/api/projects";
import React, { useCallback, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from 'lodash/uniq';

interface Props {
projects: ProjectResult[];
projectCategories: ProjectCategory[];
}

type SearchQuery = Partial<Omit<ProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProjectSearch: React.FC<Props> = ({ projects }) => {
const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
const { t } = useTranslation("projects");

// If project searching is done on the server-side, then no need for this.
const [filteredProjects, setFilteredProjects] = useState(projects);

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -28,22 +29,22 @@ const ProjectSearch: React.FC<Props> = ({ projects }) => {
label: t("Client name"),
paramName: "client",
type: "select",
options: ["Client A", "Client B", "Client C"],
options: uniq(projects.map((project) => project.client)),
},
{
label: t("Project category"),
paramName: "category",
type: "select",
options: ["Confirmed Project", "Project to be bidded"],
options: projectCategories.map((category) => category.label),
},
{
label: t("Team"),
paramName: "team",
type: "select",
options: ["TW", "WY"],
options: uniq(projects.map((project) => project.team)),
},
],
[t],
[t, projectCategories, projects],
);

const onReset = useCallback(() => {


+ 3
- 2
src/components/ProjectSearch/ProjectSearchWrapper.tsx Näytä tiedosto

@@ -1,4 +1,4 @@
import { fetchProjects } from "@/app/api/projects";
import { fetchProjectCategories, fetchProjects } from "@/app/api/projects";
import React from "react";
import ProjectSearch from "./ProjectSearch";
import ProjectSearchLoading from "./ProjectSearchLoading";
@@ -8,9 +8,10 @@ interface SubComponents {
}

const ProjectSearchWrapper: React.FC & SubComponents = async () => {
const projectCategories = await fetchProjectCategories();
const projects = await fetchProjects();

return <ProjectSearch projects={projects} />;
return <ProjectSearch projects={projects} projectCategories={projectCategories} />;
};

ProjectSearchWrapper.Loading = ProjectSearchLoading;


+ 24
- 22
src/components/TransferList/MultiSelectList.tsx Näytä tiedosto

@@ -11,11 +11,11 @@ import {
ListSubheader,
MenuItem,
Select,
SelectProps,
SelectChangeEvent,
Stack,
Typography,
} from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback } from "react";
import { LabelGroup, LabelWithId, TransferListProps } from "./TransferList";
import { useTranslation } from "react-i18next";
import uniqBy from "lodash/uniqBy";
@@ -23,7 +23,7 @@ import groupBy from "lodash/groupBy";

export const MultiSelectList: React.FC<TransferListProps> = ({
allItems,
initiallySelectedItems,
selectedItems,
selectedItemsLabel,
allItemsLabel,
onChange,
@@ -39,33 +39,31 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
(a: number, b: number) => sortMap[a].index - sortMap[b].index,
[sortMap],
);
const [selectedItems, setSelectedItems] = useState(
initiallySelectedItems.map((item) => item.id),

const handleChange = useCallback(
(event: SelectChangeEvent<number[]>) => {
const {
target: { value },
} = event;
const selectedValues =
typeof value === "string" ? [Number(value)] : value;

onChange(allItems.filter((item) => selectedValues.includes(item.id)));
},
[allItems, onChange],
);
const handleChange = useCallback<
NonNullable<SelectProps<typeof selectedItems>["onChange"]>
>((event) => {
const {
target: { value },
} = event;
setSelectedItems(typeof value === "string" ? [Number(value)] : value);
}, []);

const handleToggleAll = useCallback(
() => () => {
if (selectedItems.length === allItems.length) {
setSelectedItems([]);
onChange([]);
} else {
setSelectedItems(allItems.map((item) => item.id));
onChange(allItems);
}
},
[allItems, selectedItems.length],
[allItems, onChange, selectedItems.length],
);

useEffect(() => {
onChange(selectedItems.map((item) => sortMap[item]));
}, [onChange, selectedItems, sortMap]);

const { t } = useTranslation();
const groups: LabelGroup[] = uniqBy(
[
@@ -85,7 +83,7 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
<InputLabel>{selectedItemsLabel}</InputLabel>
<Select
multiple
value={selectedItems}
value={selectedItems.map((item) => item.id)}
onChange={handleChange}
renderValue={(values) => {
return (
@@ -153,7 +151,11 @@ export const MultiSelectList: React.FC<TransferListProps> = ({
return (
<MenuItem key={item.id} value={item.id} disableRipple>
<Checkbox
checked={selectedItems.includes(item.id)}
checked={Boolean(
selectedItems.find(
(selected) => selected.id === item.id,
),
)}
disableRipple
/>
<ListItemText sx={{ whiteSpace: "normal" }}>


+ 11
- 14
src/components/TransferList/TransferList.tsx Näytä tiedosto

@@ -33,7 +33,7 @@ export interface LabelWithId {

export interface TransferListProps {
allItems: LabelWithId[];
initiallySelectedItems: LabelWithId[];
selectedItems: LabelWithId[];
onChange: (selectedItems: LabelWithId[]) => void;
allItemsLabel: string;
selectedItemsLabel: string;
@@ -144,7 +144,7 @@ const ItemList: React.FC<ItemListProps> = ({

const TransferList: React.FC<TransferListProps> = ({
allItems,
initiallySelectedItems,
selectedItems,
allItemsLabel,
selectedItemsLabel,
onChange,
@@ -163,12 +163,15 @@ const TransferList: React.FC<TransferListProps> = ({

const [checkedList, setCheckedList] = React.useState<LabelWithId[]>([]);
const [leftList, setLeftList] = React.useState<LabelWithId[]>(
differenceBy(allItems, initiallySelectedItems, "id"),
);
const [rightList, setRightList] = React.useState<LabelWithId[]>(
initiallySelectedItems,
differenceBy(allItems, selectedItems, "id"),
);

React.useEffect(() => {
setLeftList(differenceBy(allItems, selectedItems, "id"));
}, [allItems, selectedItems]);

const rightList = selectedItems;

const leftListChecked = intersection(checkedList, leftList);
const rightListChecked = intersection(checkedList, rightList);

@@ -196,23 +199,17 @@ const TransferList: React.FC<TransferListProps> = ({
);

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

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

React.useEffect(() => {
onChange(rightList);
}, [onChange, rightList]);

return (
<Stack spacing={2} direction="row" alignItems="center" position="relative">
<ItemList


Ladataan…
Peruuta
Tallenna