diff --git a/next.config.js b/next.config.js
index a27da5d..ff62a08 100644
--- a/next.config.js
+++ b/next.config.js
@@ -5,6 +5,7 @@ const withPWA = require("next-pwa")({
dest: "public",
register: true,
skipWaiting: true,
+ disable: process.env.NODE_ENV === 'development'
});
const nextConfig = {
diff --git a/src/app/(main)/projects/create/page.tsx b/src/app/(main)/projects/create/page.tsx
index 60ab586..0679d1f 100644
--- a/src/app/(main)/projects/create/page.tsx
+++ b/src/app/(main)/projects/create/page.tsx
@@ -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 (
<>
{t("Create Project")}
diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts
index ef39406..d8106d2 100644
--- a/src/app/api/projects/actions.ts
+++ b/src/app/api/projects/actions.ts
@@ -8,7 +8,7 @@ export interface CreateProjectInputs {
// Project details
projectCode: string;
projectName: string;
- projectCategory: string;
+ projectCategoryId: number;
projectDescription: string;
// Client details
diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts
index a29be24..2fdb597 100644
--- a/src/app/api/projects/index.ts
+++ b/src/app/api/projects/index.ts
@@ -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,
diff --git a/src/app/api/tasks/index.ts b/src/app/api/tasks/index.ts
index f5889d7..f701cca 100644
--- a/src/app/api/tasks/index.ts
+++ b/src/app/api/tasks/index.ts
@@ -19,6 +19,7 @@ export interface TaskTemplate {
id: number;
code: string;
name: string;
+ tasks: Task[];
}
export const preloadTaskTemplates = () => {
diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx
index 39adc96..e5198f9 100644
--- a/src/components/CreateProject/CreateProject.tsx
+++ b/src/components/CreateProject/CreateProject.tsx
@@ -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 = ({ allTasks }) => {
+const CreateProject: React.FC = ({
+ allTasks,
+ projectCategories,
+ taskTemplates,
+}) => {
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const router = useRouter();
@@ -101,8 +108,19 @@ const CreateProject: React.FC = ({ allTasks }) => {
- {}
- {}
+ {
+
+ }
+ {
+
+ }
{}
{}
diff --git a/src/components/CreateProject/CreateProjectWrapper.tsx b/src/components/CreateProject/CreateProjectWrapper.tsx
index f5fbb5a..8c071e6 100644
--- a/src/components/CreateProject/CreateProjectWrapper.tsx
+++ b/src/components/CreateProject/CreateProjectWrapper.tsx
@@ -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 ;
+ return (
+
+ );
};
export default CreateProjectWrapper;
diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx
index 610c15f..e4027ab 100644
--- a/src/components/CreateProject/ProjectClientDetails.tsx
+++ b/src/components/CreateProject/ProjectClientDetails.tsx
@@ -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 = ({
isActive,
+ projectCategories,
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
+ control,
} = useFormContext();
return (
@@ -55,14 +63,23 @@ const ProjectClientDetails: React.FC<{ isActive: boolean }> = ({
{t("Project Category")}
-
+ (
+
+ )}
+ />
diff --git a/src/components/CreateProject/ResourceMilestone.tsx b/src/components/CreateProject/ResourceMilestone.tsx
index 9acf287..99d6b73 100644
--- a/src/components/CreateProject/ResourceMilestone.tsx
+++ b/src/components/CreateProject/ResourceMilestone.tsx
@@ -81,11 +81,15 @@ const ResourceMilestone: React.FC = ({
))}
-
-
+ {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
+ {isActive && (
+
+ )}
+ {/* MUI X-Grid will throw an error if it is rendered without any dimensions; so not rendering when not active */}
+ {isActive && }
}>
{t("Reset")}
diff --git a/src/components/CreateProject/TaskSetup.tsx b/src/components/CreateProject/TaskSetup.tsx
index 41fa76a..f054773 100644
--- a/src/components/CreateProject/TaskSetup.tsx
+++ b/src/components/CreateProject/TaskSetup.tsx
@@ -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 = ({ allTasks: tasks, isActive }) => {
+const TaskSetup: React.FC = ({
+ allTasks: tasks,
+ taskTemplates,
+ isActive,
+}) => {
const { t } = useTranslation();
- const { getValues, setValue } = useFormContext();
- const currentTasks = getValues("tasks");
+ const { setValue, watch } = useFormContext();
+ 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) => {
+ 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 = ({ allTasks: tasks, isActive }) => {
{t("Task List Source")}
-
{
const newTasks = selectedTasks.reduce(
(acc, item) => {
@@ -85,7 +123,7 @@ const TaskSetup: React.FC = ({ allTasks: tasks, isActive }) => {
selectedItemsLabel={t("Project Task List")}
/>
- }>
+ } onClick={onReset}>
{t("Reset")}
diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx
index eb65647..2cb3794 100644
--- a/src/components/LoginPage/LoginPage.tsx
+++ b/src/components/LoginPage/LoginPage.tsx
@@ -10,10 +10,10 @@ const LoginPage = () => {
-
-
-
+
+
+
diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx
index 903f7cc..37b962b 100644
--- a/src/components/ProjectSearch/ProjectSearch.tsx
+++ b/src/components/ProjectSearch/ProjectSearch.tsx
@@ -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>;
type SearchParamNames = keyof SearchQuery;
-const ProjectSearch: React.FC = ({ projects }) => {
+const ProjectSearch: React.FC = ({ 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[] = useMemo(
@@ -28,22 +29,22 @@ const ProjectSearch: React.FC = ({ 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(() => {
diff --git a/src/components/ProjectSearch/ProjectSearchWrapper.tsx b/src/components/ProjectSearch/ProjectSearchWrapper.tsx
index 737a1ef..c4d0211 100644
--- a/src/components/ProjectSearch/ProjectSearchWrapper.tsx
+++ b/src/components/ProjectSearch/ProjectSearchWrapper.tsx
@@ -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 ;
+ return ;
};
ProjectSearchWrapper.Loading = ProjectSearchLoading;
diff --git a/src/components/TransferList/MultiSelectList.tsx b/src/components/TransferList/MultiSelectList.tsx
index b74cfe2..1c8825a 100644
--- a/src/components/TransferList/MultiSelectList.tsx
+++ b/src/components/TransferList/MultiSelectList.tsx
@@ -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 = ({
allItems,
- initiallySelectedItems,
+ selectedItems,
selectedItemsLabel,
allItemsLabel,
onChange,
@@ -39,33 +39,31 @@ export const MultiSelectList: React.FC = ({
(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) => {
+ 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["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 = ({
{selectedItemsLabel}