diff --git a/src/app/(main)/projects/create/sub/not-found.tsx b/src/app/(main)/projects/create/sub/not-found.tsx
new file mode 100644
index 0000000..1cc4df3
--- /dev/null
+++ b/src/app/(main)/projects/create/sub/not-found.tsx
@@ -0,0 +1,17 @@
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography, Link } from "@mui/material";
+import NextLink from "next/link";
+
+export default async function NotFound() {
+ const { t } = await getServerI18n("projects", "common");
+
+ return (
+
+ {t("Not Found")}
+ {t("The sub project was not found or there was no any main projects!")}
+
+ {t("Return to all projects")}
+
+
+ );
+}
diff --git a/src/app/(main)/projects/create/sub/page.tsx b/src/app/(main)/projects/create/sub/page.tsx
new file mode 100644
index 0000000..bd5837e
--- /dev/null
+++ b/src/app/(main)/projects/create/sub/page.tsx
@@ -0,0 +1,53 @@
+import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
+import { fetchGrades } from "@/app/api/grades";
+import {
+ fetchProjectBuildingTypes,
+ fetchProjectCategories,
+ fetchProjectContractTypes,
+ fetchProjectFundingTypes,
+ fetchProjectLocationTypes,
+ fetchProjectServiceTypes,
+ fetchProjectWorkNatures,
+ fetchProjects,
+} from "@/app/api/projects";
+import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
+import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
+import CreateProject from "@/components/CreateProject";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import Typography from "@mui/material/Typography";
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Create Project",
+};
+
+const Projects: React.FC = async () => {
+ const { t } = await getServerI18n("projects");
+
+ // Preload necessary dependencies
+ fetchAllTasks();
+ fetchTaskTemplates();
+ fetchProjectCategories();
+ fetchProjectContractTypes();
+ fetchProjectFundingTypes();
+ fetchProjectLocationTypes();
+ fetchProjectServiceTypes();
+ fetchProjectBuildingTypes();
+ fetchProjectWorkNatures();
+ fetchAllCustomers();
+ fetchAllSubsidiaries();
+ fetchGrades();
+ preloadTeamLeads();
+ preloadStaff();
+
+ return (
+ <>
+ {t("Create Sub Project")}
+
+
+
+ >
+ );
+};
+
+export default Projects;
diff --git a/src/app/(main)/projects/page.tsx b/src/app/(main)/projects/page.tsx
index 1fe1800..129d601 100644
--- a/src/app/(main)/projects/page.tsx
+++ b/src/app/(main)/projects/page.tsx
@@ -1,7 +1,8 @@
-import { preloadProjects } from "@/app/api/projects";
+import { fetchProjectCategories, fetchProjects, preloadProjects } from "@/app/api/projects";
import ProjectSearch from "@/components/ProjectSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
+import { ButtonGroup } from "@mui/material";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
@@ -15,7 +16,9 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");
- preloadProjects();
+ // preloadProjects();
+ fetchProjectCategories();
+ const projects = await fetchProjects();
return (
<>
@@ -28,14 +31,31 @@ const Projects: React.FC = async () => {
{t("Projects")}
- }
- LinkComponent={Link}
- href="/projects/create"
+
- {t("Create Project")}
-
+ {projects.filter(project => project.status.toLowerCase() !== "deleted").length > 0 && }
+ LinkComponent={Link}
+ href="/projects/create/sub"
+ >
+ {t("Create Sub Project")}
+ }
+ }
+ LinkComponent={Link}
+ href="/projects/create"
+ >
+ {t("Create Project")}
+
+
}>
diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts
index 90d1f4a..7a64ea1 100644
--- a/src/app/api/projects/index.ts
+++ b/src/app/api/projects/index.ts
@@ -81,6 +81,12 @@ export const fetchProjects = cache(async () => {
});
});
+export const fetchMainProjects = cache(async () => {
+ return serverFetchJson(`${BASE_API_URL}/projects/main`, {
+ next: { tags: ["projects"] },
+ });
+});
+
export const fetchProjectCategories = cache(async () => {
return serverFetchJson(
`${BASE_API_URL}/projects/categories`,
diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
new file mode 100644
index 0000000..d8a2962
--- /dev/null
+++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
@@ -0,0 +1,97 @@
+"use client"
+
+import { Autocomplete, MenuItem, TextField, Checkbox } from "@mui/material";
+import { Controller, FieldValues, Path, Control, RegisterOptions } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
+import CheckBoxIcon from '@mui/icons-material/CheckBox';
+
+const icon = ;
+const checkedIcon = ;
+// label -> e.g. code - name -> 001 - WL
+// name -> WL
+interface Props {
+ control: Control,
+ options: T[],
+ name: Path, // register name
+ label?: string, // display label
+ noOptionsText?: string,
+ isMultiple?: boolean,
+ rules?: RegisterOptions
+ error?: boolean,
+}
+
+function ControlledAutoComplete<
+ T extends { id?: number | string; label?: string; name?: string },
+ TField extends FieldValues
+>(
+ props: Props
+) {
+ const { t } = useTranslation()
+ const { control, options, name, label, noOptionsText, isMultiple, rules, error } = props;
+
+ return (
+ (
+ isMultiple ?
+ {
+ // console.log(field.value)
+ return field.value?.includes(option.id)
+ })}
+ options={options}
+ getOptionLabel={(option) => option.label ?? option.name!!}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ renderOption={(params, option, { selected }) => {
+ return (
+
+
+ {option.label ?? option.name}
+
+ );
+ }}
+ onChange={(event, value) => {
+ field.onChange(value?.map(v => v.id))
+ }}
+ renderInput={(params) => }
+ />
+ :
+ option.id === field.value) ?? options[0]}
+ options={options}
+ getOptionLabel={(option) => option.label ?? option.name!!}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ renderOption={(params, option) => {
+ return (
+
+ );
+ }}
+ onChange={(event, value) => {
+ field.onChange(value?.id)
+ }}
+ renderInput={(params) => }
+ />
+ )}
+ />
+ )
+}
+
+export default ControlledAutoComplete;
\ No newline at end of file
diff --git a/src/components/ControlledAutoComplete/index.ts b/src/components/ControlledAutoComplete/index.ts
new file mode 100644
index 0000000..7e12f58
--- /dev/null
+++ b/src/components/ControlledAutoComplete/index.ts
@@ -0,0 +1 @@
+export { default } from "./ControlledAutoComplete";
\ No newline at end of file
diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx
index fac9c79..937bac5 100644
--- a/src/components/CreateProject/CreateProject.tsx
+++ b/src/components/CreateProject/CreateProject.tsx
@@ -269,6 +269,14 @@ const CreateProject: React.FC = ({
milestones: {},
totalManhour: 0,
taskTemplateId: "All",
+ projectCategoryId: projectCategories.length > 0 ? projectCategories[0].id : undefined,
+ projectLeadId: teamLeads.length > 0 ? teamLeads[0].id : undefined,
+ serviceTypeId: serviceTypes.length > 0 ? serviceTypes[0].id : undefined,
+ fundingTypeId: fundingTypes.length > 0 ? fundingTypes[0].id : undefined,
+ contractTypeId: contractTypes.length > 0 ? contractTypes[0].id : undefined,
+ locationId: locationTypes.length > 0 ? locationTypes[0].id : undefined,
+ clientSubsidiaryId: undefined,
+ clientId: allCustomers.length > 0 ? allCustomers[0].id : undefined,
...defaultInputs,
// manhourPercentageByGrade should have a sensible default
diff --git a/src/components/CreateProject/ProjectClientDetails.tsx b/src/components/CreateProject/ProjectClientDetails.tsx
index aba8916..d77defe 100644
--- a/src/components/CreateProject/ProjectClientDetails.tsx
+++ b/src/components/CreateProject/ProjectClientDetails.tsx
@@ -31,8 +31,9 @@ import { Contact, Customer, Subsidiary } from "@/app/api/customer";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import { fetchCustomer } from "@/app/api/customer/actions";
-import { Checkbox, ListItemText } from "@mui/material";
+import { Autocomplete, Checkbox, ListItemText } from "@mui/material";
import uniq from "lodash/uniq";
+import ControlledAutoComplete from "../ControlledAutoComplete/ControlledAutoComplete";
interface Props {
isActive: boolean;
@@ -64,7 +65,7 @@ const ProjectClientDetails: React.FC = ({
const { t } = useTranslation();
const {
register,
- formState: { errors },
+ formState: { errors, defaultValues },
watch,
control,
setValue,
@@ -108,8 +109,9 @@ const ProjectClientDetails: React.FC = ({
setCustomerContacts(contacts);
setCustomerSubsidiaryIds(subsidiaryIds);
- if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0])
- else setValue("clientSubsidiaryId", undefined)
+ // if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0])
+ // else
+ setValue("clientSubsidiaryId", undefined)
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
// else setValue("clientContactId", undefined)
});
@@ -122,18 +124,19 @@ const ProjectClientDetails: React.FC = ({
// get subsidiary contact combo
const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!!
setSubsidiaryContacts(contacts)
- setValue("clientContactId", contacts[0].id)
+ setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && Boolean(defaultValues?.clientSubsidiaryId) ? contacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? contacts[0].id : contacts[0].id)
setValue("isSubsidiaryContact", true)
} else if (customerContacts?.length > 0) {
setSubsidiaryContacts([])
- setValue("clientContactId", customerContacts[0].id)
+ setValue("clientContactId", selectedCustomerId === defaultValues?.clientId && !Boolean(defaultValues?.clientSubsidiaryId) ? customerContacts.find(contact => contact.id === defaultValues.clientContactId)?.id ?? customerContacts[0].id : customerContacts[0].id)
setValue("isSubsidiaryContact", false)
}
- }, [customerContacts, clientSubsidiaryId]);
+ }, [customerContacts, clientSubsidiaryId, selectedCustomerId]);
// Automatically add the team lead to the allocated staff list
const selectedTeamLeadId = watch("projectLeadId");
useEffect(() => {
+ console.log(selectedTeamLeadId)
if (selectedTeamLeadId !== undefined) {
const currentStaffIds = getValues("allocatedStaffIds");
const newList = uniq([...currentStaffIds, selectedTeamLeadId]);
@@ -141,15 +144,15 @@ const ProjectClientDetails: React.FC = ({
}
}, [getValues, selectedTeamLeadId, setValue]);
- const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
- (acc, building) => ({ ...acc, [building.id]: building.name }),
- {},
- );
+ // const buildingTypeIdNameMap = buildingTypes.reduce<{ [id: number]: string }>(
+ // (acc, building) => ({ ...acc, [building.id]: building.name }),
+ // {},
+ // );
- const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
- (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
- {},
- );
+ // const workNatureIdNameMap = workNatures.reduce<{ [id: number]: string }>(
+ // (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
+ // {},
+ // );
return (
@@ -164,12 +167,12 @@ const ProjectClientDetails: React.FC = ({
label={t("Project Code")}
fullWidth
disabled
- {...register("projectCode",
- // {
- // required: "Project code required!",
- // }
+ {...register("projectCode",
+ // {
+ // required: "Project code required!",
+ // }
)}
- // error={Boolean(errors.projectCode)}
+ // error={Boolean(errors.projectCode)}
/>
@@ -183,184 +186,79 @@ const ProjectClientDetails: React.FC = ({
/>
-
- {t("Project Category")}
- (
-
- )}
- />
-
+
-
- {t("Team Lead")}
- (
-
- )}
- />
-
+ ({ ...staff, label: `${staff.staffId} - ${staff.name} (${staff.team})` }))}
+ name="projectLeadId"
+ label={t("Team Lead")}
+ noOptionsText={t("No Team Lead")}
+ />
-
- {t("Service Type")}
- (
-
- )}
- />
-
+
-
- {t("Funding Type")}
- (
-
- )}
- />
-
+
-
- {t("Contract Type")}
- (
-
- )}
- />
-
+
-
- {t("Location")}
- (
-
- )}
- />
-
+
-
- {t("Building Types")}
- (
-
- )}
- />
-
+
-
- {t("Work Nature")}
- (
-
- )}
- />
-
+
@@ -383,13 +281,13 @@ const ProjectClientDetails: React.FC = ({
-
-
- {t("CLP Project")}
-
+
+
+ {t("CLP Project")}
+
@@ -410,29 +308,17 @@ const ProjectClientDetails: React.FC = ({
-
- {t("Client")}
- (
-
- )}
- />
-
+ ({ ...customer, label: `${customer.code} - ${customer.name}` }))}
+ name="clientId"
+ label={t("Client")}
+ noOptionsText={t("No Client")}
+ rules={{
+ required: "Please select a client"
+ }}
+ error={Boolean(errors.clientId)}
+ />
@@ -448,96 +334,42 @@ const ProjectClientDetails: React.FC = ({
{customerContacts.length > 0 && (
<>
- {customerSubsidiaryIds.length > 0 && (
-
-
- {t("Client Subsidiary")}
- {
- // if (
- // !customerSubsidiaryIds.find(
- // (subsidiaryId) => subsidiaryId === value,
- // )
- // ) {
- // return t("Please choose a valid subsidiary");
- // } else return true;
- // },
- // }}
- defaultValue={customerSubsidiaryIds[0]}
- control={control}
- name="clientSubsidiaryId"
- render={({ field }) => (
-
- )}
- />
-
-
- )}
- subsidiaryMap[subId])
+ .map((subsidiaryId, index) => {
+ const subsidiary = subsidiaryMap[subsidiaryId]
+ return { id: subsidiary.id, label: `${subsidiary.code} - ${subsidiary.name}` }
+ })]}
+ name="clientSubsidiaryId"
+ label={t("Client Subsidiary")}
+ noOptionsText={t("No Client Subsidiary")}
+ />
+
+
+ {
+ if (
+ (customerContacts.length > 0 && !customerContacts.find(
+ (contact) => contact.id === value,
+ )) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find(
+ (contact) => contact.id === value,
+ ))
+ ) {
+ return t("Please provide a valid contact");
+ } else return true;
+ },
+ }}
error={Boolean(errors.clientContactId)}
- >
- {t("Client Lead")}
- {
- if (
- (customerContacts.length > 0 && !customerContacts.find(
- (contact) => contact.id === value,
- )) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find(
- (contact) => contact.id === value,
- ))
- ) {
- return t("Please provide a valid contact");
- } else return true;
- },
- }}
- defaultValue={subsidiaryContacts?.length > 0 ? subsidiaryContacts[0].id : customerContacts[0].id}
- control={control}
- name="clientContactId"
- render={({ field }) => (
-
- )}
- />
-
+ />
diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx
index d8f502c..686eb8d 100644
--- a/src/components/SearchBox/SearchBox.tsx
+++ b/src/components/SearchBox/SearchBox.tsx
@@ -73,14 +73,20 @@ function SearchBox({
() =>
criteria.reduce>(
(acc, c) => {
- return { ...acc, [c.paramName]: c.type === "select" ? !(c.needAll === false) ? "All" : c.options[0] : "" };
+ return {
+ ...acc,
+ [c.paramName]: c.type === "select" ?
+ !(c.needAll === false) ? "All" :
+ c.options.length > 0 ? c.options[0] : ""
+ : ""
+ };
},
{} as Record
),
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
-
+
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler => {
return (e) => {
@@ -226,22 +232,22 @@ function SearchBox({
);
})}
-
- }
- onClick={handleReset}
- >
- {t("Reset")}
-
- ) || }
- onClick={handleSearch}
- >
- {(formType === "download" && t("Download")) || t("Search")}
-
-
+
+ }
+ onClick={handleReset}
+ >
+ {t("Reset")}
+
+ ) || }
+ onClick={handleSearch}
+ >
+ {(formType === "download" && t("Download")) || t("Search")}
+
+
);