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 && } 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}